From 5291e8e599dec6465b3c1401b5b1ab8a56ff0b95 Mon Sep 17 00:00:00 2001 From: Matthias Schmidt Date: Fri, 16 Mar 2018 18:30:41 +0100 Subject: [PATCH] Add `ItemListFormField` See #2509 --- .../acp/templates/__itemListFormField.tpl | 17 + .../WoltLabSuite/Core/Ui/ItemList/Static.js | 481 ++++++++++++++++++ .../DevtoolsFormBuilderTestForm.class.php | 11 +- .../builder/field/ItemListFormField.class.php | 120 +++++ 4 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 wcfsetup/install/files/acp/templates/__itemListFormField.tpl create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Ui/ItemList/Static.js create mode 100644 wcfsetup/install/files/lib/system/form/builder/field/ItemListFormField.class.php diff --git a/wcfsetup/install/files/acp/templates/__itemListFormField.tpl b/wcfsetup/install/files/acp/templates/__itemListFormField.tpl new file mode 100644 index 0000000000..8dd2725e2c --- /dev/null +++ b/wcfsetup/install/files/acp/templates/__itemListFormField.tpl @@ -0,0 +1,17 @@ +{include file='__formFieldHeader'} + +isAutofocused()} autofocus{/if}{if $field->isRequired()} required{/if}{if $field->isImmutable()} disabled{/if}> + + + +{include file='__formFieldFooter'} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/ItemList/Static.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/ItemList/Static.js new file mode 100644 index 0000000000..4df1833822 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/ItemList/Static.js @@ -0,0 +1,481 @@ +/** + * Flexible UI element featuring both a list of items and an input field. + * + * @author Alexander Ebert, Matthias Schmidt + * @copyright 2001-2018 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Ui/ItemList/Static + */ +define(['Core', 'Dictionary', 'Language', 'Dom/Traverse', 'EventKey', 'Ui/SimpleDropdown'], function(Core, Dictionary, Language, DomTraverse, EventKey, UiSimpleDropdown) { + "use strict"; + + var _activeId = ''; + var _data = new Dictionary(); + var _didInit = false; + + var _callbackKeyDown = null; + var _callbackKeyPress = null; + var _callbackKeyUp = null; + var _callbackPaste = null; + var _callbackRemoveItem = null; + var _callbackBlur = null; + + /** + * @exports WoltLabSuite/Core/Ui/ItemList/Static + */ + return { + /** + * Initializes an item list. + * + * The `values` argument must be empty or contain a list of strings or object, e.g. + * `['foo', 'bar']` or `[{ objectId: 1337, value: 'baz'}, {...}]` + * + * @param {string} elementId input element id + * @param {Array} values list of existing values + * @param {Object} options option list + */ + init: function(elementId, values, options) { + var element = elById(elementId); + if (element === null) { + throw new Error("Expected a valid element id, '" + elementId + "' is invalid."); + } + + // remove data from previous instance + if (_data.has(elementId)) { + var tmp = _data.get(elementId); + + for (var key in tmp) { + if (tmp.hasOwnProperty(key)) { + var el = tmp[key]; + if (el instanceof Element && el.parentNode) { + elRemove(el); + } + } + } + + UiSimpleDropdown.destroy(elementId); + _data.delete(elementId); + } + + options = Core.extend({ + // maximum number of items this list may contain, `-1` for infinite + maxItems: -1, + // maximum length of an item value, `-1` for infinite + maxLength: -1, + + // initial value will be interpreted as comma separated value and submitted as such + isCSV: false, + + // will be invoked whenever the items change, receives the element id first and list of values second + callbackChange: null, + // callback once the form is about to be submitted + callbackSubmit: null, + // value may contain the placeholder `{$objectId}` + submitFieldName: '' + }, options); + + var form = DomTraverse.parentByTag(element, 'FORM'); + if (form !== null) { + if (options.isCSV === false) { + if (!options.submitFieldName.length && typeof options.callbackSubmit !== 'function') { + throw new Error("Expected a valid function for option 'callbackSubmit', a non-empty value for option 'submitFieldName' or enabling the option 'submitFieldCSV'."); + } + + form.addEventListener('submit', (function() { + var values = this.getValues(elementId); + if (options.submitFieldName.length) { + var input; + for (var i = 0, length = values.length; i < length; i++) { + input = elCreate('input'); + input.type = 'hidden'; + input.name = options.submitFieldName.replace(/{$objectId}/, values[i].objectId); + input.value = values[i].value; + + form.appendChild(input); + } + } + else { + options.callbackSubmit(form, values); + } + }).bind(this)); + } + } + + this._setup(); + + var data = this._createUI(element, options); + _data.set(elementId, { + dropdownMenu: null, + element: data.element, + list: data.list, + listItem: data.element.parentNode, + options: options, + shadow: data.shadow + }); + + values = (data.values.length) ? data.values : values; + if (Array.isArray(values)) { + var value; + for (var i = 0, length = values.length; i < length; i++) { + value = values[i]; + if (typeof value === 'string') { + value = { objectId: 0, value: value }; + } + + this._addItem(elementId, value); + } + } + }, + + /** + * Returns the list of current values. + * + * @param {string} elementId input element id + * @return {Array} list of objects containing object id and value + */ + getValues: function(elementId) { + if (!_data.has(elementId)) { + throw new Error("Element id '" + elementId + "' is unknown."); + } + + var data = _data.get(elementId); + var values = []; + elBySelAll('.item > span', data.list, function(span) { + values.push({ + objectId: ~~elData(span, 'object-id'), + value: span.textContent + }); + }); + + return values; + }, + + /** + * Sets the list of current values. + * + * @param {string} elementId input element id + * @param {Array} values list of objects containing object id and value + */ + setValues: function(elementId, values) { + if (!_data.has(elementId)) { + throw new Error("Element id '" + elementId + "' is unknown."); + } + + var data = _data.get(elementId); + + // remove all existing items first + var i, length; + var items = DomTraverse.childrenByClass(data.list, 'item'); + for (i = 0, length = items.length; i < length; i++) { + this._removeItem(null, items[i], true); + } + + // add new items + for (i = 0, length = values.length; i < length; i++) { + this._addItem(elementId, values[i]); + } + }, + + /** + * Binds static event listeners. + */ + _setup: function() { + if (_didInit) { + return; + } + + _didInit = true; + + _callbackKeyDown = this._keyDown.bind(this); + _callbackKeyPress = this._keyPress.bind(this); + _callbackKeyUp = this._keyUp.bind(this); + _callbackPaste = this._paste.bind(this); + _callbackRemoveItem = this._removeItem.bind(this); + _callbackBlur = this._blur.bind(this); + }, + + /** + * Creates the DOM structure for target element. If `element` is a `