Convert `Ui/Poll/Editor` to TypeScript (#3690)
authorMatthias Schmidt <gravatronics@live.com>
Wed, 4 Nov 2020 15:15:13 +0000 (16:15 +0100)
committerGitHub <noreply@github.com>
Wed, 4 Nov 2020 15:15:13 +0000 (16:15 +0100)
* Convert `Ui/Poll/Editor` to TypeScript

* Fix eslint issues in `Ui/Poll/Editor`

* Apply changes from code review

* Use prettier on `Ui/Poll/Editor`

* Scope variables in case statements in `Ui/Poll/Editor`

* Fix tsc errors in `Ui/Poll/Editor`

wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Poll/Editor.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Poll/Editor.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Poll/Editor.ts [new file with mode: 0644]

index 5a866f21dde997b90ce2407fa649fa34458506a1..dce03de84068e948ecc0594be938f97f93d4c7f3 100644 (file)
 /**
  * Handles the data to create and edit a poll in a form created via form builder.
  *
- * @author     Alexander Ebert, Matthias Schmidt
- * @copyright  2001-2020 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLabSuite/Core/Ui/Poll/Editor
- * @since      5.2
+ * @author  Alexander Ebert, Matthias Schmidt
+ * @copyright  2001-2020 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Ui/Poll/Editor
  */
-define([
-    'Core',
-    'Dom/Util',
-    'EventHandler',
-    'EventKey',
-    'Language',
-    'WoltLabSuite/Core/Date/Picker',
-    'WoltLabSuite/Core/Ui/Sortable/List'
-], function (Core, DomUtil, EventHandler, EventKey, Language, DatePicker, UiSortableList) {
+define(["require", "exports", "tslib", "../../Core", "../../Language", "../Sortable/List", "../../Event/Handler", "../../Date/Picker"], function (require, exports, tslib_1, Core, Language, List_1, EventHandler, DatePicker) {
     "use strict";
-    function UiPollEditor(containerId, pollOptions, wysiwygId, options) {
-        this.init(containerId, pollOptions, wysiwygId, options);
-    }
-    UiPollEditor.prototype = {
-        /**
-         * Initializes the poll editor.
-         *
-         * @param      {string}        containerId     id of the poll options container
-         * @param      {object[]}      pollOptions     existing poll options
-         * @param      {string}        wysiwygId       id of the related wysiwyg editor
-         * @param      {object}        options         additional poll options
-         */
-        init: function (containerId, pollOptions, wysiwygId, options) {
-            this._container = elById(containerId);
-            if (this._container === null) {
+    Core = tslib_1.__importStar(Core);
+    Language = tslib_1.__importStar(Language);
+    List_1 = tslib_1.__importDefault(List_1);
+    EventHandler = tslib_1.__importStar(EventHandler);
+    DatePicker = tslib_1.__importStar(DatePicker);
+    class UiPollEditor {
+        constructor(containerId, pollOptions, wysiwygId, options) {
+            const container = document.getElementById(containerId);
+            if (container === null) {
                 throw new Error("Unknown poll editor container with id '" + containerId + "'.");
             }
-            this._wysiwygId = wysiwygId;
-            if (wysiwygId !== '' && elById(wysiwygId) === null) {
+            this.container = container;
+            this.wysiwygId = wysiwygId;
+            if (wysiwygId !== "" && document.getElementById(wysiwygId) === null) {
                 throw new Error("Unknown wysiwyg field with id '" + wysiwygId + "'.");
             }
-            this.questionField = elById(this._wysiwygId + 'Poll_question');
-            var optionLists = elByClass('sortableList', this._container);
-            if (optionLists.length === 0) {
+            this.questionField = document.getElementById(this.wysiwygId + "Poll_question");
+            const optionList = this.container.querySelector(".sortableList");
+            if (optionList === null) {
                 throw new Error("Cannot find poll options list for container with id '" + containerId + "'.");
             }
-            this.optionList = optionLists[0];
-            this.endTimeField = elById(this._wysiwygId + 'Poll_endTime');
-            this.maxVotesField = elById(this._wysiwygId + 'Poll_maxVotes');
-            this.isChangeableYesField = elById(this._wysiwygId + 'Poll_isChangeable');
-            this.isChangeableNoField = elById(this._wysiwygId + 'Poll_isChangeable_no');
-            this.isPublicYesField = elById(this._wysiwygId + 'Poll_isPublic');
-            this.isPublicNoField = elById(this._wysiwygId + 'Poll_isPublic_no');
-            this.resultsRequireVoteYesField = elById(this._wysiwygId + 'Poll_resultsRequireVote');
-            this.resultsRequireVoteNoField = elById(this._wysiwygId + 'Poll_resultsRequireVote_no');
-            this.sortByVotesYesField = elById(this._wysiwygId + 'Poll_sortByVotes');
-            this.sortByVotesNoField = elById(this._wysiwygId + 'Poll_sortByVotes_no');
-            this._optionCount = 0;
-            this._options = Core.extend({
+            this.optionList = optionList;
+            this.endTimeField = document.getElementById(this.wysiwygId + "Poll_endTime");
+            this.maxVotesField = document.getElementById(this.wysiwygId + "Poll_maxVotes");
+            this.isChangeableYesField = document.getElementById(this.wysiwygId + "Poll_isChangeable");
+            this.isChangeableNoField = document.getElementById(this.wysiwygId + "Poll_isChangeable_no");
+            this.isPublicYesField = document.getElementById(this.wysiwygId + "Poll_isPublic");
+            this.isPublicNoField = document.getElementById(this.wysiwygId + "Poll_isPublic_no");
+            this.resultsRequireVoteYesField = document.getElementById(this.wysiwygId + "Poll_resultsRequireVote");
+            this.resultsRequireVoteNoField = document.getElementById(this.wysiwygId + "Poll_resultsRequireVote_no");
+            this.sortByVotesYesField = document.getElementById(this.wysiwygId + "Poll_sortByVotes");
+            this.sortByVotesNoField = document.getElementById(this.wysiwygId + "Poll_sortByVotes_no");
+            this.optionCount = 0;
+            this.options = Core.extend({
                 isAjax: false,
-                maxOptions: 20
+                maxOptions: 20,
             }, options);
-            this._createOptionList(pollOptions || []);
-            new UiSortableList({
+            this.createOptionList(pollOptions || []);
+            new List_1.default({
                 containerId: containerId,
                 options: {
-                    toleranceElement: '> div'
-                }
+                    toleranceElement: "> div",
+                },
             });
-            if (this._options.isAjax) {
-                var events = ['handleError', 'reset', 'submit', 'validate'];
-                for (var i = 0, length = events.length; i < length; i++) {
-                    var event = events[i];
-                    EventHandler.add('com.woltlab.wcf.redactor2', event + '_' + this._wysiwygId, this['_' + event].bind(this));
-                }
+            if (this.options.isAjax) {
+                ["handleError", "reset", "submit", "validate"].forEach((event) => {
+                    EventHandler.add("com.woltlab.wcf.redactor2", event + "_" + this.wysiwygId, (...args) => this[event](...args));
+                });
             }
             else {
-                var form = this._container.closest('form');
+                const form = this.container.closest("form");
                 if (form === null) {
                     throw new Error("Cannot find form for container with id '" + containerId + "'.");
                 }
-                form.addEventListener('submit', this._submit.bind(this));
-            }
-        },
-        /**
-         * Adds an option based on below the option for which the `Add Option` button has
-         * been clicked.
-         *
-         * @param      {Event}         event           icon click event
-         */
-        _addOption: function (event) {
-            event.preventDefault();
-            if (this._optionCount === this._options.maxOptions) {
-                return false;
+                form.addEventListener("submit", (ev) => this.submit(ev));
             }
-            this._createOption(undefined, undefined, event.currentTarget.closest('li'));
-        },
+        }
         /**
-         * Creates a new option based on the given data or an empty option if no option data
-         * is given.
-         *
-         * @param      {string}        optionValue     value of the option
-         * @param      {integer}       optionId        id of the option
-         * @param      {Element?}      insertAfter     optional element after which the new option is added
-         * @private
+         * Creates a poll option with the given data or an empty poll option of no data is given.
          */
-        _createOption: function (optionValue, optionId, insertAfter) {
-            optionValue = optionValue || '';
-            optionId = ~~optionId || 0;
-            var listItem = elCreate('LI');
-            listItem.className = 'sortableNode';
-            elData(listItem, 'option-id', optionId);
+        createOption(optionValue, optionId, insertAfter) {
+            optionValue = optionValue || "";
+            optionId = optionId || "0";
+            const listItem = document.createElement("LI");
+            listItem.classList.add("sortableNode");
+            listItem.dataset.optionId = optionId;
             if (insertAfter) {
-                DomUtil.insertAfter(listItem, insertAfter);
+                insertAfter.insertAdjacentElement("afterend", listItem);
             }
             else {
                 this.optionList.appendChild(listItem);
             }
-            var pollOptionInput = elCreate('div');
-            pollOptionInput.className = 'pollOptionInput';
+            const pollOptionInput = document.createElement("div");
+            pollOptionInput.classList.add("pollOptionInput");
             listItem.appendChild(pollOptionInput);
-            var sortHandle = elCreate('span');
-            sortHandle.className = 'icon icon16 fa-arrows sortableNodeHandle';
+            const sortHandle = document.createElement("span");
+            sortHandle.classList.add("icon", "icon16", "fa-arrows", "sortableNodeHandle");
             pollOptionInput.appendChild(sortHandle);
             // buttons
-            var addButton = elCreate('a');
-            elAttr(addButton, 'role', 'button');
-            elAttr(addButton, 'href', '#');
-            addButton.className = 'icon icon16 fa-plus jsTooltip jsAddOption pointer';
-            elAttr(addButton, 'title', Language.get('wcf.poll.button.addOption'));
-            addButton.addEventListener('click', this._addOption.bind(this));
+            const addButton = document.createElement("a");
+            listItem.setAttribute("role", "button");
+            listItem.setAttribute("href", "#");
+            addButton.classList.add("icon", "icon16", "fa-plus", "jsTooltip", "jsAddOption", "pointer");
+            addButton.setAttribute("title", Language.get("wcf.poll.button.addOption"));
+            addButton.addEventListener("click", () => this.createOption());
             pollOptionInput.appendChild(addButton);
-            var deleteButton = elCreate('a');
-            elAttr(deleteButton, 'role', 'button');
-            elAttr(deleteButton, 'href', '#');
-            deleteButton.className = 'icon icon16 fa-times jsTooltip jsDeleteOption pointer';
-            elAttr(deleteButton, 'title', Language.get('wcf.poll.button.removeOption'));
-            deleteButton.addEventListener('click', this._removeOption.bind(this));
+            const deleteButton = document.createElement("a");
+            deleteButton.setAttribute("role", "button");
+            deleteButton.setAttribute("href", "#");
+            deleteButton.classList.add("icon", "icon16", "fa-times", "jsTooltip", "jsDeleteOption", "pointer");
+            deleteButton.setAttribute("title", Language.get("wcf.poll.button.removeOption"));
+            deleteButton.addEventListener("click", (ev) => this.removeOption(ev));
             pollOptionInput.appendChild(deleteButton);
             // input field
-            var optionInput = elCreate('input');
-            elAttr(optionInput, 'type', 'text');
+            const optionInput = document.createElement("input");
+            optionInput.type = "text";
             optionInput.value = optionValue;
-            elAttr(optionInput, 'maxlength', 255);
-            optionInput.addEventListener('keydown', this._optionInputKeyDown.bind(this));
-            optionInput.addEventListener('click', function () {
+            optionInput.maxLength = 255;
+            optionInput.addEventListener("keydown", (ev) => this.optionInputKeyDown(ev));
+            optionInput.addEventListener("click", () => {
                 // work-around for some weird focus issue on iOS/Android
-                if (document.activeElement !== this) {
-                    this.focus();
+                if (document.activeElement !== optionInput) {
+                    optionInput.focus();
                 }
             });
             pollOptionInput.appendChild(optionInput);
             if (insertAfter !== null) {
                 optionInput.focus();
             }
-            this._optionCount++;
-            if (this._optionCount === this._options.maxOptions) {
-                elBySelAll('span.jsAddOption', this.optionList, function (icon) {
-                    icon.classList.remove('pointer');
-                    icon.classList.add('disabled');
+            this.optionCount++;
+            if (this.optionCount === this.options.maxOptions) {
+                this.optionList.querySelectorAll(".jsAddOption").forEach((icon) => {
+                    icon.classList.remove("pointer");
+                    icon.classList.add("disabled");
                 });
             }
-        },
+        }
         /**
-         * Adds the given poll option to the option list.
-         *
-         * @param      {object[]}      pollOptions     data of the added options
+         * Populates the option list with the current options.
          */
-        _createOptionList: function (pollOptions) {
-            for (var i = 0, length = pollOptions.length; i < length; i++) {
-                var option = pollOptions[i];
-                this._createOption(option.optionValue, option.optionID);
-            }
-            // add empty option field to add new options
-            if (this._optionCount < this._options.maxOptions) {
-                this._createOption();
+        createOptionList(pollOptions) {
+            pollOptions.forEach((option) => {
+                this.createOption(option.optionValue, option.optionID);
+            });
+            if (this.optionCount < this.options.maxOptions) {
+                this.createOption();
             }
-        },
+        }
         /**
-         * Handles errors when the data is saved via AJAX.
-         *
-         * @param      {object}        data    request response data
+         * Handles validation errors returned by Ajax request.
          */
-        _handleError: function (data) {
+        handleError(data) {
             switch (data.returnValues.fieldName) {
-                case this._wysiwygId + 'Poll_endTime':
-                case this._wysiwygId + 'Poll_maxVotes':
-                    var fieldName = data.returnValues.fieldName.replace(this._wysiwygId + 'Poll_', '');
-                    var small = elCreate('small');
-                    small.className = 'innerError';
-                    small.innerHTML = Language.get('wcf.poll.' + fieldName + '.error.' + data.returnValues.errorType);
-                    var element = elById(data.returnValues.fieldName);
-                    var errorParent = element.closest('dd');
-                    DomUtil.prepend(small, element.nextSibling);
+                case this.wysiwygId + "Poll_endTime":
+                case this.wysiwygId + "Poll_maxVotes": {
+                    const fieldName = data.returnValues.fieldName.replace(this.wysiwygId + "Poll_", "");
+                    const small = document.createElement("small");
+                    small.classList.add("innerError");
+                    small.innerHTML = Language.get("wcf.poll." + fieldName + ".error." + data.returnValues.errorType);
+                    const field = document.getElementById(data.returnValues.fieldName);
+                    field.nextSibling.insertAdjacentElement("afterbegin", small);
                     data.cancel = true;
                     break;
+                }
             }
-        },
+        }
         /**
-         * Adds an empty poll option after the current option when clicking enter.
-         *
-         * @param      {Event}         event   key event
+         * Adds another option field below the current option field after pressing Enter.
          */
-        _optionInputKeyDown: function (event) {
-            // ignore every key except for [Enter]
-            if (!EventKey.Enter(event)) {
+        optionInputKeyDown(event) {
+            if (event.key !== "Enter") {
                 return;
             }
-            Core.triggerEvent(elByClass('jsAddOption', event.currentTarget.parentNode)[0], 'click');
+            const target = event.currentTarget;
+            const addOption = target.parentElement.querySelector(".jsAddOption");
+            Core.triggerEvent(addOption, "click");
             event.preventDefault();
-        },
+        }
         /**
-         * Removes a poll option after clicking on the `Remove Option` button.
-         *
-         * @param      {Event}         event   click event
+         * Removes a poll option after clicking on its deletion button.
          */
-        _removeOption: function (event) {
+        removeOption(event) {
             event.preventDefault();
-            elRemove(event.currentTarget.closest('li'));
-            this._optionCount--;
-            elBySelAll('span.jsAddOption', this.optionList, function (icon) {
-                icon.classList.add('pointer');
-                icon.classList.remove('disabled');
-            });
-            if (this.optionList.length === 0) {
-                this._createOption();
+            const button = event.currentTarget;
+            button.closest("li").remove();
+            this.optionCount--;
+            if (this.optionList.childElementCount === 0) {
+                this.createOption();
             }
-        },
+            else {
+                this.optionList.querySelectorAll(".jsAddOption").forEach((icon) => {
+                    icon.classList.add("pointer");
+                    icon.classList.remove("disabled");
+                });
+            }
+        }
         /**
-         * Resets all poll-related form fields.
+         * Resets all poll fields.
          */
-        _reset: function () {
-            this.questionField.value = '';
-            this._optionCount = 0;
-            this.optionList.innerHtml = '';
-            this._createOption();
+        reset() {
+            this.questionField.value = "";
+            this.optionCount = 0;
+            this.optionList.innerHTML = "";
+            this.createOption();
             DatePicker.clear(this.endTimeField);
-            this.maxVotesField.value = 1;
+            this.maxVotesField.value = "1";
             this.isChangeableYesField.checked = false;
             this.isChangeableNoField.checked = true;
             this.isPublicYesField.checked = false;
@@ -243,102 +202,94 @@ define([
             this.resultsRequireVoteNoField.checked = true;
             this.sortByVotesYesField.checked = false;
             this.sortByVotesNoField.checked = true;
-            EventHandler.fire('com.woltlab.wcf.poll.editor', 'reset', {
-                pollEditor: this
+            EventHandler.fire("com.woltlab.wcf.poll.editor", "reset", {
+                pollEditor: this,
             });
-        },
+        }
         /**
-         * Is called if the form is submitted or before the AJAX request is sent.
-         *
-         * @param      {Event?}        event   form submit event
+         * Handles the poll data if the form is submitted.
          */
-        _submit: function (event) {
-            if (this._options.isAjax) {
-                event.poll = this.getData();
-                EventHandler.fire('com.woltlab.wcf.poll.editor', 'submit', {
+        submit(event) {
+            if (this.options.isAjax) {
+                EventHandler.fire("com.woltlab.wcf.poll.editor", "submit", {
                     event: event,
-                    pollEditor: this
+                    pollEditor: this,
                 });
             }
             else {
-                var form = this._container.closest('form');
-                var options = this.getOptions();
-                for (var i = 0, length = options.length; i < length; i++) {
-                    var input = elCreate('input');
-                    elAttr(input, 'type', 'hidden');
-                    elAttr(input, 'name', this._wysiwygId + 'Poll_options[' + i + ']');
-                    input.value = options[i];
+                const form = this.container.closest("form");
+                this.getOptions().forEach((option, i) => {
+                    const input = document.createElement("input");
+                    input.type = "hidden";
+                    input.name = `${this.wysiwygId} + 'Poll_options[${i}}]`;
+                    input.value = option;
                     form.appendChild(input);
-                }
+                });
             }
-        },
+        }
         /**
-         * Is called to validate the poll data.
-         *
-         * @param      {object}        data    event data
+         * Validates the poll data.
          */
-        _validate: function (data) {
-            if (this.questionField.value.trim() === '') {
+        validate(data) {
+            if (this.questionField.value.trim() === "") {
                 return;
             }
-            var nonEmptyOptionCount = 0;
-            for (var i = 0, length = this.optionList.children.length; i < length; i++) {
-                var optionInput = elBySel('input[type=text]', this.optionList.children[i]);
-                if (optionInput.value.trim() !== '') {
+            let nonEmptyOptionCount = 0;
+            Array.from(this.optionList.children).forEach((listItem) => {
+                const optionInput = listItem.querySelector("input[type=text]");
+                if (optionInput.value.trim() !== "") {
                     nonEmptyOptionCount++;
                 }
-            }
+            });
             if (nonEmptyOptionCount === 0) {
-                data.api.throwError(this._container, Language.get('wcf.global.form.error.empty'));
+                data.api.throwError(this.container, Language.get("wcf.global.form.error.empty"));
                 data.valid = false;
             }
             else {
-                var maxVotes = ~~this.maxVotesField.value;
+                const maxVotes = ~~this.maxVotesField.value;
                 if (maxVotes && maxVotes > nonEmptyOptionCount) {
-                    data.api.throwError(this.maxVotesField.parentNode, Language.get('wcf.poll.maxVotes.error.invalid'));
+                    data.api.throwError(this.maxVotesField.parentElement, Language.get("wcf.poll.maxVotes.error.invalid"));
                     data.valid = false;
                 }
                 else {
-                    EventHandler.fire('com.woltlab.wcf.poll.editor', 'validate', {
+                    EventHandler.fire("com.woltlab.wcf.poll.editor", "validate", {
                         data: data,
-                        pollEditor: this
+                        pollEditor: this,
                     });
                 }
             }
-        },
+        }
         /**
-         * Returns all poll data.
-         *
-         * @return      {object}
+         * Returns the data of the poll.
          */
-        getData: function () {
-            var data = {};
-            data[this.questionField.id] = this.questionField.value;
-            data[this._wysiwygId + 'Poll_options'] = this.getOptions();
-            data[this.endTimeField.id] = this.endTimeField.value;
-            data[this.maxVotesField.id] = this.maxVotesField.value;
-            data[this.isChangeableYesField.id] = !!this.isChangeableYesField.checked;
-            data[this.isPublicYesField.id] = !!this.isPublicYesField.checked;
-            data[this.resultsRequireVoteYesField.id] = !!this.resultsRequireVoteYesField.checked;
-            data[this.sortByVotesYesField.id] = !!this.sortByVotesYesField.checked;
-            return data;
-        },
+        getData() {
+            return {
+                [this.questionField.id]: this.questionField.value,
+                [this.wysiwygId + "Poll_options"]: this.getOptions(),
+                [this.endTimeField.id]: this.endTimeField.value,
+                [this.maxVotesField.id]: this.maxVotesField.value,
+                [this.isChangeableYesField.id]: !!this.isChangeableYesField.checked,
+                [this.isPublicYesField.id]: !!this.isPublicYesField.checked,
+                [this.resultsRequireVoteYesField.id]: !!this.resultsRequireVoteYesField.checked,
+                [this.sortByVotesYesField.id]: !!this.sortByVotesYesField.checked,
+            };
+        }
         /**
-         * Returns all entered poll options.
+         * Returns the selectable options in the poll.
          *
-         * @return      {string[]}
+         * Format: `{optionID}_{option}` with `optionID = 0` if it is a new option.
          */
-        getOptions: function () {
-            var options = [];
-            for (var i = 0, length = this.optionList.children.length; i < length; i++) {
-                var listItem = this.optionList.children[i];
-                var optionValue = elBySel('input[type=text]', listItem).value.trim();
-                if (optionValue !== '') {
-                    options.push(elData(listItem, 'option-id') + '_' + optionValue);
+        getOptions() {
+            const options = [];
+            Array.from(this.optionList.children).forEach((listItem) => {
+                const optionValue = listItem.querySelector("input[type=text]").value.trim();
+                if (optionValue !== "") {
+                    options.push(`${listItem.dataset.optionId}_${optionValue}`);
                 }
-            }
+            });
             return options;
         }
-    };
+    }
+    Core.enableLegacyInheritance(UiPollEditor);
     return UiPollEditor;
 });
index 47d3c5c57e2073b6e984208ebcee2bf3ec31c500..09c1ffe571bc01163ee5262fa9859c6b863e13a1 100644 (file)
+"use strict";
 /**
  * Handles the data to create and edit a poll in a form created via form builder.
- * 
- * @author     Alexander Ebert, Matthias Schmidt
- * @copyright  2001-2020 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLabSuite/Core/Ui/Poll/Editor
- * @since      5.2
+ *
+ * @author  Alexander Ebert, Matthias Schmidt
+ * @copyright  2001-2020 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Ui/Poll/Editor
  */
-define([
-       'Core',
-       'Dom/Util',
-       'EventHandler',
-       'EventKey',
-       'Language',
-       'WoltLabSuite/Core/Date/Picker',
-       'WoltLabSuite/Core/Ui/Sortable/List'
-], function(
-       Core,
-       DomUtil,
-       EventHandler,
-       EventKey,
-       Language,
-       DatePicker,
-       UiSortableList
-) {
-       "use strict";
-       
-       function UiPollEditor(containerId, pollOptions, wysiwygId, options) {
-               this.init(containerId, pollOptions, wysiwygId, options);
-       }
-       UiPollEditor.prototype = {
-               /**
-                * Initializes the poll editor.
-                * 
-                * @param       {string}        containerId     id of the poll options container
-                * @param       {object[]}      pollOptions     existing poll options
-                * @param       {string}        wysiwygId       id of the related wysiwyg editor
-                * @param       {object}        options         additional poll options
-                */
-               init: function(containerId, pollOptions, wysiwygId, options) {
-                       this._container = elById(containerId);
-                       if (this._container === null) {
-                               throw new Error("Unknown poll editor container with id '" + containerId + "'.");
-                       }
-                       
-                       this._wysiwygId = wysiwygId;
-                       if (wysiwygId !== '' && elById(wysiwygId) === null) {
-                               throw new Error("Unknown wysiwyg field with id '" + wysiwygId + "'.");
-                       }
-                       
-                       this.questionField = elById(this._wysiwygId + 'Poll_question');
-                       
-                       var optionLists = elByClass('sortableList', this._container);
-                       if (optionLists.length === 0) {
-                               throw new Error("Cannot find poll options list for container with id '" + containerId + "'.");
-                       }
-                       this.optionList = optionLists[0];
-                       
-                       this.endTimeField = elById(this._wysiwygId + 'Poll_endTime');
-                       this.maxVotesField = elById(this._wysiwygId + 'Poll_maxVotes');
-                       this.isChangeableYesField = elById(this._wysiwygId + 'Poll_isChangeable');
-                       this.isChangeableNoField = elById(this._wysiwygId + 'Poll_isChangeable_no');
-                       this.isPublicYesField = elById(this._wysiwygId + 'Poll_isPublic');
-                       this.isPublicNoField = elById(this._wysiwygId + 'Poll_isPublic_no');
-                       this.resultsRequireVoteYesField = elById(this._wysiwygId + 'Poll_resultsRequireVote');
-                       this.resultsRequireVoteNoField = elById(this._wysiwygId + 'Poll_resultsRequireVote_no');
-                       this.sortByVotesYesField = elById(this._wysiwygId + 'Poll_sortByVotes');
-                       this.sortByVotesNoField = elById(this._wysiwygId + 'Poll_sortByVotes_no');
-                       
-                       this._optionCount = 0;
-                       this._options = Core.extend({
-                               isAjax: false,
-                               maxOptions: 20
-                       }, options);
-                       
-                       this._createOptionList(pollOptions || []);
-                       
-                       new UiSortableList({
-                               containerId: containerId,
-                               options: {
-                                       toleranceElement: '> div'
-                               }
-                       });
-                       
-                       if (this._options.isAjax) {
-                               var events = ['handleError', 'reset', 'submit', 'validate'];
-                               for (var i = 0, length = events.length; i < length; i++) {
-                                       var event = events[i];
-                                       
-                                       EventHandler.add(
-                                               'com.woltlab.wcf.redactor2',
-                                               event + '_' + this._wysiwygId,
-                                               this['_' + event].bind(this)
-                                       );
-                               }
-                       }
-                       else {
-                               var form = this._container.closest('form');
-                               if (form === null) {
-                                       throw new Error("Cannot find form for container with id '" + containerId + "'.");
-                               }
-                               
-                               form.addEventListener('submit', this._submit.bind(this));
-                       }
-               },
-               
-               /**
-                * Adds an option based on below the option for which the `Add Option` button has
-                * been clicked.
-                * 
-                * @param       {Event}         event           icon click event
-                */
-               _addOption: function(event) {
-                       event.preventDefault();
-                       
-                       if (this._optionCount === this._options.maxOptions) {
-                               return false;
-                       }
-                       
-                       this._createOption(
-                               undefined,
-                               undefined,
-                               event.currentTarget.closest('li')
-                       );
-               },
-               
-               /**
-                * Creates a new option based on the given data or an empty option if no option data
-                * is given.
-                * 
-                * @param       {string}        optionValue     value of the option
-                * @param       {integer}       optionId        id of the option
-                * @param       {Element?}      insertAfter     optional element after which the new option is added
-                * @private
-                */
-               _createOption: function(optionValue, optionId, insertAfter) {
-                       optionValue = optionValue || '';
-                       optionId = ~~optionId || 0;
-                       
-                       var listItem = elCreate('LI');
-                       listItem.className = 'sortableNode';
-                       elData(listItem, 'option-id', optionId);
-                       
-                       if (insertAfter) {
-                               DomUtil.insertAfter(listItem, insertAfter);
-                       }
-                       else {
-                               this.optionList.appendChild(listItem);
-                       }
-                       
-                       var pollOptionInput = elCreate('div');
-                       pollOptionInput.className = 'pollOptionInput';
-                       listItem.appendChild(pollOptionInput);
-                       
-                       var sortHandle = elCreate('span');
-                       sortHandle.className = 'icon icon16 fa-arrows sortableNodeHandle';
-                       pollOptionInput.appendChild(sortHandle);
-                       
-                       // buttons
-                       var addButton = elCreate('a');
-                       elAttr(addButton, 'role', 'button');
-                       elAttr(addButton, 'href', '#');
-                       addButton.className = 'icon icon16 fa-plus jsTooltip jsAddOption pointer';
-                       elAttr(addButton, 'title', Language.get('wcf.poll.button.addOption'));
-                       addButton.addEventListener('click', this._addOption.bind(this));
-                       pollOptionInput.appendChild(addButton);
-                       
-                       var deleteButton = elCreate('a');
-                       elAttr(deleteButton, 'role', 'button');
-                       elAttr(deleteButton, 'href', '#');
-                       deleteButton.className = 'icon icon16 fa-times jsTooltip jsDeleteOption pointer';
-                       elAttr(deleteButton, 'title', Language.get('wcf.poll.button.removeOption'));
-                       deleteButton.addEventListener('click', this._removeOption.bind(this));
-                       pollOptionInput.appendChild(deleteButton);
-                       
-                       // input field
-                       var optionInput = elCreate('input');
-                       elAttr(optionInput, 'type', 'text');
-                       optionInput.value = optionValue;
-                       elAttr(optionInput, 'maxlength', 255);
-                       optionInput.addEventListener('keydown', this._optionInputKeyDown.bind(this));
-                       optionInput.addEventListener('click', function() {
-                               // work-around for some weird focus issue on iOS/Android
-                               if (document.activeElement !== this) {
-                                       this.focus();
-                               }
-                       });
-                       pollOptionInput.appendChild(optionInput);
-                       
-                       if (insertAfter !== null) {
-                               optionInput.focus();
-                       }
-                       
-                       this._optionCount++;
-                       if (this._optionCount === this._options.maxOptions) {
-                               elBySelAll('span.jsAddOption', this.optionList, function(icon) {
-                                       icon.classList.remove('pointer');
-                                       icon.classList.add('disabled');
-                               });
-                       }
-               },
-               
-               /**
-                * Adds the given poll option to the option list.
-                * 
-                * @param       {object[]}      pollOptions     data of the added options
-                */
-               _createOptionList: function(pollOptions) {
-                       for (var i = 0, length = pollOptions.length; i < length; i++) {
-                               var option = pollOptions[i];
-                               this._createOption(option.optionValue, option.optionID);
-                       }
-                       
-                       // add empty option field to add new options
-                       if (this._optionCount < this._options.maxOptions) {
-                               this._createOption();
-                       }
-               },
-               
-               /**
-                * Handles errors when the data is saved via AJAX.
-                * 
-                * @param       {object}        data    request response data
-                */
-               _handleError: function (data) {
-                       switch (data.returnValues.fieldName) {
-                               case this._wysiwygId + 'Poll_endTime':
-                               case this._wysiwygId + 'Poll_maxVotes':
-                                       var fieldName = data.returnValues.fieldName.replace(this._wysiwygId + 'Poll_', '');
-                                       
-                                       var small = elCreate('small');
-                                       small.className = 'innerError';
-                                       small.innerHTML = Language.get('wcf.poll.' + fieldName + '.error.' + data.returnValues.errorType);
-                                       
-                                       var element = elById(data.returnValues.fieldName);
-                                       var errorParent = element.closest('dd');
-                                       
-                                       DomUtil.prepend(small, element.nextSibling);
-                                       
-                                       data.cancel = true;
-                                       break;
-                       }
-               },
-               
-               /**
-                * Adds an empty poll option after the current option when clicking enter.
-                * 
-                * @param       {Event}         event   key event
-                */
-               _optionInputKeyDown: function(event) {
-                       // ignore every key except for [Enter]
-                       if (!EventKey.Enter(event)) {
-                               return;
-                       }
-                       
-                       Core.triggerEvent(elByClass('jsAddOption', event.currentTarget.parentNode)[0], 'click');
-                       
-                       event.preventDefault();
-               },
-               
-               /**
-                * Removes a poll option after clicking on the `Remove Option` button.
-                * 
-                * @param       {Event}         event   click event
-                */
-               _removeOption: function (event) {
-                       event.preventDefault();
-                       
-                       elRemove(event.currentTarget.closest('li'));
-                       
-                       this._optionCount--;
-                       
-                       elBySelAll('span.jsAddOption', this.optionList, function(icon) {
-                               icon.classList.add('pointer');
-                               icon.classList.remove('disabled');
-                       });
-                       
-                       if (this.optionList.length === 0) {
-                               this._createOption();
-                       }
-               },
-               
-               /**
-                * Resets all poll-related form fields.
-                */
-               _reset: function() {
-                       this.questionField.value = '';
-                       
-                       this._optionCount = 0;
-                       this.optionList.innerHtml = '';
-                       this._createOption();
-                       
-                       DatePicker.clear(this.endTimeField);
-                       
-                       this.maxVotesField.value = 1;
-                       this.isChangeableYesField.checked = false;
-                       this.isChangeableNoField.checked = true;
-                       this.isPublicYesField.checked = false;
-                       this.isPublicNoField.checked = true;
-                       this.resultsRequireVoteYesField.checked = false;
-                       this.resultsRequireVoteNoField.checked = true;
-                       this.sortByVotesYesField.checked = false;
-                       this.sortByVotesNoField.checked = true;
-                       
-                       EventHandler.fire(
-                               'com.woltlab.wcf.poll.editor',
-                               'reset',
-                               {
-                                       pollEditor: this
-                               }
-                       );
-               },
-               
-               /**
-                * Is called if the form is submitted or before the AJAX request is sent.
-                * 
-                * @param       {Event?}        event   form submit event
-                */
-               _submit: function(event) {
-                       if (this._options.isAjax) {
-                               event.poll = this.getData();
-                               
-                               EventHandler.fire(
-                                       'com.woltlab.wcf.poll.editor',
-                                       'submit',
-                                       {
-                                               event: event,
-                                               pollEditor: this
-                                       }
-                               );
-                       }
-                       else {
-                               var form = this._container.closest('form');
-                               
-                               var options = this.getOptions();
-                               for (var i = 0, length = options.length; i < length; i++) {
-                                       var input = elCreate('input');
-                                       elAttr(input, 'type', 'hidden');
-                                       elAttr(input, 'name', this._wysiwygId + 'Poll_options[' + i + ']');
-                                       input.value = options[i];
-                                       form.appendChild(input);
-                               }
-                       }
-               },
-               
-               /**
-                * Is called to validate the poll data.
-                * 
-                * @param       {object}        data    event data
-                */
-               _validate: function(data) {
-                       if (this.questionField.value.trim() === '') {
-                               return;
-                       }
-                       
-                       var nonEmptyOptionCount = 0;
-                       for (var i = 0, length = this.optionList.children.length; i < length; i++) {
-                               var optionInput = elBySel('input[type=text]', this.optionList.children[i]);
-                               if (optionInput.value.trim() !== '') {
-                                       nonEmptyOptionCount++;
-                               }
-                       }
-                       
-                       if (nonEmptyOptionCount === 0) {
-                               data.api.throwError(this._container, Language.get('wcf.global.form.error.empty'));
-                               data.valid = false;
-                       }
-                       else {
-                               var maxVotes = ~~this.maxVotesField.value;
-                               
-                               if (maxVotes && maxVotes > nonEmptyOptionCount) {
-                                       data.api.throwError(this.maxVotesField.parentNode, Language.get('wcf.poll.maxVotes.error.invalid'));
-                                       data.valid = false;
-                               }
-                               else {
-                                       EventHandler.fire(
-                                               'com.woltlab.wcf.poll.editor',
-                                               'validate',
-                                               {
-                                                       data: data,
-                                                       pollEditor: this
-                                               }
-                                       );
-                               }
-                       }
-               },
-               
-               /**
-                * Returns all poll data.
-                *
-                * @return      {object}
-                */
-               getData: function() {
-                       var data = {};
-                       
-                       data[this.questionField.id] = this.questionField.value;
-                       data[this._wysiwygId + 'Poll_options'] = this.getOptions();
-                       data[this.endTimeField.id] = this.endTimeField.value;
-                       data[this.maxVotesField.id] = this.maxVotesField.value;
-                       data[this.isChangeableYesField.id] = !!this.isChangeableYesField.checked;
-                       data[this.isPublicYesField.id] = !!this.isPublicYesField.checked;
-                       data[this.resultsRequireVoteYesField.id] = !!this.resultsRequireVoteYesField.checked;
-                       data[this.sortByVotesYesField.id] = !!this.sortByVotesYesField.checked;
-                       
-                       return data;
-               },
-               
-               /**
-                * Returns all entered poll options.
-                *
-                * @return      {string[]}
-                */
-               getOptions: function() {
-                       var options = [];
-                       for (var i = 0, length = this.optionList.children.length; i < length; i++) {
-                               var listItem = this.optionList.children[i];
-                               var optionValue = elBySel('input[type=text]', listItem).value.trim();
-                               
-                               if (optionValue !== '') {
-                                       options.push(elData(listItem, 'option-id') + '_' + optionValue);
-                               }
-                       }
-                       
-                       return options;
-               }
-       };
-       
-       return UiPollEditor;
-});
+var Core = require("../../Core");
+var Language = require("../../Language");
+var List_1 = require("../Sortable/List");
+var EventHandler = require("../../Event/Handler");
+var DatePicker = require("../../Date/Picker");
+var UiPollEditor = /** @class */ (function () {
+    function UiPollEditor(containerId, pollOptions, wysiwygId, options) {
+        var _this = this;
+        var container = document.getElementById(containerId);
+        if (container === null) {
+            throw new Error("Unknown poll editor container with id '" + containerId + "'.");
+        }
+        this.container = container;
+        this.wysiwygId = wysiwygId;
+        if (wysiwygId !== "" && document.getElementById(wysiwygId) === null) {
+            throw new Error("Unknown wysiwyg field with id '" + wysiwygId + "'.");
+        }
+        this.questionField = document.getElementById(this.wysiwygId + "Poll_question");
+        var optionList = this.container.querySelector(".sortableList");
+        if (optionList === null) {
+            throw new Error("Cannot find poll options list for container with id '" + containerId + "'.");
+        }
+        this.optionList = optionList;
+        this.endTimeField = document.getElementById(this.wysiwygId + "Poll_endTime");
+        this.maxVotesField = document.getElementById(this.wysiwygId + "Poll_maxVotes");
+        this.isChangeableYesField = document.getElementById(this.wysiwygId + "Poll_isChangeable");
+        this.isChangeableNoField = document.getElementById(this.wysiwygId + "Poll_isChangeable_no");
+        this.isPublicYesField = document.getElementById(this.wysiwygId + "Poll_isPublic");
+        this.isPublicNoField = document.getElementById(this.wysiwygId + "Poll_isPublic_no");
+        this.resultsRequireVoteYesField = document.getElementById(this.wysiwygId + "Poll_resultsRequireVote");
+        this.resultsRequireVoteNoField = document.getElementById(this.wysiwygId + "Poll_resultsRequireVote_no");
+        this.sortByVotesYesField = document.getElementById(this.wysiwygId + "Poll_sortByVotes");
+        this.sortByVotesNoField = document.getElementById(this.wysiwygId + "Poll_sortByVotes_no");
+        this.optionCount = 0;
+        this.options = Core.extend({
+            isAjax: false,
+            maxOptions: 20
+        }, options);
+        this.createOptionList(pollOptions || []);
+        new List_1["default"]({
+            containerId: containerId,
+            options: {
+                toleranceElement: "> div"
+            }
+        });
+        if (this.options.isAjax) {
+            ["handleError", "reset", "submit", "validate"].forEach(function (event) {
+                EventHandler.add("com.woltlab.wcf.redactor2", event + "_" + _this.wysiwygId, function () {
+                    var args = [];
+                    for (var _i = 0; _i < arguments.length; _i++) {
+                        args[_i] = arguments[_i];
+                    }
+                    return _this[event].apply(_this, args);
+                });
+            });
+        }
+        else {
+            var form = this.container.closest("form");
+            if (form === null) {
+                throw new Error("Cannot find form for container with id '" + containerId + "'.");
+            }
+            form.addEventListener("submit", function (ev) { return _this.submit(ev); });
+        }
+    }
+    /**
+     * Creates a poll option with the given data or an empty poll option of no data is given.
+     */
+    UiPollEditor.prototype.createOption = function (optionValue, optionId, insertAfter) {
+        var _this = this;
+        optionValue = optionValue || "";
+        optionId = optionId || "0";
+        var listItem = document.createElement("LI");
+        listItem.classList.add("sortableNode");
+        listItem.dataset.optionId = optionId;
+        if (insertAfter) {
+            insertAfter.insertAdjacentElement("afterend", listItem);
+        }
+        else {
+            this.optionList.appendChild(listItem);
+        }
+        var pollOptionInput = document.createElement("div");
+        pollOptionInput.classList.add("pollOptionInput");
+        listItem.appendChild(pollOptionInput);
+        var sortHandle = document.createElement("span");
+        sortHandle.classList.add("icon", "icon16", "fa-arrows", "sortableNodeHandle");
+        pollOptionInput.appendChild(sortHandle);
+        // buttons
+        var addButton = document.createElement("a");
+        listItem.setAttribute("role", "button");
+        listItem.setAttribute("href", "#");
+        addButton.classList.add("icon", "icon16", "fa-plus", "jsTooltip", "jsAddOption", "pointer");
+        addButton.setAttribute("title", Language.get("wcf.poll.button.addOption"));
+        addButton.addEventListener("click", function () { return _this.createOption(); });
+        pollOptionInput.appendChild(addButton);
+        var deleteButton = document.createElement("a");
+        deleteButton.setAttribute("role", "button");
+        deleteButton.setAttribute("href", "#");
+        deleteButton.classList.add("icon", "icon16", "fa-times", "jsTooltip", "jsDeleteOption", "pointer");
+        deleteButton.setAttribute("title", Language.get("wcf.poll.button.removeOption"));
+        deleteButton.addEventListener("click", function (ev) { return _this.removeOption(ev); });
+        pollOptionInput.appendChild(deleteButton);
+        // input field
+        var optionInput = document.createElement("input");
+        optionInput.type = "text";
+        optionInput.value = optionValue;
+        optionInput.maxLength = 255;
+        optionInput.addEventListener("keydown", function (ev) { return _this.optionInputKeyDown(ev); });
+        optionInput.addEventListener("click", function () {
+            // work-around for some weird focus issue on iOS/Android
+            if (document.activeElement !== optionInput) {
+                optionInput.focus();
+            }
+        });
+        pollOptionInput.appendChild(optionInput);
+        if (insertAfter !== null) {
+            optionInput.focus();
+        }
+        this.optionCount++;
+        if (this.optionCount === this.options.maxOptions) {
+            this.optionList.querySelectorAll(".jsAddOption").forEach(function (icon) {
+                icon.classList.remove("pointer");
+                icon.classList.add("disabled");
+            });
+        }
+    };
+    /**
+     * Populates the option list with the current options.
+     */
+    UiPollEditor.prototype.createOptionList = function (pollOptions) {
+        var _this = this;
+        pollOptions.forEach(function (option) {
+            _this.createOption(option.optionValue, option.optionID);
+        });
+        if (this.optionCount < this.options.maxOptions) {
+            this.createOption();
+        }
+    };
+    /**
+     * Handles validation errors returned by Ajax request.
+     */
+    UiPollEditor.prototype.handleError = function (data) {
+        switch (data.returnValues.fieldName) {
+            case this.wysiwygId + "Poll_endTime":
+            case this.wysiwygId + "Poll_maxVotes": {
+                var fieldName = data.returnValues.fieldName.replace(this.wysiwygId + "Poll_", "");
+                var small = document.createElement("small");
+                small.classList.add("innerError");
+                small.innerHTML = Language.get("wcf.poll." + fieldName + ".error." + data.returnValues.errorType);
+                var field = document.getElementById(data.returnValues.fieldName);
+                field.nextSibling.insertAdjacentElement("afterbegin", small);
+                data.cancel = true;
+                break;
+            }
+        }
+    };
+    /**
+     * Adds another option field below the current option field after pressing Enter.
+     */
+    UiPollEditor.prototype.optionInputKeyDown = function (event) {
+        if (event.key !== "Enter") {
+            return;
+        }
+        var target = event.currentTarget;
+        var addOption = target.parentElement.querySelector(".jsAddOption");
+        Core.triggerEvent(addOption, "click");
+        event.preventDefault();
+    };
+    /**
+     * Removes a poll option after clicking on its deletion button.
+     */
+    UiPollEditor.prototype.removeOption = function (event) {
+        event.preventDefault();
+        var button = event.currentTarget;
+        button.closest("li").remove();
+        this.optionCount--;
+        if (this.optionList.childElementCount === 0) {
+            this.createOption();
+        }
+        else {
+            this.optionList.querySelectorAll(".jsAddOption").forEach(function (icon) {
+                icon.classList.add("pointer");
+                icon.classList.remove("disabled");
+            });
+        }
+    };
+    /**
+     * Resets all poll fields.
+     */
+    UiPollEditor.prototype.reset = function () {
+        this.questionField.value = "";
+        this.optionCount = 0;
+        this.optionList.innerHTML = "";
+        this.createOption();
+        DatePicker.clear(this.endTimeField);
+        this.maxVotesField.value = "1";
+        this.isChangeableYesField.checked = false;
+        this.isChangeableNoField.checked = true;
+        this.isPublicYesField.checked = false;
+        this.isPublicNoField.checked = true;
+        this.resultsRequireVoteYesField.checked = false;
+        this.resultsRequireVoteNoField.checked = true;
+        this.sortByVotesYesField.checked = false;
+        this.sortByVotesNoField.checked = true;
+        EventHandler.fire("com.woltlab.wcf.poll.editor", "reset", {
+            pollEditor: this
+        });
+    };
+    /**
+     * Handles the poll data if the form is submitted.
+     */
+    UiPollEditor.prototype.submit = function (event) {
+        var _this = this;
+        if (this.options.isAjax) {
+            EventHandler.fire("com.woltlab.wcf.poll.editor", "submit", {
+                event: event,
+                pollEditor: this
+            });
+        }
+        else {
+            var form_1 = this.container.closest("form");
+            this.getOptions().forEach(function (option, i) {
+                var input = document.createElement("input");
+                input.type = "hidden";
+                input.name = _this.wysiwygId + " + 'Poll_options[" + i + "}]";
+                input.value = option;
+                form_1.appendChild(input);
+            });
+        }
+    };
+    /**
+     * Validates the poll data.
+     */
+    UiPollEditor.prototype.validate = function (data) {
+        if (this.questionField.value.trim() === "") {
+            return;
+        }
+        var nonEmptyOptionCount = 0;
+        Array.from(this.optionList.children).forEach(function (listItem) {
+            var optionInput = listItem.querySelector("input[type=text]");
+            if (optionInput.value.trim() !== "") {
+                nonEmptyOptionCount++;
+            }
+        });
+        if (nonEmptyOptionCount === 0) {
+            data.api.throwError(this.container, Language.get("wcf.global.form.error.empty"));
+            data.valid = false;
+        }
+        else {
+            var maxVotes = ~~this.maxVotesField.value;
+            if (maxVotes && maxVotes > nonEmptyOptionCount) {
+                data.api.throwError(this.maxVotesField.parentElement, Language.get("wcf.poll.maxVotes.error.invalid"));
+                data.valid = false;
+            }
+            else {
+                EventHandler.fire("com.woltlab.wcf.poll.editor", "validate", {
+                    data: data,
+                    pollEditor: this
+                });
+            }
+        }
+    };
+    /**
+     * Returns the data of the poll.
+     */
+    UiPollEditor.prototype.getData = function () {
+        var _a;
+        return _a = {},
+            _a[this.questionField.id] = this.questionField.value,
+            _a[this.wysiwygId + "Poll_options"] = this.getOptions(),
+            _a[this.endTimeField.id] = this.endTimeField.value,
+            _a[this.maxVotesField.id] = this.maxVotesField.value,
+            _a[this.isChangeableYesField.id] = !!this.isChangeableYesField.checked,
+            _a[this.isPublicYesField.id] = !!this.isPublicYesField.checked,
+            _a[this.resultsRequireVoteYesField.id] = !!this.resultsRequireVoteYesField.checked,
+            _a[this.sortByVotesYesField.id] = !!this.sortByVotesYesField.checked,
+            _a;
+    };
+    /**
+     * Returns the selectable options in the poll.
+     *
+     * Format: `{optionID}_{option}` with `optionID = 0` if it is a new option.
+     */
+    UiPollEditor.prototype.getOptions = function () {
+        var options = [];
+        Array.from(this.optionList.children).forEach(function (listItem) {
+            var optionValue = listItem.querySelector("input[type=text]").value.trim();
+            if (optionValue !== "") {
+                options.push(listItem.dataset.optionId + "_" + optionValue);
+            }
+        });
+        return options;
+    };
+    return UiPollEditor;
+}());
+Core.enableLegacyInheritance(UiPollEditor);
+module.exports = UiPollEditor;
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Poll/Editor.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Poll/Editor.ts
new file mode 100644 (file)
index 0000000..ae8a449
--- /dev/null
@@ -0,0 +1,393 @@
+/**
+ * Handles the data to create and edit a poll in a form created via form builder.
+ *
+ * @author  Alexander Ebert, Matthias Schmidt
+ * @copyright  2001-2020 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Ui/Poll/Editor
+ */
+
+import * as Core from "../../Core";
+import * as Language from "../../Language";
+import UiSortableList from "../Sortable/List";
+import * as EventHandler from "../../Event/Handler";
+import * as DatePicker from "../../Date/Picker";
+import { DatabaseObjectActionResponse } from "../../Ajax/Data";
+
+interface UiPollEditorOptions {
+  isAjax: boolean;
+  maxOptions: number;
+}
+
+interface PollOption {
+  optionID: string;
+  optionValue: string;
+}
+
+interface AjaxReturnValue {
+  errorType: string;
+  fieldName: string;
+}
+
+interface AjaxResponse extends DatabaseObjectActionResponse {
+  returnValues: AjaxReturnValue;
+}
+
+interface ValidationApi {
+  throwError: (HTMLElement, string) => void;
+}
+
+interface ValidationData {
+  api: ValidationApi;
+  valid: boolean;
+}
+
+class UiPollEditor {
+  private readonly container: HTMLElement;
+  private readonly endTimeField: HTMLInputElement;
+  private readonly isChangeableNoField: HTMLInputElement;
+  private readonly isChangeableYesField: HTMLInputElement;
+  private readonly isPublicNoField: HTMLInputElement;
+  private readonly isPublicYesField: HTMLInputElement;
+  private readonly maxVotesField: HTMLInputElement;
+  private optionCount: number;
+  private readonly options: UiPollEditorOptions;
+  private readonly optionList: HTMLOListElement;
+  private readonly questionField: HTMLInputElement;
+  private readonly resultsRequireVoteNoField: HTMLInputElement;
+  private readonly resultsRequireVoteYesField: HTMLInputElement;
+  private readonly sortByVotesNoField: HTMLInputElement;
+  private readonly sortByVotesYesField: HTMLInputElement;
+  private readonly wysiwygId: string;
+
+  constructor(containerId: string, pollOptions: PollOption[], wysiwygId: string, options: UiPollEditorOptions) {
+    const container = document.getElementById(containerId);
+    if (container === null) {
+      throw new Error("Unknown poll editor container with id '" + containerId + "'.");
+    }
+    this.container = container;
+
+    this.wysiwygId = wysiwygId;
+    if (wysiwygId !== "" && document.getElementById(wysiwygId) === null) {
+      throw new Error("Unknown wysiwyg field with id '" + wysiwygId + "'.");
+    }
+
+    this.questionField = document.getElementById(this.wysiwygId + "Poll_question") as HTMLInputElement;
+
+    const optionList = this.container.querySelector(".sortableList");
+    if (optionList === null) {
+      throw new Error("Cannot find poll options list for container with id '" + containerId + "'.");
+    }
+    this.optionList = optionList as HTMLOListElement;
+
+    this.endTimeField = document.getElementById(this.wysiwygId + "Poll_endTime") as HTMLInputElement;
+    this.maxVotesField = document.getElementById(this.wysiwygId + "Poll_maxVotes") as HTMLInputElement;
+    this.isChangeableYesField = document.getElementById(this.wysiwygId + "Poll_isChangeable") as HTMLInputElement;
+    this.isChangeableNoField = document.getElementById(this.wysiwygId + "Poll_isChangeable_no") as HTMLInputElement;
+    this.isPublicYesField = document.getElementById(this.wysiwygId + "Poll_isPublic") as HTMLInputElement;
+    this.isPublicNoField = document.getElementById(this.wysiwygId + "Poll_isPublic_no") as HTMLInputElement;
+    this.resultsRequireVoteYesField = document.getElementById(
+      this.wysiwygId + "Poll_resultsRequireVote",
+    ) as HTMLInputElement;
+    this.resultsRequireVoteNoField = document.getElementById(
+      this.wysiwygId + "Poll_resultsRequireVote_no",
+    ) as HTMLInputElement;
+    this.sortByVotesYesField = document.getElementById(this.wysiwygId + "Poll_sortByVotes") as HTMLInputElement;
+    this.sortByVotesNoField = document.getElementById(this.wysiwygId + "Poll_sortByVotes_no") as HTMLInputElement;
+
+    this.optionCount = 0;
+
+    this.options = Core.extend(
+      {
+        isAjax: false,
+        maxOptions: 20,
+      },
+      options,
+    ) as UiPollEditorOptions;
+
+    this.createOptionList(pollOptions || []);
+
+    new UiSortableList({
+      containerId: containerId,
+      options: {
+        toleranceElement: "> div",
+      },
+    });
+
+    if (this.options.isAjax) {
+      ["handleError", "reset", "submit", "validate"].forEach((event) => {
+        EventHandler.add("com.woltlab.wcf.redactor2", event + "_" + this.wysiwygId, (...args: unknown[]) =>
+          this[event](...args),
+        );
+      });
+    } else {
+      const form = this.container.closest("form");
+      if (form === null) {
+        throw new Error("Cannot find form for container with id '" + containerId + "'.");
+      }
+
+      form.addEventListener("submit", (ev) => this.submit(ev));
+    }
+  }
+
+  /**
+   * Creates a poll option with the given data or an empty poll option of no data is given.
+   */
+  private createOption(optionValue?: string, optionId?: string, insertAfter?: HTMLElement): void {
+    optionValue = optionValue || "";
+    optionId = optionId || "0";
+
+    const listItem = document.createElement("LI") as HTMLLIElement;
+    listItem.classList.add("sortableNode");
+    listItem.dataset.optionId = optionId;
+
+    if (insertAfter) {
+      insertAfter.insertAdjacentElement("afterend", listItem);
+    } else {
+      this.optionList.appendChild(listItem);
+    }
+
+    const pollOptionInput = document.createElement("div");
+    pollOptionInput.classList.add("pollOptionInput");
+    listItem.appendChild(pollOptionInput);
+
+    const sortHandle = document.createElement("span");
+    sortHandle.classList.add("icon", "icon16", "fa-arrows", "sortableNodeHandle");
+    pollOptionInput.appendChild(sortHandle);
+
+    // buttons
+    const addButton = document.createElement("a");
+    listItem.setAttribute("role", "button");
+    listItem.setAttribute("href", "#");
+    addButton.classList.add("icon", "icon16", "fa-plus", "jsTooltip", "jsAddOption", "pointer");
+    addButton.setAttribute("title", Language.get("wcf.poll.button.addOption"));
+    addButton.addEventListener("click", () => this.createOption());
+    pollOptionInput.appendChild(addButton);
+
+    const deleteButton = document.createElement("a");
+    deleteButton.setAttribute("role", "button");
+    deleteButton.setAttribute("href", "#");
+    deleteButton.classList.add("icon", "icon16", "fa-times", "jsTooltip", "jsDeleteOption", "pointer");
+    deleteButton.setAttribute("title", Language.get("wcf.poll.button.removeOption"));
+    deleteButton.addEventListener("click", (ev) => this.removeOption(ev));
+    pollOptionInput.appendChild(deleteButton);
+
+    // input field
+    const optionInput = document.createElement("input");
+    optionInput.type = "text";
+    optionInput.value = optionValue;
+    optionInput.maxLength = 255;
+    optionInput.addEventListener("keydown", (ev) => this.optionInputKeyDown(ev));
+    optionInput.addEventListener("click", () => {
+      // work-around for some weird focus issue on iOS/Android
+      if (document.activeElement !== optionInput) {
+        optionInput.focus();
+      }
+    });
+    pollOptionInput.appendChild(optionInput);
+
+    if (insertAfter !== null) {
+      optionInput.focus();
+    }
+
+    this.optionCount++;
+    if (this.optionCount === this.options.maxOptions) {
+      this.optionList.querySelectorAll(".jsAddOption").forEach((icon: HTMLSpanElement) => {
+        icon.classList.remove("pointer");
+        icon.classList.add("disabled");
+      });
+    }
+  }
+
+  /**
+   * Populates the option list with the current options.
+   */
+  private createOptionList(pollOptions: PollOption[]): void {
+    pollOptions.forEach((option) => {
+      this.createOption(option.optionValue, option.optionID);
+    });
+
+    if (this.optionCount < this.options.maxOptions) {
+      this.createOption();
+    }
+  }
+
+  /**
+   * Handles validation errors returned by Ajax request.
+   */
+  private handleError(data: AjaxResponse): void {
+    switch (data.returnValues.fieldName) {
+      case this.wysiwygId + "Poll_endTime":
+      case this.wysiwygId + "Poll_maxVotes": {
+        const fieldName = data.returnValues.fieldName.replace(this.wysiwygId + "Poll_", "");
+
+        const small = document.createElement("small");
+        small.classList.add("innerError");
+        small.innerHTML = Language.get("wcf.poll." + fieldName + ".error." + data.returnValues.errorType);
+
+        const field = document.getElementById(data.returnValues.fieldName)!;
+        (field.nextSibling! as HTMLElement).insertAdjacentElement("afterbegin", small);
+
+        data.cancel = true;
+        break;
+      }
+    }
+  }
+
+  /**
+   * Adds another option field below the current option field after pressing Enter.
+   */
+  private optionInputKeyDown(event: KeyboardEvent): void {
+    if (event.key !== "Enter") {
+      return;
+    }
+
+    const target = event.currentTarget as HTMLInputElement;
+    const addOption = target.parentElement!.querySelector(".jsAddOption") as HTMLSpanElement;
+    Core.triggerEvent(addOption, "click");
+
+    event.preventDefault();
+  }
+
+  /**
+   * Removes a poll option after clicking on its deletion button.
+   */
+  private removeOption(event: Event): void {
+    event.preventDefault();
+
+    const button = event.currentTarget as HTMLSpanElement;
+    button.closest("li")!.remove();
+
+    this.optionCount--;
+
+    if (this.optionList.childElementCount === 0) {
+      this.createOption();
+    } else {
+      this.optionList.querySelectorAll(".jsAddOption").forEach((icon) => {
+        icon.classList.add("pointer");
+        icon.classList.remove("disabled");
+      });
+    }
+  }
+
+  /**
+   * Resets all poll fields.
+   */
+  private reset(): void {
+    this.questionField.value = "";
+
+    this.optionCount = 0;
+    this.optionList.innerHTML = "";
+    this.createOption();
+
+    DatePicker.clear(this.endTimeField);
+
+    this.maxVotesField.value = "1";
+    this.isChangeableYesField.checked = false;
+    this.isChangeableNoField.checked = true;
+    this.isPublicYesField.checked = false;
+    this.isPublicNoField.checked = true;
+    this.resultsRequireVoteYesField.checked = false;
+    this.resultsRequireVoteNoField.checked = true;
+    this.sortByVotesYesField.checked = false;
+    this.sortByVotesNoField.checked = true;
+
+    EventHandler.fire("com.woltlab.wcf.poll.editor", "reset", {
+      pollEditor: this,
+    });
+  }
+
+  /**
+   * Handles the poll data if the form is submitted.
+   */
+  private submit(event: Event): void {
+    if (this.options.isAjax) {
+      EventHandler.fire("com.woltlab.wcf.poll.editor", "submit", {
+        event: event,
+        pollEditor: this,
+      });
+    } else {
+      const form = this.container.closest("form")!;
+
+      this.getOptions().forEach((option, i) => {
+        const input = document.createElement("input");
+        input.type = "hidden";
+        input.name = `${this.wysiwygId} + 'Poll_options[${i}}]`;
+        input.value = option;
+        form.appendChild(input);
+      });
+    }
+  }
+
+  /**
+   * Validates the poll data.
+   */
+  private validate(data: ValidationData): void {
+    if (this.questionField.value.trim() === "") {
+      return;
+    }
+
+    let nonEmptyOptionCount = 0;
+    Array.from(this.optionList.children).forEach((listItem: HTMLLIElement) => {
+      const optionInput = listItem.querySelector("input[type=text]") as HTMLInputElement;
+      if (optionInput.value.trim() !== "") {
+        nonEmptyOptionCount++;
+      }
+    });
+
+    if (nonEmptyOptionCount === 0) {
+      data.api.throwError(this.container, Language.get("wcf.global.form.error.empty"));
+      data.valid = false;
+    } else {
+      const maxVotes = ~~this.maxVotesField.value;
+
+      if (maxVotes && maxVotes > nonEmptyOptionCount) {
+        data.api.throwError(this.maxVotesField.parentElement, Language.get("wcf.poll.maxVotes.error.invalid"));
+        data.valid = false;
+      } else {
+        EventHandler.fire("com.woltlab.wcf.poll.editor", "validate", {
+          data: data,
+          pollEditor: this,
+        });
+      }
+    }
+  }
+
+  /**
+   * Returns the data of the poll.
+   */
+  public getData(): object {
+    return {
+      [this.questionField.id]: this.questionField.value,
+      [this.wysiwygId + "Poll_options"]: this.getOptions(),
+      [this.endTimeField.id]: this.endTimeField.value,
+      [this.maxVotesField.id]: this.maxVotesField.value,
+      [this.isChangeableYesField.id]: !!this.isChangeableYesField.checked,
+      [this.isPublicYesField.id]: !!this.isPublicYesField.checked,
+      [this.resultsRequireVoteYesField.id]: !!this.resultsRequireVoteYesField.checked,
+      [this.sortByVotesYesField.id]: !!this.sortByVotesYesField.checked,
+    };
+  }
+
+  /**
+   * Returns the selectable options in the poll.
+   *
+   * Format: `{optionID}_{option}` with `optionID = 0` if it is a new option.
+   */
+  public getOptions(): string[] {
+    const options: string[] = [];
+    Array.from(this.optionList.children).forEach((listItem: HTMLLIElement) => {
+      const optionValue = (listItem.querySelector("input[type=text]")! as HTMLInputElement).value.trim();
+
+      if (optionValue !== "") {
+        options.push(`${listItem.dataset.optionId!}_${optionValue}`);
+      }
+    });
+
+    return options;
+  }
+}
+
+Core.enableLegacyInheritance(UiPollEditor);
+
+export = UiPollEditor;