From 87338c1eb0fb77d3af487d96dd16d1033769aeb2 Mon Sep 17 00:00:00 2001 From: Matthias Schmidt Date: Sat, 9 Mar 2019 13:33:05 +0100 Subject: [PATCH] Add quick reply support for WYSIWYG form builder poll Close #2852 --- .../templates/__pollOptionsFormField.tpl | 15 +- .../acp/templates/__pollOptionsFormField.tpl | 15 +- .../js/WoltLabSuite/Core/Ui/Poll/Editor.js | 410 ++++++++++++++++++ 3 files changed, 428 insertions(+), 12 deletions(-) create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Poll/Editor.js diff --git a/com.woltlab.wcf/templates/__pollOptionsFormField.tpl b/com.woltlab.wcf/templates/__pollOptionsFormField.tpl index 65fe710244..c2ea0a9edb 100644 --- a/com.woltlab.wcf/templates/__pollOptionsFormField.tpl +++ b/com.woltlab.wcf/templates/__pollOptionsFormField.tpl @@ -6,18 +6,21 @@ {js application='wcf' file='WCF.Poll' bundle='WCF.Combined'} diff --git a/wcfsetup/install/files/acp/templates/__pollOptionsFormField.tpl b/wcfsetup/install/files/acp/templates/__pollOptionsFormField.tpl index 65fe710244..c2ea0a9edb 100644 --- a/wcfsetup/install/files/acp/templates/__pollOptionsFormField.tpl +++ b/wcfsetup/install/files/acp/templates/__pollOptionsFormField.tpl @@ -6,18 +6,21 @@ {js application='wcf' file='WCF.Poll' bundle='WCF.Combined'} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Poll/Editor.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Poll/Editor.js new file mode 100644 index 0000000000..20aa864d80 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Poll/Editor.js @@ -0,0 +1,410 @@ +/** + * Handles the data to create and edit a poll in a form created via form builder. + * + * @author Alexander Ebert, Matthias Schmidt + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Ui/Poll/Editor + * @since 5.2 + */ +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 { + console.log(this.optionList); + 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 = false; + this.isPublicNoField = true; + this.resultsRequireVoteYesField = false; + this.resultsRequireVoteNoField = true; + this.sortByVotesYesField = false; + this.sortByVotesNoField = 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) { + 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); + } + } + + if (this._options.isAjax) { + event.poll = {}; + + event.poll[this.questionField.id] = this.questionField.value; + event.poll[this._wysiwygId + 'Poll_options'] = options; + event.poll[this.endTimeField.id] = this.endTimeField.value; + event.poll[this.maxVotesField.id] = this.maxVotesField.value; + event.poll[this.isChangeableYesField.id] = !!this.isChangeableYesField.checked; + event.poll[this.isPublicYesField.id] = !!this.isPublicYesField.checked; + event.poll[this.resultsRequireVoteYesField.id] = !!this.resultsRequireVoteYesField.checked; + event.poll[this.sortByVotesYesField.id] = !!this.sortByVotesYesField.checked; + + EventHandler.fire( + 'com.woltlab.wcf.poll.editor', + 'submit', + { + event: event, + pollEditor: this + } + ); + } + else { + var form = this._container.closest('form'); + + 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 + } + ); + } + } + } + }; + + return UiPollEditor; +}); \ No newline at end of file -- 2.20.1