From fe7801f3260beffc1133ade2c9d684d143440f63 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sat, 9 Jul 2016 16:50:25 +0200 Subject: [PATCH] Added live filter for scrollable checkbox lists --- .../install/files/acp/templates/boxAdd.tpl | 28 ++- .../install/files/acp/templates/pageAdd.tpl | 34 +++- .../files/js/WoltLab/WCF/Permission.js | 6 +- .../js/WoltLab/WCF/Ui/ItemList/Filter.js | 184 ++++++++++++++++++ .../style/ui/scrollableCheckboxList.scss | 8 + wcfsetup/install/lang/de.xml | 3 + wcfsetup/install/lang/en.xml | 3 + 7 files changed, 242 insertions(+), 24 deletions(-) create mode 100644 wcfsetup/install/files/js/WoltLab/WCF/Ui/ItemList/Filter.js diff --git a/wcfsetup/install/files/acp/templates/boxAdd.tpl b/wcfsetup/install/files/acp/templates/boxAdd.tpl index ceb6e473cc..ed53d968d7 100644 --- a/wcfsetup/install/files/acp/templates/boxAdd.tpl +++ b/wcfsetup/install/files/acp/templates/boxAdd.tpl @@ -389,15 +389,23 @@
@@ -409,7 +417,7 @@
-
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Permission.js b/wcfsetup/install/files/js/WoltLab/WCF/Permission.js index a4834a82ec..577babff48 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/Permission.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/Permission.js @@ -2,7 +2,7 @@ * Manages user permissions. * * @author Matthias Schmidt - * @copyright 2001-2015 WoltLab GmbH + * @copyright 2001-2016 WoltLab GmbH * @license GNU Lesser General Public License * @module WoltLab/WCF/Permission */ @@ -14,7 +14,7 @@ define(['Dictionary'], function(Dictionary) { /** * @exports WoltLab/WCF/Permission */ - var Permission = { + return { /** * Adds a single permission to the store. * @@ -58,6 +58,4 @@ define(['Dictionary'], function(Dictionary) { return false; } }; - - return Permission; }); diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/ItemList/Filter.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/ItemList/Filter.js new file mode 100644 index 0000000000..ca252b1a38 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ui/ItemList/Filter.js @@ -0,0 +1,184 @@ +/** + * Provides a filter input for checkbox lists. + * + * @author Alexander Ebert + * @copyright 2001-2016 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLab/WCF/Permission + */ +define(['EventKey', 'Language', 'List', 'StringUtil', 'Dom/Util'], function (EventKey, Language, List, StringUtil, DomUtil) { + "use strict"; + + /** + * Creates a new filter input. + * + * @param {string} elementId list element id + * @constructor + */ + function UiItemListFilter(elementId) { this.init(elementId); } + UiItemListFilter.prototype = { + /** + * Creates a new filter input. + * + * @param {string} elementId list element id + */ + init: function(elementId) { + this._value = ''; + + var element = elById(elementId); + if (element === null) { + throw new Error("Expected a valid element id, '" + elementId + "' does not match anything."); + } + else if (!element.classList.contains('scrollableCheckboxList')) { + throw new Error("Filter only works with elements with the CSS class 'scrollableCheckboxList'."); + } + + var container = elCreate('div'); + container.className = 'itemListFilter'; + + element.parentNode.insertBefore(container, element); + container.appendChild(element); + + var inputAddon = elCreate('div'); + inputAddon.className = 'inputAddon'; + + var input = elCreate('input'); + input.className = 'long'; + input.type = 'text'; + input.placeholder = Language.get('wcf.global.filter.placeholder'); + input.addEventListener('keydown', function (event) { + if (EventKey.Enter(event)) { + event.preventDefault(); + } + }); + input.addEventListener('keyup', this._keyup.bind(this)); + + var clearButton = elCreate('a'); + clearButton.href = '#'; + clearButton.className = 'button inputSuffix jsTooltip'; + clearButton.title = Language.get('wcf.global.filter.button.clear'); + clearButton.innerHTML = ''; + clearButton.addEventListener('click', (function(event) { + event.preventDefault(); + + this._input.value = ''; + this._keyup(); + }).bind(this)); + + inputAddon.appendChild(input); + inputAddon.appendChild(clearButton); + + container.appendChild(inputAddon); + + this._container = container; + this._element = element; + this._input = input; + this._items = null; + this._fragment = null; + }, + + /** + * Builds the item list and rebuilds the items' DOM for easier manipulation. + * + * @protected + */ + _buildItems: function() { + this._items = new List(); + + var item; + for (var i = 0, length = this._element.childElementCount; i < length; i++) { + item = this._element.children[i]; + + var label = item.children[0]; + var text = label.textContent.trim(); + + var checkbox = label.children[0]; + while (checkbox.nextSibling) { + label.removeChild(checkbox.nextSibling); + } + + label.appendChild(document.createTextNode(' ')); + + var span = elCreate('span'); + span.textContent = text; + label.appendChild(span); + + this._items.add({ + item: item, + span: span, + text: text + }); + } + }, + + /** + * Rebuilds the list on keyup, uses case-insensitive matching. + * + * @protected + */ + _keyup: function() { + var value = this._input.value.trim(); + if (this._value === value) { + return; + } + + if (this._fragment === null) { + this._fragment = document.createDocumentFragment(); + + // set fixed height to avoid layout jumps + this._element.style.setProperty('height', this._element.offsetHeight + 'px', ''); + } + + // move list into fragment before editing items, increases performance + // by avoiding the browser to perform repaint/layout over and over again + this._fragment.appendChild(this._element); + + if (this._items === null) { + this._buildItems(); + } + + var regexp = new RegExp('(' + StringUtil.escapeRegExp(value) + ')', 'i'); + var hasVisibleItems = (value === ''); + this._items.forEach(function (item) { + if (value === '') { + item.span.textContent = item.text; + + elShow(item.item); + } + else { + if (regexp.test(item.text)) { + item.span.innerHTML = item.text.replace(regexp, '$1'); + + elShow(item.item); + hasVisibleItems = true; + } + else { + elHide(item.item); + } + } + }); + + this._container.insertBefore(this._fragment.firstChild, this._container.firstChild); + this._value = value; + + var innerError = this._container.nextElementSibling; + if (innerError && !innerError.classList.contains('innerError')) innerError = null; + + if (hasVisibleItems) { + if (innerError) { + elRemove(innerError); + } + } + else { + if (!innerError) { + innerError = elCreate('small'); + innerError.className = 'innerError'; + innerError.textContent = Language.get('wcf.global.filter.error.noMatches'); + DomUtil.insertAfter(innerError, this._container); + } + } + } + }; + + return UiItemListFilter; +}); diff --git a/wcfsetup/install/files/style/ui/scrollableCheckboxList.scss b/wcfsetup/install/files/style/ui/scrollableCheckboxList.scss index c338e139a1..9114516d18 100644 --- a/wcfsetup/install/files/style/ui/scrollableCheckboxList.scss +++ b/wcfsetup/install/files/style/ui/scrollableCheckboxList.scss @@ -13,3 +13,11 @@ white-space: nowrap; } } + +.itemListFilter { + max-width: 500px; + + > .inputAddon { + margin-top: 5px; + } +} diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 59e9223f95..0ee3dc2cc0 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -2364,6 +2364,9 @@ Fehler sind beispielsweise:

Hinweis: Der Fehlercode wird zufällig generiert und erlaubt keinen Rückschluss auf die Ursache und ist daher für Dritte nutzlos.

]]> {$exceptionID}]]> + + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 2d3e0e270d..242bf916ef 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -2342,6 +2342,9 @@ Errors are:

Notice: The error code was randomly generated and has no use beyond looking up the full message.

]]> {$exceptionID}]]> + + + -- 2.20.1