Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions com.woltlab.wcf/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,6 @@
Required order of the following steps for the update to 6.3:
<instruction type="database" run="standalone">acp/database/update_com.woltlab.wcf_6.3_step1.php</instruction>
<instruction type="script">acp/update_com.woltlab.wcf_6.3_userGroupAssignment.php</instruction>
<instruction type="script">acp/update_com.woltlab.wcf_6.3_notice.php</instruction>
-->
</package>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script data-relocate="true">
{jsphrase name='wcf.global.filter.button.visibility'}
{jsphrase name='wcf.global.filter.button.clear'}
{jsphrase name='wcf.global.filter.error.noMatches'}
{jsphrase name='wcf.global.filter.placeholder'}
{jsphrase name='wcf.global.filter.visibility.activeOnly'}
{jsphrase name='wcf.global.filter.visibility.highlightActive'}
{jsphrase name='wcf.global.filter.visibility.showAll'}

require(['WoltLabSuite/Core/Component/ItemList/Categorized'], ({ CategorizedItemList }) => {
new CategorizedItemList('{unsafe:$field->getPrefixedId()|encodeJS}_list');
});
</script>

<div class="itemListFilter" id="{$field->getPrefixedId()}_list">
<div class="inputAddon">
<input type="text" class="long" placeholder="{lang}wcf.global.filter.placeholder{/lang}">
<button type="button" class="button clearButton inputSuffix disabled jsTooltip" title="{lang}wcf.global.filter.button.clear{/lang}">{icon name="xmark" solid=true}</button>
</div>
<ul class="scrollableCheckboxList">
{foreach from=$field->getNestedOptions() item=__fieldNestedOption}
<li
{if $__fieldNestedOption[depth] > 0} style="padding-left: {$__fieldNestedOption[depth]*20}px"{/if}
{if !$__fieldNestedOption[isSelectable]} class="scrollableCheckboxList__category"{/if}
>
{if !$__fieldNestedOption[isSelectable]}
<span class="scrollableCheckboxList__category__label">{unsafe:$__fieldNestedOption[label]}</span>
{else}
<label>
<input {*
*}type="radio" {*
*}name="{$field->getPrefixedId()}" {*
*}value="{$__fieldNestedOption[value]}"{*
*}{if !$field->getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if}{*
*}{if $field->getValue() == $__fieldNestedOption[value] && $__fieldNestedOption[isSelectable]} checked{/if}{*
*}{if $field->isImmutable()} disabled{/if}{*
*}> <span>{unsafe:$__fieldNestedOption[label]}</span>
</label>
{/if}
</li>
{/foreach}
</ul>
</div>
30 changes: 30 additions & 0 deletions com.woltlab.wcf/templates/shared_cssClassnameFormField.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<ul class="classNameSelection{if !$field->getClasses()|empty} {implode from=$field->getClasses() item=class glue=' '}{$class}{/implode}{/if}">
{foreach from=$field->getOptions() key=className item=label}
<li{if $className == 'custom'} class="custom"{/if}>
<label class="classNameSelection__label">
<input {*
*}type="radio" {*
*}name="{$field->getPrefixedId()}" {*
*}value="{$className}"{*
*}{if !$field->getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item=class glue=' '}{$class}{/implode}"{/if}{*
*}{if $field->getValue() === $className || ($className === 'custom' && !$field->getCustomClassName()|empty)} checked{/if}{*
*}{if $field->isImmutable()} disabled{/if}{*
*}{foreach from=$field->getFieldAttributes() key=attributeName item=attributeValue} {$attributeName}="{$attributeValue}"{/foreach}{*
*}>
{if $className == 'custom'}
<span class="classNameSelection__span">
<input type="text" id="{$field->getPrefixedId()}Custom" {*
*}name="{$field->getPrefixedId()}customCssClassName" {*
*}value="{$field->getCustomClassName()}" {*
*}{if $field->isImmutable()} disabled{/if}{*
*}class="long classNameSelection__custom__input" {*
*}pattern="{$field->getPattern()}"{*
*}>
</span>
{else}
{unsafe:$field->renderVisualTemplate($className, $label)}
{/if}
</label>
</li>
{/foreach}
</ul>
11 changes: 11 additions & 0 deletions com.woltlab.wcf/templates/shared_timeFormField.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<input {*
*}type="datetime" data-ignore-timezone="1" data-time-only="1" {*
*}id="{$field->getPrefixedId()}" {*
*}name="{$field->getPrefixedId()}" {*
*}value="{$field->getValue()}"{*
*}{if !$field->getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if}{*
*}{if $field->isAutofocused()} autofocus{/if}{*
*}{if $field->isRequired()} required{/if}{*
*}{if $field->isImmutable()} disabled{/if}{*
*}{foreach from=$field->getFieldAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{*
*}>
140 changes: 140 additions & 0 deletions ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* Provides a filter input for a categorized item list.
*
* @author Olaf Braun
* @copyright 2001-2025 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @sice 6.3
*/

import { innerError } from "WoltLabSuite/Core/Dom/Util";
import { getPhrase } from "WoltLabSuite/Core/Language";
import { escapeRegExp } from "WoltLabSuite/Core/StringUtil";

type Item = {
element: HTMLLIElement;
span: HTMLSpanElement;
text: string;
};

type Category = {
items: Item[];
element: HTMLLIElement;
};

export class CategorizedItemList {
readonly #container: HTMLElement;
readonly #elementList: HTMLUListElement;
readonly #input: HTMLInputElement;
#value: string = "";
readonly #clearButton: HTMLButtonElement;
#categories: Category[] = [];
readonly #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<HTMLUListElement>(".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<HTMLButtonElement>(".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<HTMLLIElement>(":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.textContent!.trim(),
});
} else {
const items: Item[] = [];
category = {
items: items,
element: li,
};
this.#categories.push(category);
}
}
}

#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.#filterItems(category);
});

const hasVisibleItem = this.#elementList.querySelector(".scrollableCheckboxList > li:not([hidden])") !== null;

this.#container.insertAdjacentElement("beforeend", this.#elementList);

innerError(this.#container, hasVisibleItem ? false : getPhrase("wcf.global.filter.error.noMatches"));
}

#filterItems(category: Category): void {
const regexp = new RegExp("(" + escapeRegExp(this.#value) + ")", "i");

let hasMatchingItem = false;
for (const item of category.items) {
if (this.#value === "") {
item.span.innerHTML = item.text; // Reset highlighting

hasMatchingItem = true;
item.element.hidden = false;
} else if (regexp.test(item.text)) {
item.span.innerHTML = item.text.replace(regexp, "<u>$1</u>");

item.element.hidden = false;
hasMatchingItem = true;
} else {
item.element.hidden = true;
}
}

category.element.hidden = !hasMatchingItem;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@
MediumtextDatabaseTableColumn::create('conditions'),
DefaultFalseBooleanDatabaseTableColumn::create('isLegacy'),
]),
PartialDatabaseTable::create('wcf1_notice')
->columns([
MediumtextDatabaseTableColumn::create('conditions'),
DefaultFalseBooleanDatabaseTableColumn::create('isLegacy'),
]),
];
Loading