From: Matthias Schmidt Date: Wed, 4 Nov 2020 15:15:13 +0000 (+0100) Subject: Convert `Ui/Poll/Editor` to TypeScript (#3690) X-Git-Tag: 5.4.0_Alpha_1~639 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=04f34267a5c8a874e9a5e9cd050be026d1e8bf7a;p=GitHub%2FWoltLab%2FWCF.git Convert `Ui/Poll/Editor` to TypeScript (#3690) * 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` --- diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Poll/Editor.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Poll/Editor.js index 5a866f21dd..dce03de840 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Poll/Editor.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Poll/Editor.js @@ -1,240 +1,199 @@ /** * 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 - * @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 + * @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; }); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Poll/Editor.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Poll/Editor.js index 47d3c5c57e..09c1ffe571 100644 --- a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Poll/Editor.js +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Poll/Editor.js @@ -1,430 +1,305 @@ +"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 - * @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 + * @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 index 0000000000..ae8a4498f8 --- /dev/null +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Poll/Editor.ts @@ -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 + * @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;