Added live filter for scrollable checkbox lists
authorAlexander Ebert <ebert@woltlab.com>
Sat, 9 Jul 2016 14:50:25 +0000 (16:50 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Sat, 9 Jul 2016 14:50:25 +0000 (16:50 +0200)
wcfsetup/install/files/acp/templates/boxAdd.tpl
wcfsetup/install/files/acp/templates/pageAdd.tpl
wcfsetup/install/files/js/WoltLab/WCF/Permission.js
wcfsetup/install/files/js/WoltLab/WCF/Ui/ItemList/Filter.js [new file with mode: 0644]
wcfsetup/install/files/style/ui/scrollableCheckboxList.scss
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index ceb6e473cc3e6fb19f2904ff7dcb92cf2adccc4d..ed53d968d779f10b9db9b59604472838be26b133 100644 (file)
                                        <dd>
                                                <label><input type="checkbox" id="visibleEverywhere" name="visibleEverywhere" value="1"{if $visibleEverywhere} checked{/if}> {lang}wcf.acp.box.visibleEverywhere{/lang}</label>
                                                <script data-relocate="true">
-                                                       elById('visibleEverywhere').addEventListener('change', function() {
-                                                               if (this.checked) {
-                                                                       elShow(elById('visibilityExceptionHidden'));
-                                                                       elHide(elById('visibilityExceptionVisible'));
-                                                               }
-                                                               else {
-                                                                       elHide(elById('visibilityExceptionHidden'));
-                                                                       elShow(elById('visibilityExceptionVisible'));
-                                                               } 
+                                                       require(['Language', 'WoltLab/WCF/Ui/ItemList/Filter'], function(Language, UiItemListFilter) {
+                                                               Language.addObject({
+                                                                       'wcf.global.filter.button.clear': '{lang}wcf.global.filter.button.clear{/lang}',
+                                                                       'wcf.global.filter.error.noMatches': '{lang}wcf.global.filter.error.noMatches{/lang}',
+                                                                       'wcf.global.filter.placeholder': '{lang}wcf.global.filter.placeholder{/lang}'
+                                                               });
+                                                               
+                                                               new UiItemListFilter('boxVisibilitySettings');
+                                                               
+                                                               // visibility toggle
+                                                               var visibilityExceptionHidden = elById('visibilityExceptionHidden');
+                                                               var visibilityExceptionVisible = elById('visibilityExceptionVisible');
+                                                               
+                                                               elById('visibleEverywhere').addEventListener('change', function() {
+                                                                       window[this.checked ? 'elShow' : 'elHide'](visibilityExceptionHidden);
+                                                                       window[this.checked ? 'elHide' : 'elShow'](visibilityExceptionVisible);
+                                                               });
                                                        });
                                                </script>
                                        </dd>
                                                <span id="visibilityExceptionHidden"{if !$visibleEverywhere} style="display: none"{/if}>{lang}wcf.acp.box.visibilityException.hidden{/lang}</span>
                                        </dt>
                                        <dd>
-                                               <ul class="scrollableCheckboxList">
+                                               <ul class="scrollableCheckboxList" id="boxVisibilitySettings">
                                                        {foreach from=$pageNodeList item=pageNode}
                                                                <li{if $pageNode->getDepth() > 1} style="padding-left: {$pageNode->getDepth()*20-20}px"{/if}>
                                                                        <label><input type="checkbox" name="pageIDs[]" value="{@$pageNode->pageID}"{if $pageNode->pageID|in_array:$pageIDs} checked{/if}> {$pageNode->name}</label>
index 72fb01709953c74dad2078d2656e01b01a5e5e29..7c8671b4318e58222cb9bdd8cdb4b6a62b4fd2ae 100644 (file)
@@ -2,15 +2,18 @@
 
 <script data-relocate="true">
        $(function() {
-               $('#isLandingPage').change(function(event) {
-                       if ($('#isLandingPage')[0].checked) {
-                               $('#isDisabled')[0].checked = false;
-                               $('#isDisabled')[0].disabled = true;
-                       }
-                       else {
-                               $('#isDisabled')[0].disabled = false;
-                       }
-               }).trigger('change');
+               var isDisabled = elById('isDisabled');
+               if (isDisabled !== null) {
+                       $('#isLandingPage').change(function() {
+                               if ($('#isLandingPage')[0].checked) {
+                                       isDisabled.checked = false;
+                                       isDisabled.disabled = true;
+                               }
+                               else {
+                                       isDisabled.disabled = false;
+                               }
+                       }).trigger('change');
+               }
                
                {if $action != 'edit' || !$page->isLandingPage}
                        $('#isDisabled').change(function(event) {
                                <dl{if $errorField == 'boxIDs'} class="formError"{/if}>
                                        <dt>{lang}wcf.acp.page.boxes{/lang}</dt>
                                        <dd>
-                                               <ul class="scrollableCheckboxList">
+                                               <ul class="scrollableCheckboxList" id="boxVisibilitySettings">
                                                        {foreach from=$availableBoxes item=availableBox}
                                                                <li>
                                                                        <label><input type="checkbox" name="boxIDs[]" value="{@$availableBox->boxID}"{if $availableBox->boxID|in_array:$boxIDs} checked{/if}{if $availableBox->identifier == 'com.woltlab.wcf.MainMenu'} disabled{/if}> {$availableBox->name}</label>
                                                                {/if}
                                                        </small>
                                                {/if}
+                                               <script data-relocate="true">
+                                                       require(['Language', 'WoltLab/WCF/Ui/ItemList/Filter'], function(Language, UiItemListFilter) {
+                                                               Language.addObject({
+                                                                       'wcf.global.filter.button.clear': '{lang}wcf.global.filter.button.clear{/lang}',
+                                                                       'wcf.global.filter.error.noMatches': '{lang}wcf.global.filter.error.noMatches{/lang}',
+                                                                       'wcf.global.filter.placeholder': '{lang}wcf.global.filter.placeholder{/lang}'
+                                                               });
+                                                               
+                                                               new UiItemListFilter('boxVisibilitySettings');
+                                                       });
+                                               </script>
                                        </dd>
                                </dl>
                        </div>
index a4834a82ec91880bc61680982046a5145c86e14a..577babff4888233d5193175267fa98c9e6826325 100644 (file)
@@ -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 <http://opensource.org/licenses/lgpl-license.php>
  * @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 (file)
index 0000000..ca252b1
--- /dev/null
@@ -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 <http://opensource.org/licenses/lgpl-license.php>
+ * @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 = '<span class="icon icon16 fa-times"></span>';
+                       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, '<u>$1</u>');
+                                               
+                                               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;
+});
index c338e139a18691bb9416dcbbe4b9e5a06b30a8e1..9114516d189c64ccf6b54316979f6e066aefac4b 100644 (file)
                white-space: nowrap;
        }
 }
+
+.itemListFilter {
+       max-width: 500px;
+       
+       > .inputAddon {
+               margin-top: 5px;
+       }
+}
index 59e9223f95bd2d080c73098199fae32081360a1e..0ee3dc2cc0916ab2dde0740db1c12a81a1f7d007 100644 (file)
@@ -2364,6 +2364,9 @@ Fehler sind beispielsweise:
 <p class="exceptionText">Hinweis: Der Fehlercode wird zufällig generiert und erlaubt keinen Rückschluss auf die Ursache und ist daher für Dritte nutzlos.</p>]]></item>
                <item name="wcf.global.exception.title"><![CDATA[Ein Fehler ist aufgetreten]]></item>
                <item name="wcf.global.exception.subtitle"><![CDATA[Interner Fehlercode: <span class="exceptionInlineCodeWrapper"><span class="exceptionInlineCode">{$exceptionID}</span></span>]]></item>
+               <item name="wcf.global.filter.button.clear"><![CDATA[Filter löschen]]></item>
+               <item name="wcf.global.filter.error.noMatches"><![CDATA[Keine Übereinstimmungen gefunden.]]></item>
+               <item name="wcf.global.filter.placeholder"><![CDATA[Nach Name filtern]]></item>
                <item name="wcf.global.success"><![CDATA[Die Aktion wurde erfolgreich ausgeführt.]]></item>
                <item name="wcf.global.success.add"><![CDATA[Der Eintrag wurde gespeichert.]]></item>
                <item name="wcf.global.success.edit"><![CDATA[Ihre Änderungen wurden gespeichert.]]></item>
index 2d3e0e270dc1a99ac6cef04fee9a9a8cdb47ab8f..242bf916ef2bced2be6093f0a5155a1ea169678b 100644 (file)
@@ -2342,6 +2342,9 @@ Errors are:
 <p class="exceptionText">Notice: The error code was randomly generated and has no use beyond looking up the full message.</p>]]></item>
                <item name="wcf.global.exception.title"><![CDATA[An error has occured]]></item>
                <item name="wcf.global.exception.subtitle"><![CDATA[Internal error code: <span class="exceptionInlineCodeWrapper"><span class="exceptionInlineCode">{$exceptionID}</span></span>]]></item>
+               <item name="wcf.global.filter.button.clear"><![CDATA[Clear Filter]]></item>
+               <item name="wcf.global.filter.error.noMatches"><![CDATA[Filter does not match anything.]]></item>
+               <item name="wcf.global.filter.placeholder"><![CDATA[Filter by name]]></item>
                <item name="wcf.global.success"><![CDATA[The action has been completed successfully.]]></item>
                <item name="wcf.global.success.add"><![CDATA[The entry has been saved.]]></item>
                <item name="wcf.global.success.edit"><![CDATA[Your changes have been saved.]]></item>