From a3b8bdac6b3d48f866ecfafc2bcd767b9e636286 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Wed, 24 Jun 2015 19:27:17 +0200 Subject: [PATCH] Added `UI/ItemList`, `UI/ItemList/User` and `UI/Suggestion` --- .../templates/headIncludeJavaScript.tpl | 1 + .../files/js/WoltLab/WCF/UI/ItemList.js | 413 ++++++++++++++++++ .../files/js/WoltLab/WCF/UI/ItemList/User.js | 49 +++ .../files/js/WoltLab/WCF/UI/Suggestion.js | 245 +++++++++++ wcfsetup/install/files/style/form.less | 87 ++++ wcfsetup/install/lang/de.xml | 1 + wcfsetup/install/lang/en.xml | 1 + 7 files changed, 797 insertions(+) create mode 100644 wcfsetup/install/files/js/WoltLab/WCF/UI/ItemList.js create mode 100644 wcfsetup/install/files/js/WoltLab/WCF/UI/ItemList/User.js create mode 100644 wcfsetup/install/files/js/WoltLab/WCF/UI/Suggestion.js diff --git a/com.woltlab.wcf/templates/headIncludeJavaScript.tpl b/com.woltlab.wcf/templates/headIncludeJavaScript.tpl index 580f9c5fc2..ef04cb4058 100644 --- a/com.woltlab.wcf/templates/headIncludeJavaScript.tpl +++ b/com.woltlab.wcf/templates/headIncludeJavaScript.tpl @@ -96,6 +96,7 @@ requirejs.config({ 'wcf.global.form.error.empty': '{lang}wcf.global.form.error.empty{/lang}', 'wcf.global.form.error.greaterThan': '{lang __literal=true}wcf.global.form.error.greaterThan{/lang}', 'wcf.global.form.error.lessThan': '{lang __literal=true}wcf.global.form.error.lessThan{/lang}', + 'wcf.global.form.input.maxItems': '{lang}wcf.global.form.input.maxItems{/lang}', 'wcf.global.language.noSelection': '{lang}wcf.global.language.noSelection{/lang}', 'wcf.global.loading': '{lang}wcf.global.loading{/lang}', 'wcf.global.page.jumpTo': '{lang}wcf.global.page.jumpTo{/lang}', diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/ItemList.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/ItemList.js new file mode 100644 index 0000000000..31946d400a --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/UI/ItemList.js @@ -0,0 +1,413 @@ +/** + * Flexible UI element featuring both a list of items and an input field with suggestion support. + * + * @author Alexander Ebert + * @copyright 2001-2015 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLab/WCF/UI/ItemList + */ +define(['Core', 'Dictionary', 'Language', 'DOM/Traverse', 'WoltLab/WCF/UI/Suggestion'], function(Core, Dictionary, Language, DOMTraverse, UISuggestion) { + "use strict"; + + var _activeId = ''; + var _data = new Dictionary(); + var _didInit = false; + + var _callbackKeyDown = null; + var _callbackKeyPress = null; + var _callbackKeyUp = null; + var _callbackRemoveItem = null; + + /** + * @exports WoltLab/WCF/UI/ItemList + */ + var UIItemList = { + /** + * 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 = document.getElementById(elementId); + if (element === null) { + throw new Error("Expected a valid element id."); + } + + options = Core.extend({ + // search parameters for suggestions + ajax: { + actionName: 'getSearchResultList', + className: '', + data: {} + }, + + // list of excluded string values, e.g. `['ignore', 'these strings', 'when', 'searching']` + excludedSearchValues: [], + // maximum number of items this list may contain, `-1` for infinite + maxItems: -1, + // maximum length of an item value, `-1` for infinite + maxLength: -1, + // disallow custom values, only values offered by the suggestion dropdown are accepted + restricted: false, + + // 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 = document.createElement('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, values); + var suggestion = new UISuggestion(elementId, { + ajax: options.ajax, + callbackSelect: this._addItem.bind(this), + excludedSearchValues: options.excludedSearchValues + }); + + _data.set(elementId, { + dropdownMenu: null, + element: data.element, + list: data.list, + listItem: data.element.parentNode, + options: options, + shadow: data.shadow, + suggestion: suggestion + }); + + 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} element id 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 items = DOMTraverse.childrenByClass(data.list, 'item'); + var values = [], value, item; + for (var i = 0, length = items.length; i < length; i++) { + item = items[i]; + value = { + objectId: item.getAttribute('data-object-id'), + value: DOMTraverse.childByTag(item, 'SPAN').textContent + }; + + values.push(value); + } + + return values; + }, + + /** + * 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); + _callbackRemoveItem = this._removeItem.bind(this); + }, + + /** + * Creates the DOM structure for target element. If `element` is a `