From be973d31be87758b8bd8c9228c00b120b38aa6dd Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Fri, 18 Jul 2025 12:41:04 +0200 Subject: [PATCH 1/4] Group the condition types and make it possible to open/close the groups in the input field. --- ...ed_categorizedSingleSelectionFormField.tpl | 40 ++++ .../Core/Component/ItemList/Categorized.ts | 182 ++++++++++++++++++ .../Core/Component/ItemList/Categorized.js | 146 ++++++++++++++ .../lib/action/ConditionAddAction.class.php | 81 ++++++-- .../provider/UserConditionProvider.class.php | 10 + .../condition/type/IConditionType.class.php | 7 + .../ActivePageRequestConditionType.class.php | 6 + .../DayOfWeekRequestConditionType.class.php | 6 + ...NotDayOfWeekRequestConditionType.class.php | 6 + .../NotOnPageRequestConditionType.class.php | 6 + .../TimeRequestConditionType.class.php | 6 + .../user/BooleanUserConditionType.class.php | 7 + .../HasNotTrophyUserConditionType.class.php | 6 + .../user/HasTrophyUserConditionType.class.php | 6 + .../user/InGroupUserConditionType.class.php | 6 + .../user/IntegerUserConditionType.class.php | 7 + .../user/IsEnabledConditionType.class.php | 2 +- .../user/IsNullUserConditionType.class.php | 7 + .../user/LanguageUserConditionType.class.php | 6 + .../NotInGroupUserConditionType.class.php | 6 + ...egistrationDateUserConditionType.class.php | 6 + ...egistrationDaysUserConditionType.class.php | 6 + .../user/SignatureUserConditionType.class.php | 1 + .../user/StringUserConditionType.class.php | 7 + wcfsetup/install/lang/de.xml | 4 + wcfsetup/install/lang/en.xml | 4 + 26 files changed, 565 insertions(+), 12 deletions(-) create mode 100644 com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl create mode 100644 ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js diff --git a/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl b/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl new file mode 100644 index 0000000000..245e5ab921 --- /dev/null +++ b/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl @@ -0,0 +1,40 @@ + + +
+
+ + +
+ +
\ No newline at end of file diff --git a/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts b/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts new file mode 100644 index 0000000000..b29b9270a2 --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts @@ -0,0 +1,182 @@ +/** + * Provides a filter input for a categorized item list. + * + * @author Olaf Braun + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @sice 6.3 + */ + +import { innerError, show, hide, isHidden } from "WoltLabSuite/Core/Dom/Util"; +import { getPhrase } from "WoltLabSuite/Core/Language"; +import { escapeRegExp } from "WoltLabSuite/Core/StringUtil"; + +interface Item { + element: HTMLLIElement; + span: HTMLSpanElement; + text: string; +} + +interface Category { + items: Item[]; + element: HTMLLIElement; +} + +export class CategorizedItemList { + #container: HTMLElement; + #elementList: HTMLUListElement; + #input: HTMLInputElement; + #value: string = ""; + #clearButton: HTMLButtonElement; + #categories: Category[] = []; + #fragment: DocumentFragment; + + constructor(elementId: string) { + this.#fragment = document.createDocumentFragment(); + + const container = document.getElementById(elementId); + if (!container) { + throw new Error(`Element with ID ${elementId} not found.`); + } + + this.#container = container; + this.#elementList = this.#container.querySelector(".scrollableCheckboxList")!; + + this.#input = this.#container.querySelector(".inputAddon > input") as HTMLInputElement; + this.#input.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + } + }); + this.#input.addEventListener("keyup", () => this.#keyup()); + + this.#clearButton = this.#container.querySelector(".inputAddon > .clearButton")!; + this.#clearButton.addEventListener("click", (event) => { + event.preventDefault(); + + this.#input.value = ""; + this.#keyup(); + }); + + this.#buildItemMap(); + } + + #buildItemMap(): void { + let category: Category | null = null; + for (const li of this.#elementList.querySelectorAll(":scope > li")) { + const input = li.querySelector('input[type="radio"]'); + if (input) { + if (!category) { + throw new Error("Input found without a preceding category."); + } + + category.items.push({ + element: li, + span: li.querySelector("span")!, + text: li.innerText.trim(), + }); + } else { + const items: Item[] = []; + category = { + items: items, + element: li, + }; + this.#categories.push(category); + + li.addEventListener("click", (event) => { + this.#categoryClick(event, li, items); + }); + } + } + } + + #categoryClick(event: MouseEvent, li: HTMLLIElement, items: Item[]): void { + event.preventDefault(); + + const isOpen = !this.#categoryIsOpen(li); + li.dataset.open = isOpen ? "true" : "false"; + + li.querySelector("fa-icon")!.setIcon(isOpen ? "chevron-down" : "chevron-right"); + + this.#showItems({ + items: items, + element: li, + }); + } + + #categoryIsOpen(category: HTMLLIElement): boolean { + return category.dataset.open === "true"; + } + + #keyup(): void { + const value = this.#input.value.trim(); + if (this.#value === value) { + return; + } + + this.#value = value; + + if (this.#value) { + this.#clearButton.classList.remove("disabled"); + } else { + this.#clearButton.classList.add("disabled"); + } + + // 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.#elementList); + + this.#categories.forEach((category) => { + this.#showItems(category); + }); + + const hasVisibleItems = Array.from( + this.#elementList.querySelectorAll(".scrollableCheckboxList > li"), + ).some((li) => { + return !isHidden(li); + }); + + this.#container.insertAdjacentElement("beforeend", this.#elementList); + + innerError(this.#container, hasVisibleItems ? false : getPhrase("wcf.global.filter.error.noMatches")); + } + + #showItems(category: Category): void { + const categoryIsOpen = this.#categoryIsOpen(category.element); + const regexp = new RegExp("(" + escapeRegExp(this.#value) + ")", "i"); + + if (this.#value === "") { + show(category.element); + + category.items.forEach((item) => { + item.span.innerHTML = item.text; // Reset highlighting + + if (categoryIsOpen) { + show(item.element); + } else { + hide(item.element); + } + }); + } else { + if (category.items.some((item) => regexp.test(item.text))) { + show(category.element); + + category.items.forEach((item) => { + if (categoryIsOpen && regexp.test(item.text)) { + item.span.innerHTML = item.text.replace(regexp, "$1"); + + show(item.element); + } else { + hide(item.element); + } + }); + } else { + hide(category.element); + + category.items.forEach((item) => { + hide(item.element); + }); + } + } + } +} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js new file mode 100644 index 0000000000..b198ddea7f --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js @@ -0,0 +1,146 @@ +/** + * Provides a filter input for a categorized item list. + * + * @author Olaf Braun + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @sice 6.3 + */ +define(["require", "exports", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/StringUtil"], function (require, exports, Util_1, Language_1, StringUtil_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.CategorizedItemList = void 0; + class CategorizedItemList { + #container; + #elementList; + #input; + #value = ""; + #clearButton; + #categories = []; + #fragment; + constructor(elementId) { + this.#fragment = document.createDocumentFragment(); + const container = document.getElementById(elementId); + if (!container) { + throw new Error(`Element with ID ${elementId} not found.`); + } + this.#container = container; + this.#elementList = this.#container.querySelector(".scrollableCheckboxList"); + this.#input = this.#container.querySelector(".inputAddon > input"); + this.#input.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + } + }); + this.#input.addEventListener("keyup", () => this.#keyup()); + this.#clearButton = this.#container.querySelector(".inputAddon > .clearButton"); + this.#clearButton.addEventListener("click", (event) => { + event.preventDefault(); + this.#input.value = ""; + this.#keyup(); + }); + this.#buildItemMap(); + } + #buildItemMap() { + let category = null; + for (const li of this.#elementList.querySelectorAll(":scope > li")) { + const input = li.querySelector('input[type="radio"]'); + if (input) { + if (!category) { + throw new Error("Input found without a preceding category."); + } + category.items.push({ + element: li, + span: li.querySelector("span"), + text: li.innerText.trim(), + }); + } + else { + const items = []; + category = { + items: items, + element: li, + }; + this.#categories.push(category); + li.addEventListener("click", (event) => { + this.#categoryClick(event, li, items); + }); + } + } + } + #categoryClick(event, li, items) { + event.preventDefault(); + const isOpen = !this.#categoryIsOpen(li); + li.dataset.open = isOpen ? "true" : "false"; + li.querySelector("fa-icon").setIcon(isOpen ? "chevron-down" : "chevron-right"); + this.#showItems({ + items: items, + element: li, + }); + } + #categoryIsOpen(category) { + return category.dataset.open === "true"; + } + #keyup() { + const value = this.#input.value.trim(); + if (this.#value === value) { + return; + } + this.#value = value; + if (this.#value) { + this.#clearButton.classList.remove("disabled"); + } + else { + this.#clearButton.classList.add("disabled"); + } + // 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.#elementList); + this.#categories.forEach((category) => { + this.#showItems(category); + }); + const hasVisibleItems = Array.from(this.#elementList.querySelectorAll(".scrollableCheckboxList > li")).some((li) => { + return !(0, Util_1.isHidden)(li); + }); + this.#container.insertAdjacentElement("beforeend", this.#elementList); + (0, Util_1.innerError)(this.#container, hasVisibleItems ? false : (0, Language_1.getPhrase)("wcf.global.filter.error.noMatches")); + } + #showItems(category) { + const categoryIsOpen = this.#categoryIsOpen(category.element); + const regexp = new RegExp("(" + (0, StringUtil_1.escapeRegExp)(this.#value) + ")", "i"); + if (this.#value === "") { + (0, Util_1.show)(category.element); + category.items.forEach((item) => { + item.span.innerHTML = item.text; // Reset highlighting + if (categoryIsOpen) { + (0, Util_1.show)(item.element); + } + else { + (0, Util_1.hide)(item.element); + } + }); + } + else { + if (category.items.some((item) => regexp.test(item.text))) { + (0, Util_1.show)(category.element); + category.items.forEach((item) => { + if (categoryIsOpen && regexp.test(item.text)) { + item.span.innerHTML = item.text.replace(regexp, "$1"); + (0, Util_1.show)(item.element); + } + else { + (0, Util_1.hide)(item.element); + } + }); + } + else { + (0, Util_1.hide)(category.element); + category.items.forEach((item) => { + (0, Util_1.hide)(item.element); + }); + } + } + } + } + exports.CategorizedItemList = CategorizedItemList; +}); diff --git a/wcfsetup/install/files/lib/action/ConditionAddAction.class.php b/wcfsetup/install/files/lib/action/ConditionAddAction.class.php index 83c4c2772b..4e3da4d954 100644 --- a/wcfsetup/install/files/lib/action/ConditionAddAction.class.php +++ b/wcfsetup/install/files/lib/action/ConditionAddAction.class.php @@ -82,22 +82,14 @@ private function getForm(AbstractConditionProvider $provider): Psr15DialogForm self::class, WCF::getLanguage()->get('wcf.condition.add') ); - $options = \array_map( - static fn (IConditionType $conditionType) => WCF::getLanguage()->get($conditionType->getLabel()), - $provider->getConditionTypes() - ); - $collator = new \Collator(WCF::getLanguage()->getLocale()); - \uasort( - $options, - static fn (string $a, string $b) => $collator->compare($a, $b) - ); $form->appendChild( - SingleSelectionFormField::create('conditionType') + $this->getConditionTypeFormField() + ->id('conditionType') ->label('wcf.condition.condition') ->filterable() ->required() - ->options($options) + ->options($this->getOptions($provider), true, false) ); $form->markRequiredFields(false); @@ -105,4 +97,71 @@ private function getForm(AbstractConditionProvider $provider): Psr15DialogForm return $form; } + + /** + * @param AbstractConditionProvider> $provider + * + * @return array{} + */ + private function getOptions(AbstractConditionProvider $provider): array + { + $conditionTypes = $provider->getConditionTypes(); + + $grouped = []; + foreach ($conditionTypes as $key => $conditionType) { + $category = $conditionType->getCategory(); + $label = $conditionType->getLabel(); + + if (!isset($grouped[$category])) { + $grouped[$category] = [ + "items" => [], + "label" => WCF::getLanguage()->get('wcf.condition.category.' . $category), + ]; + } + + $grouped[$category]["items"][$key] = WCF::getLanguage()->get($label); + } + + $collator = new \Collator(WCF::getLanguage()->getLocale()); + + foreach ($grouped as &$category) { + \uasort($category["items"], static function ($labelA, $labelB) use ($collator) { + return $collator->compare($labelA, $labelB); + }); + } + unset($category); + + \uasort($grouped, static function ($catA, $catB) use ($collator) { + return $collator->compare($catA['label'], $catB['label']); + }); + + $options = []; + + foreach ($grouped as $categoryKey => $category) { + $options[] = [ + 'depth' => 0, + 'isSelectable' => false, + 'label' => $category["label"], + 'value' => $categoryKey, + ]; + + foreach ($category["items"] as $key => $label) { + $options[] = [ + 'depth' => 1, + 'isSelectable' => true, + 'label' => $label, + 'value' => $key, + ]; + } + } + + return $options; + } + + private function getConditionTypeFormField(): SingleSelectionFormField + { + return new class extends SingleSelectionFormField { + protected $templateName = 'shared_categorizedSingleSelectionFormField'; + }; + } } diff --git a/wcfsetup/install/files/lib/system/condition/provider/UserConditionProvider.class.php b/wcfsetup/install/files/lib/system/condition/provider/UserConditionProvider.class.php index 6ac0a8792b..b1d3736336 100644 --- a/wcfsetup/install/files/lib/system/condition/provider/UserConditionProvider.class.php +++ b/wcfsetup/install/files/lib/system/condition/provider/UserConditionProvider.class.php @@ -39,6 +39,7 @@ public function __construct() new StringUserConditionType( identifier: "username", columnName: "username", + category: "user", migrateKeyName: "username", migrateConditionObjectType: 'com.woltlab.wcf.user.username' ), @@ -47,6 +48,7 @@ public function __construct() new StringUserConditionType( identifier: "email", columnName: "email", + category: "user", migrateKeyName: "email", migrateConditionObjectType: 'com.woltlab.wcf.user.email' ), @@ -70,6 +72,7 @@ public function __construct() new IsNullUserConditionType( identifier: "avatar", columnName: 'avatarFileID', + category: 'userProfile', migrateKeyName: 'userAvatar', migrateConditionObjectType: 'com.woltlab.wcf.user.avatar' ), @@ -81,6 +84,7 @@ public function __construct() new IsNullUserConditionType( identifier: "coverPhoto", columnName: 'coverPhotoFileID', + category: 'userProfile', migrateKeyName: 'userCoverPhoto', migrateConditionObjectType: 'com.woltlab.wcf.coverPhoto' ), @@ -89,6 +93,7 @@ public function __construct() new BooleanUserConditionType( identifier: "isBanned", columnName: 'banned', + category: 'user', migrateKeyName: 'userIsBanned', migrateConditionObjectType: 'com.woltlab.wcf.user.state' ), @@ -100,6 +105,7 @@ public function __construct() new IsNullUserConditionType( identifier: "isEmailConfirmed", columnName: 'emailConfirmed', + category: 'user', migrateKeyName: 'userIsEmailConfirmed', migrateConditionObjectType: 'com.woltlab.wcf.user.state' ), @@ -108,6 +114,7 @@ public function __construct() new BooleanUserConditionType( identifier: "isMultifactorActive", columnName: 'multifactorActive', + category: 'user', migrateKeyName: 'multifactorActive', migrateConditionObjectType: 'com.woltlab.wcf.user.multifactor' ), @@ -122,6 +129,7 @@ public function __construct() new IntegerUserConditionType( identifier: "activityPoints", columnName: "activityPoints", + category: 'userProfile', migrateConditionObjectType: 'com.woltlab.wcf.user.activityPoints' ), ); @@ -129,6 +137,7 @@ public function __construct() new IntegerUserConditionType( identifier: "likesReceived", columnName: "likesReceived", + category: 'userProfile', migrateConditionObjectType: 'com.woltlab.wcf.user.likesReceived' ), ); @@ -136,6 +145,7 @@ public function __construct() new IntegerUserConditionType( identifier: "trophyPoints", columnName: "trophyPoints", + category: 'userProfile', migrateConditionObjectType: 'com.woltlab.wcf.user.trophyPoints' ), ); diff --git a/wcfsetup/install/files/lib/system/condition/type/IConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/IConditionType.class.php index 03b9aa4219..fee6e9f8c0 100644 --- a/wcfsetup/install/files/lib/system/condition/type/IConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/IConditionType.class.php @@ -36,4 +36,11 @@ public function getLabel(): string; * @param TFilter $filter */ public function setFilter(mixed $filter): void; + + /** + * Get the name of the category for this condition type. + * All condition types with the same category are grouped together. + * The language variable for the category name is `wcf.condition.category.`. + */ + public function getCategory(): string; } diff --git a/wcfsetup/install/files/lib/system/condition/type/request/ActivePageRequestConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/request/ActivePageRequestConditionType.class.php index af5436701c..b1e075568a 100644 --- a/wcfsetup/install/files/lib/system/condition/type/request/ActivePageRequestConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/request/ActivePageRequestConditionType.class.php @@ -47,6 +47,12 @@ public function matches(): bool return \in_array(RequestHandler::getInstance()->getActivePageID(), $this->filter); } + #[\Override] + public function getCategory(): string + { + return "page"; + } + #[\Override] public function migrateConditionData(array &$conditionData): array { diff --git a/wcfsetup/install/files/lib/system/condition/type/request/DayOfWeekRequestConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/request/DayOfWeekRequestConditionType.class.php index fff8550465..ef6b2a5f2b 100644 --- a/wcfsetup/install/files/lib/system/condition/type/request/DayOfWeekRequestConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/request/DayOfWeekRequestConditionType.class.php @@ -53,6 +53,12 @@ public function matches(): bool return $dateTime->format('w') === $this->filter; } + #[\Override] + public function getCategory(): string + { + return "pointInTime"; + } + #[\Override] public function migrateConditionData(array &$conditionData): array { diff --git a/wcfsetup/install/files/lib/system/condition/type/request/NotDayOfWeekRequestConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/request/NotDayOfWeekRequestConditionType.class.php index f6853a99a6..c415c74f51 100644 --- a/wcfsetup/install/files/lib/system/condition/type/request/NotDayOfWeekRequestConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/request/NotDayOfWeekRequestConditionType.class.php @@ -53,6 +53,12 @@ public function matches(): bool return $dateTime->format('w') !== $this->filter; } + #[\Override] + public function getCategory(): string + { + return "pointInTime"; + } + #[\Override] public function migrateConditionData(array &$conditionData): array { diff --git a/wcfsetup/install/files/lib/system/condition/type/request/NotOnPageRequestConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/request/NotOnPageRequestConditionType.class.php index 3b80a49c73..1b29d932a1 100644 --- a/wcfsetup/install/files/lib/system/condition/type/request/NotOnPageRequestConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/request/NotOnPageRequestConditionType.class.php @@ -49,6 +49,12 @@ public function matches(): bool return RequestHandler::getInstance()->getActivePageID() !== (int)$this->filter; } + #[\Override] + public function getCategory(): string + { + return "page"; + } + #[\Override] public function migrateConditionData(array &$conditionData): array { diff --git a/wcfsetup/install/files/lib/system/condition/type/request/TimeRequestConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/request/TimeRequestConditionType.class.php index 2784c364ea..06aa7371f3 100644 --- a/wcfsetup/install/files/lib/system/condition/type/request/TimeRequestConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/request/TimeRequestConditionType.class.php @@ -74,6 +74,12 @@ public function matches(): bool }; } + #[\Override] + public function getCategory(): string + { + return "pointInTime"; + } + /** * @return array */ diff --git a/wcfsetup/install/files/lib/system/condition/type/user/BooleanUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/BooleanUserConditionType.class.php index c2ee9504da..561a9de226 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/BooleanUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/BooleanUserConditionType.class.php @@ -26,6 +26,7 @@ class BooleanUserConditionType extends AbstractConditionType implements IDatabas public function __construct( public readonly string $identifier, public readonly string $columnName, + public readonly string $category, public readonly ?string $migrateKeyName = null, public readonly ?string $migrateConditionObjectType = null, ) { @@ -69,6 +70,12 @@ public function matches(object $object): bool } } + #[\Override] + public function getCategory(): string + { + return $this->category; + } + #[\Override] public function migrateConditionData(array &$conditionData): array { diff --git a/wcfsetup/install/files/lib/system/condition/type/user/HasNotTrophyUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/HasNotTrophyUserConditionType.class.php index 2c2306ae1b..523f232045 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/HasNotTrophyUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/HasNotTrophyUserConditionType.class.php @@ -71,6 +71,12 @@ public function matches(object $object): bool return !\in_array((int)$this->filter, $trophyIDs, true); } + #[\Override] + public function getCategory(): string + { + return "userProfile"; + } + /** * @return Trophy[] */ diff --git a/wcfsetup/install/files/lib/system/condition/type/user/HasTrophyUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/HasTrophyUserConditionType.class.php index 72b710fb7d..8333af3317 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/HasTrophyUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/HasTrophyUserConditionType.class.php @@ -71,6 +71,12 @@ public function matches(object $object): bool return \in_array((int)$this->filter, $trophyIDs, true); } + #[\Override] + public function getCategory(): string + { + return "userProfile"; + } + /** * @return Trophy[] */ diff --git a/wcfsetup/install/files/lib/system/condition/type/user/InGroupUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/InGroupUserConditionType.class.php index 3b57a26675..a64510d45b 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/InGroupUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/InGroupUserConditionType.class.php @@ -71,6 +71,12 @@ public function matches(object $object): bool return \in_array((int)$this->filter, $object->getGroupIDs(), true); } + #[\Override] + public function getCategory(): string + { + return "user"; + } + #[\Override] public function canMigrateConditionData(string $objectType): bool { diff --git a/wcfsetup/install/files/lib/system/condition/type/user/IntegerUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/IntegerUserConditionType.class.php index 5b427ff3a4..9319ece74e 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/IntegerUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/IntegerUserConditionType.class.php @@ -29,6 +29,7 @@ class IntegerUserConditionType extends AbstractConditionType implements IDatabas public function __construct( public readonly string $identifier, public readonly string $columnName, + public readonly string $category, public readonly ?string $migrateConditionObjectType = null, ) { } @@ -83,6 +84,12 @@ public function matches(object $object): bool }; } + #[\Override] + public function getCategory(): string + { + return $this->category; + } + /** * @return string[] */ diff --git a/wcfsetup/install/files/lib/system/condition/type/user/IsEnabledConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/IsEnabledConditionType.class.php index 1a81c66eb1..84cdcd27b2 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/IsEnabledConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/IsEnabledConditionType.class.php @@ -14,7 +14,7 @@ final class IsEnabledConditionType extends BooleanUserConditionType { public function __construct() { - parent::__construct("isEnabled", 'activationCode', 'userIsEnabled', 'com.woltlab.wcf.user.state'); + parent::__construct("isEnabled", 'activationCode', 'user', 'userIsEnabled', 'com.woltlab.wcf.user.state'); } #[\Override] diff --git a/wcfsetup/install/files/lib/system/condition/type/user/IsNullUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/IsNullUserConditionType.class.php index 6f243bc89f..f032230ec9 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/IsNullUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/IsNullUserConditionType.class.php @@ -26,6 +26,7 @@ class IsNullUserConditionType extends AbstractConditionType implements IDatabase public function __construct( public readonly string $identifier, public readonly string $columnName, + public readonly string $category, public readonly ?string $migrateKeyName = null, public readonly ?string $migrateConditionObjectType = null, ) {} @@ -68,6 +69,12 @@ public function matches(object $object): bool } } + #[\Override] + public function getCategory(): string + { + return $this->category; + } + #[\Override] public function migrateConditionData(array &$conditionData): array { diff --git a/wcfsetup/install/files/lib/system/condition/type/user/LanguageUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/LanguageUserConditionType.class.php index ba74a10afd..04ae4fa1d3 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/LanguageUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/LanguageUserConditionType.class.php @@ -61,6 +61,12 @@ public function matches(object $object): bool return (int)$this->filter === $object->languageID; } + #[\Override] + public function getCategory(): string + { + return "user"; + } + #[\Override] public function migrateConditionData(array &$conditionData): array { diff --git a/wcfsetup/install/files/lib/system/condition/type/user/NotInGroupUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/NotInGroupUserConditionType.class.php index 12731c3864..126edf1b97 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/NotInGroupUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/NotInGroupUserConditionType.class.php @@ -71,6 +71,12 @@ public function matches(object $object): bool return !\in_array((int)$this->filter, $object->getGroupIDs(), true); } + #[\Override] + public function getCategory(): string + { + return "user"; + } + #[\Override] public function canMigrateConditionData(string $objectType): bool { diff --git a/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDateUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDateUserConditionType.class.php index 717d5b0fcc..51ecca460a 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDateUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDateUserConditionType.class.php @@ -79,6 +79,12 @@ public function matches(object $object): bool }; } + #[\Override] + public function getCategory(): string + { + return "user"; + } + /** * @return string[] */ diff --git a/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDaysUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDaysUserConditionType.class.php index febacbd506..dd26a01eec 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDaysUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDaysUserConditionType.class.php @@ -56,6 +56,12 @@ public function matches(object $object): bool }; } + #[\Override] + public function getCategory(): string + { + return "user"; + } + /** * @return array{condition: string, timestamp: int} */ diff --git a/wcfsetup/install/files/lib/system/condition/type/user/SignatureUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/SignatureUserConditionType.class.php index 30e5e74b8a..f8b8fffeac 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/SignatureUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/SignatureUserConditionType.class.php @@ -17,6 +17,7 @@ public function __construct() parent::__construct( 'signature', 'signature', + 'userProfile', 'userSignature', 'com.woltlab.wcf.user.signature' ); diff --git a/wcfsetup/install/files/lib/system/condition/type/user/StringUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/StringUserConditionType.class.php index d3f6d022b1..6032f4eb5b 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/StringUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/StringUserConditionType.class.php @@ -30,6 +30,7 @@ class StringUserConditionType extends AbstractConditionType implements IDatabase public function __construct( public readonly string $identifier, public readonly string $columnName, + public readonly string $category, public readonly ?string $migrateKeyName = null, public readonly ?string $migrateConditionObjectType = null, ) {} @@ -107,6 +108,12 @@ private function getConditions(): array ]; } + #[\Override] + public function getCategory(): string + { + return $this->category; + } + #[\Override] public function canMigrateConditionData(string $objectType): bool { diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index f9d817fdfb..3fb5b6c8e0 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -3570,6 +3570,10 @@ Erlaubte Dateiendungen: {', '|implode:$attachmentHandler->getFormattedAllowedExt + + + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index bd2e8c60d7..3be6ec4e53 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -3493,6 +3493,10 @@ Allowed extensions: {', '|implode:$attachmentHandler->getFormattedAllowedExtensi + + + + From 587f5e8374bed6636a1c05df51ab6035f2d1249c Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Tue, 22 Jul 2025 17:40:39 +0200 Subject: [PATCH 2/4] Minor code fixes --- ...ed_categorizedSingleSelectionFormField.tpl | 6 ++--- .../Core/Component/ItemList/Categorized.ts | 24 +++++++++---------- .../Core/Component/ItemList/Categorized.js | 2 +- .../lib/action/ConditionAddAction.class.php | 12 +++++----- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl b/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl index 245e5ab921..c29c828893 100644 --- a/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl +++ b/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl @@ -14,7 +14,7 @@
- +
    @@ -32,9 +32,9 @@ *}> {unsafe:$__fieldNestedOption[label]} {else} - + {/if} {/foreach}
-
\ No newline at end of file + diff --git a/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts b/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts index b29b9270a2..b8c9b95a6f 100644 --- a/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts +++ b/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts @@ -11,25 +11,25 @@ import { innerError, show, hide, isHidden } from "WoltLabSuite/Core/Dom/Util"; import { getPhrase } from "WoltLabSuite/Core/Language"; import { escapeRegExp } from "WoltLabSuite/Core/StringUtil"; -interface Item { +type Item = { element: HTMLLIElement; span: HTMLSpanElement; text: string; -} +}; -interface Category { +type Category = { items: Item[]; element: HTMLLIElement; -} +}; export class CategorizedItemList { - #container: HTMLElement; - #elementList: HTMLUListElement; - #input: HTMLInputElement; + readonly #container: HTMLElement; + readonly #elementList: HTMLUListElement; + readonly #input: HTMLInputElement; #value: string = ""; - #clearButton: HTMLButtonElement; + readonly #clearButton: HTMLButtonElement; #categories: Category[] = []; - #fragment: DocumentFragment; + readonly #fragment: DocumentFragment; constructor(elementId: string) { this.#fragment = document.createDocumentFragment(); @@ -72,8 +72,8 @@ export class CategorizedItemList { category.items.push({ element: li, - span: li.querySelector("span")!, - text: li.innerText.trim(), + span: li.querySelector("span")!, + text: li.textContent!.trim(), }); } else { const items: Item[] = []; @@ -96,7 +96,7 @@ export class CategorizedItemList { const isOpen = !this.#categoryIsOpen(li); li.dataset.open = isOpen ? "true" : "false"; - li.querySelector("fa-icon")!.setIcon(isOpen ? "chevron-down" : "chevron-right"); + li.querySelector("fa-icon")!.setIcon(isOpen ? "chevron-down" : "chevron-right"); this.#showItems({ items: items, diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js index b198ddea7f..40236cd405 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js @@ -52,7 +52,7 @@ define(["require", "exports", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/L category.items.push({ element: li, span: li.querySelector("span"), - text: li.innerText.trim(), + text: li.textContent.trim(), }); } else { diff --git a/wcfsetup/install/files/lib/action/ConditionAddAction.class.php b/wcfsetup/install/files/lib/action/ConditionAddAction.class.php index 4e3da4d954..193571b434 100644 --- a/wcfsetup/install/files/lib/action/ConditionAddAction.class.php +++ b/wcfsetup/install/files/lib/action/ConditionAddAction.class.php @@ -114,18 +114,18 @@ private function getOptions(AbstractConditionProvider $provider): array if (!isset($grouped[$category])) { $grouped[$category] = [ - "items" => [], - "label" => WCF::getLanguage()->get('wcf.condition.category.' . $category), + 'items' => [], + 'label' => WCF::getLanguage()->get('wcf.condition.category.' . $category), ]; } - $grouped[$category]["items"][$key] = WCF::getLanguage()->get($label); + $grouped[$category]['items'][$key] = WCF::getLanguage()->get($label); } $collator = new \Collator(WCF::getLanguage()->getLocale()); foreach ($grouped as &$category) { - \uasort($category["items"], static function ($labelA, $labelB) use ($collator) { + \uasort($category['items'], static function ($labelA, $labelB) use ($collator) { return $collator->compare($labelA, $labelB); }); } @@ -141,11 +141,11 @@ private function getOptions(AbstractConditionProvider $provider): array $options[] = [ 'depth' => 0, 'isSelectable' => false, - 'label' => $category["label"], + 'label' => $category['label'], 'value' => $categoryKey, ]; - foreach ($category["items"] as $key => $label) { + foreach ($category['items'] as $key => $label) { $options[] = [ 'depth' => 1, 'isSelectable' => true, From 96f5f07c2cfe90cf635694189d95a7c27f42daa8 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Tue, 22 Jul 2025 18:01:02 +0200 Subject: [PATCH 3/4] Remove the collapse feature, improve visuals of categories --- ...ed_categorizedSingleSelectionFormField.tpl | 11 ++++++---- .../Core/Component/ItemList/Categorized.ts | 18 --------------- .../Core/Component/ItemList/Categorized.js | 13 ----------- .../style/ui/scrollableCheckboxList.scss | 22 +++++++++++++++++++ 4 files changed, 29 insertions(+), 35 deletions(-) diff --git a/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl b/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl index c29c828893..4e204d2fb5 100644 --- a/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl +++ b/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl @@ -19,8 +19,13 @@
    {foreach from=$field->getNestedOptions() item=__fieldNestedOption} - 0} style="padding-left: {$__fieldNestedOption[depth]*20}px"{/if}{if !$__fieldNestedOption[isSelectable]} data-open="true"{/if}> - {if $__fieldNestedOption[isSelectable]} +
  • 0} style="padding-left: {$__fieldNestedOption[depth]*20}px"{/if} + {if !$__fieldNestedOption[isSelectable]} class="scrollableCheckboxList__category"{/if} + > + {if !$__fieldNestedOption[isSelectable]} + {unsafe:$__fieldNestedOption[label]} + {else} - {else} - {/if}
  • {/foreach} diff --git a/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts b/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts index b8c9b95a6f..8946e4997a 100644 --- a/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts +++ b/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts @@ -82,28 +82,10 @@ export class CategorizedItemList { element: li, }; this.#categories.push(category); - - li.addEventListener("click", (event) => { - this.#categoryClick(event, li, items); - }); } } } - #categoryClick(event: MouseEvent, li: HTMLLIElement, items: Item[]): void { - event.preventDefault(); - - const isOpen = !this.#categoryIsOpen(li); - li.dataset.open = isOpen ? "true" : "false"; - - li.querySelector("fa-icon")!.setIcon(isOpen ? "chevron-down" : "chevron-right"); - - this.#showItems({ - items: items, - element: li, - }); - } - #categoryIsOpen(category: HTMLLIElement): boolean { return category.dataset.open === "true"; } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js index 40236cd405..3ae574d3f5 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js @@ -62,22 +62,9 @@ define(["require", "exports", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/L element: li, }; this.#categories.push(category); - li.addEventListener("click", (event) => { - this.#categoryClick(event, li, items); - }); } } } - #categoryClick(event, li, items) { - event.preventDefault(); - const isOpen = !this.#categoryIsOpen(li); - li.dataset.open = isOpen ? "true" : "false"; - li.querySelector("fa-icon").setIcon(isOpen ? "chevron-down" : "chevron-right"); - this.#showItems({ - items: items, - element: li, - }); - } #categoryIsOpen(category) { return category.dataset.open === "true"; } diff --git a/wcfsetup/install/files/style/ui/scrollableCheckboxList.scss b/wcfsetup/install/files/style/ui/scrollableCheckboxList.scss index 2ef8fb1420..02feb3d11d 100644 --- a/wcfsetup/install/files/style/ui/scrollableCheckboxList.scss +++ b/wcfsetup/install/files/style/ui/scrollableCheckboxList.scss @@ -14,6 +14,28 @@ } } +.scrollableCheckboxList__category:not(:first-child) { + margin-top: 5px; +} + +.scrollableCheckboxList__category__label { + align-items: center; + color: var(--wcfContentDimmedText); + column-gap: 10px; + display: flex; + font-size: 12px; + margin-bottom: 5px; + white-space: nowrap; + + &::after { + border-top: 1px solid currentColor; + content: ""; + display: block; + width: 100%; + opacity: 0.34; + } +} + .dialogContent .scrollableCheckboxList { max-height: 300px; } From 8dd6df7b537463fba6c528bf48d0170430883a76 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Tue, 22 Jul 2025 19:31:27 +0200 Subject: [PATCH 4/4] Simplify the filtering, use native `hidden` property instead --- .../Core/Component/ItemList/Categorized.ts | 58 ++++++------------- .../Core/Component/ItemList/Categorized.js | 53 +++++------------ 2 files changed, 33 insertions(+), 78 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts b/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts index 8946e4997a..f0eecbafa1 100644 --- a/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts +++ b/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts @@ -7,7 +7,7 @@ * @sice 6.3 */ -import { innerError, show, hide, isHidden } from "WoltLabSuite/Core/Dom/Util"; +import { innerError } from "WoltLabSuite/Core/Dom/Util"; import { getPhrase } from "WoltLabSuite/Core/Language"; import { escapeRegExp } from "WoltLabSuite/Core/StringUtil"; @@ -86,10 +86,6 @@ export class CategorizedItemList { } } - #categoryIsOpen(category: HTMLLIElement): boolean { - return category.dataset.open === "true"; - } - #keyup(): void { const value = this.#input.value.trim(); if (this.#value === value) { @@ -109,56 +105,36 @@ export class CategorizedItemList { this.#fragment.appendChild(this.#elementList); this.#categories.forEach((category) => { - this.#showItems(category); + this.#filterItems(category); }); - const hasVisibleItems = Array.from( - this.#elementList.querySelectorAll(".scrollableCheckboxList > li"), - ).some((li) => { - return !isHidden(li); - }); + const hasVisibleItem = this.#elementList.querySelector(".scrollableCheckboxList > li:not([hidden])") !== null; this.#container.insertAdjacentElement("beforeend", this.#elementList); - innerError(this.#container, hasVisibleItems ? false : getPhrase("wcf.global.filter.error.noMatches")); + innerError(this.#container, hasVisibleItem ? false : getPhrase("wcf.global.filter.error.noMatches")); } - #showItems(category: Category): void { - const categoryIsOpen = this.#categoryIsOpen(category.element); + #filterItems(category: Category): void { const regexp = new RegExp("(" + escapeRegExp(this.#value) + ")", "i"); - if (this.#value === "") { - show(category.element); - - category.items.forEach((item) => { + let hasMatchingItem = false; + for (const item of category.items) { + if (this.#value === "") { item.span.innerHTML = item.text; // Reset highlighting - if (categoryIsOpen) { - show(item.element); - } else { - hide(item.element); - } - }); - } else { - if (category.items.some((item) => regexp.test(item.text))) { - show(category.element); + hasMatchingItem = true; + item.element.hidden = false; + } else if (regexp.test(item.text)) { + item.span.innerHTML = item.text.replace(regexp, "$1"); - category.items.forEach((item) => { - if (categoryIsOpen && regexp.test(item.text)) { - item.span.innerHTML = item.text.replace(regexp, "$1"); - - show(item.element); - } else { - hide(item.element); - } - }); + item.element.hidden = false; + hasMatchingItem = true; } else { - hide(category.element); - - category.items.forEach((item) => { - hide(item.element); - }); + item.element.hidden = true; } } + + category.element.hidden = !hasMatchingItem; } } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js index 3ae574d3f5..92bd82ae7d 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js @@ -65,9 +65,6 @@ define(["require", "exports", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/L } } } - #categoryIsOpen(category) { - return category.dataset.open === "true"; - } #keyup() { const value = this.#input.value.trim(); if (this.#value === value) { @@ -84,49 +81,31 @@ define(["require", "exports", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/L // by avoiding the browser to perform repaint/layout over and over again this.#fragment.appendChild(this.#elementList); this.#categories.forEach((category) => { - this.#showItems(category); - }); - const hasVisibleItems = Array.from(this.#elementList.querySelectorAll(".scrollableCheckboxList > li")).some((li) => { - return !(0, Util_1.isHidden)(li); + this.#filterItems(category); }); + const hasVisibleItem = this.#elementList.querySelector(".scrollableCheckboxList > li:not([hidden])") !== null; this.#container.insertAdjacentElement("beforeend", this.#elementList); - (0, Util_1.innerError)(this.#container, hasVisibleItems ? false : (0, Language_1.getPhrase)("wcf.global.filter.error.noMatches")); + (0, Util_1.innerError)(this.#container, hasVisibleItem ? false : (0, Language_1.getPhrase)("wcf.global.filter.error.noMatches")); } - #showItems(category) { - const categoryIsOpen = this.#categoryIsOpen(category.element); + #filterItems(category) { const regexp = new RegExp("(" + (0, StringUtil_1.escapeRegExp)(this.#value) + ")", "i"); - if (this.#value === "") { - (0, Util_1.show)(category.element); - category.items.forEach((item) => { + let hasMatchingItem = false; + for (const item of category.items) { + if (this.#value === "") { item.span.innerHTML = item.text; // Reset highlighting - if (categoryIsOpen) { - (0, Util_1.show)(item.element); - } - else { - (0, Util_1.hide)(item.element); - } - }); - } - else { - if (category.items.some((item) => regexp.test(item.text))) { - (0, Util_1.show)(category.element); - category.items.forEach((item) => { - if (categoryIsOpen && regexp.test(item.text)) { - item.span.innerHTML = item.text.replace(regexp, "$1"); - (0, Util_1.show)(item.element); - } - else { - (0, Util_1.hide)(item.element); - } - }); + hasMatchingItem = true; + item.element.hidden = false; + } + else if (regexp.test(item.text)) { + item.span.innerHTML = item.text.replace(regexp, "$1"); + item.element.hidden = false; + hasMatchingItem = true; } else { - (0, Util_1.hide)(category.element); - category.items.forEach((item) => { - (0, Util_1.hide)(item.element); - }); + item.element.hidden = true; } } + category.element.hidden = !hasMatchingItem; } } exports.CategorizedItemList = CategorizedItemList;