Add quick reply support for WYSIWYG form builder poll
authorMatthias Schmidt <gravatronics@live.com>
Sat, 9 Mar 2019 12:33:05 +0000 (13:33 +0100)
committerMatthias Schmidt <gravatronics@live.com>
Sat, 9 Mar 2019 12:33:05 +0000 (13:33 +0100)
Close #2852

com.woltlab.wcf/templates/__pollOptionsFormField.tpl
wcfsetup/install/files/acp/templates/__pollOptionsFormField.tpl
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Poll/Editor.js [new file with mode: 0644]

index 65fe710244478ea860c6ee13efc193eb08aeef85..c2ea0a9edb2546bb251f0c3b3eee48ff701528cb 100644 (file)
@@ -6,18 +6,21 @@
 
 {js application='wcf' file='WCF.Poll' bundle='WCF.Combined'}
 <script data-relocate="true">
-       require(['Dom/Traverse', 'Dom/Util', 'Language'], function(DomTraverse, DomUtil, Language) {
+       require(['Dom/Traverse', 'Dom/Util', 'Language', 'WoltLabSuite/Core/Ui/Poll/Editor'], function(DomTraverse, DomUtil, Language, UiPollEditor) {
                Language.addObject({
                        'wcf.poll.button.addOption': '{lang}wcf.poll.button.addOption{/lang}',
-                       'wcf.poll.button.removeOption': '{lang}wcf.poll.button.removeOption{/lang}'
+                       'wcf.poll.button.removeOption': '{lang}wcf.poll.button.removeOption{/lang}',
+                       'wcf.poll.maxVotes.error.invalid': '{lang}wcf.poll.maxVotes.error.invalid{/lang}'
                });
                
-               new WCF.Poll.Management(
+               new UiPollEditor(
                        DomUtil.identify(DomTraverse.childByTag(elById('{@$field->getPrefixedId()}Container'), 'DD')),
                        [ {implode from=$field->getValue() item=pollOption}{ optionID: {@$pollOption[optionID]}, optionValue: '{$pollOption[optionValue]|encodeJS}' }{/implode} ],
-                       {@POLL_MAX_OPTIONS},
-                       '{if $field->getDocument()->isAjax()}{@$field->getWysiwygId()}{/if}',
-                       '{@$field->getPrefixedId()}'
+                       '{@$field->getWysiwygId()}',
+                       {
+                               isAjax: {if $field->getDocument()->isAjax()}true{else}false{/if},
+                               maxOptions: {@POLL_MAX_OPTIONS}
+                       }
                );
        });
 </script>
index 65fe710244478ea860c6ee13efc193eb08aeef85..c2ea0a9edb2546bb251f0c3b3eee48ff701528cb 100644 (file)
@@ -6,18 +6,21 @@
 
 {js application='wcf' file='WCF.Poll' bundle='WCF.Combined'}
 <script data-relocate="true">
-       require(['Dom/Traverse', 'Dom/Util', 'Language'], function(DomTraverse, DomUtil, Language) {
+       require(['Dom/Traverse', 'Dom/Util', 'Language', 'WoltLabSuite/Core/Ui/Poll/Editor'], function(DomTraverse, DomUtil, Language, UiPollEditor) {
                Language.addObject({
                        'wcf.poll.button.addOption': '{lang}wcf.poll.button.addOption{/lang}',
-                       'wcf.poll.button.removeOption': '{lang}wcf.poll.button.removeOption{/lang}'
+                       'wcf.poll.button.removeOption': '{lang}wcf.poll.button.removeOption{/lang}',
+                       'wcf.poll.maxVotes.error.invalid': '{lang}wcf.poll.maxVotes.error.invalid{/lang}'
                });
                
-               new WCF.Poll.Management(
+               new UiPollEditor(
                        DomUtil.identify(DomTraverse.childByTag(elById('{@$field->getPrefixedId()}Container'), 'DD')),
                        [ {implode from=$field->getValue() item=pollOption}{ optionID: {@$pollOption[optionID]}, optionValue: '{$pollOption[optionValue]|encodeJS}' }{/implode} ],
-                       {@POLL_MAX_OPTIONS},
-                       '{if $field->getDocument()->isAjax()}{@$field->getWysiwygId()}{/if}',
-                       '{@$field->getPrefixedId()}'
+                       '{@$field->getWysiwygId()}',
+                       {
+                               isAjax: {if $field->getDocument()->isAjax()}true{else}false{/if},
+                               maxOptions: {@POLL_MAX_OPTIONS}
+                       }
                );
        });
 </script>
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 (file)
index 0000000..20aa864
--- /dev/null
@@ -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 <http://opensource.org/licenses/lgpl-license.php>
+ * @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