--- /dev/null
+<ul class="scrollableCheckboxList" {*
+ *}id="lineBreakSeparatedTextOption_{$field->getPrefixedId()}"{*
+ *}{if $field->getValue()|empty} style="display: none"{/if}{*
+*}>
+ {foreach from=$field->getValue() item=value}
+ <li data-value="{$value}">
+ <button class="jsDeleteItem jsTooltip" type="button" title="{lang}wcf.global.button.delete{/lang}">
+ {icon name='trash'}
+ </button>
+ <button class="jsEditItem jsTooltip" type="button" title="{lang}wcf.global.button.edit{/lang}">
+ {icon name='edit'}
+ </button>
+ <span>{$value}</span>
+ </li>
+ {/foreach}
+</ul>
+
+<script data-relocate="true">
+ require(["WoltLabSuite/Core/Form/Builder/Field/Controller/MultilineItemList"], ({ MultilineItemList }) => {
+ {jsphrase name='wcf.acp.option.type.lineBreakSeparatedText.placeholder'}
+ {jsphrase name='wcf.acp.option.type.lineBreakSeparatedText.clearList.confirmMessage'}
+ {jsphrase name='wcf.global.button.save'}
+ {jsphrase name='wcf.global.button.cancel'}
+ {jsphrase name='wcf.global.button.edit'}
+ WoltLabLanguage.registerPhrase("wcf.acp.option.type.lineBreakSeparatedText.error.duplicate", '{jslang __literal=true}wcf.acp.option.type.lineBreakSeparatedText.error.duplicate{/jslang}');
+
+ new MultilineItemList(document.getElementById('lineBreakSeparatedTextOption_{@$field->getPrefixedId()|encodeJS}'), {
+ submitFieldName: '{@$field->getPrefixedId()|encodeJS}[]',
+ });
+ });
+ {if $field->isFilterable()}
+ require(["WoltLabSuite/Core/Ui/ItemList/Filter"], (UiItemListFilter) => {
+ {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'}
+
+ new UiItemListFilter('lineBreakSeparatedTextOption_{@$field->getPrefixedId()|encodeJS}');
+ });
+ {/if}
+</script>
"wcfsetup/install/files/acp/templates"
],
"templates": [
+ "__multilineItemListFormField",
"__aclFormField",
"__booleanFormField",
"__buttonFormField",
--- /dev/null
+/**
+ * Handles the JavaScript part of a multiline item list form field.
+ *
+ * @author Olaf Braun
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.0
+ */
+
+import * as UiItemList from "WoltLabSuite/Core/Ui/ItemList/LineBreakSeparatedText";
+import { getPhrase } from "WoltLabSuite/Core/Language";
+import DomUtil from "WoltLabSuite/Core/Dom/Util";
+
+const _data = new Map<string, MultilineItemListFormField>();
+
+export class MultilineItemListFormField extends UiItemList.UiItemListLineBreakSeparatedText {
+ /**
+ @inheritDoc
+ */
+ constructor(itemList: HTMLUListElement, options: UiItemList.LineBreakSeparatedTextOptions) {
+ super(itemList, options);
+ if (options.submitFieldName != null) {
+ _data.set(options.submitFieldName, this);
+ } else {
+ _data.set(this.submitField!.id, this);
+ }
+ }
+
+ public getItems(): Set<string> {
+ return this.items;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected initValues() {
+ super.initValues();
+ Array.from(this.itemList.children).forEach((el: HTMLElement) => {
+ el.querySelector(".jsEditItem")!.addEventListener("click", (ev) => {
+ this.editItem(ev);
+ });
+ });
+ }
+
+ /**
+ * Begin the editing of an item.
+ */
+ protected editItem(event: Event): void {
+ if (this.uiDisabled) {
+ return;
+ }
+ const button = event.currentTarget as HTMLElement;
+ const li = button.closest("li")!;
+ const deleteButton = li.querySelector<HTMLButtonElement>(".jsDeleteItem")!;
+ const valueSpan = li.querySelector<HTMLSpanElement>("span")!;
+ const value = button.closest("li")!.dataset.value!;
+ //hide old buttons
+ DomUtil.hide(deleteButton);
+ DomUtil.hide(button);
+ DomUtil.hide(valueSpan);
+ //insert temporary input field and buttons
+ const input = document.createElement("input");
+ input.type = "text";
+ input.value = value;
+ const saveButton = document.createElement("button");
+ saveButton.classList.add("jsSaveItem", "jsTooltip");
+ saveButton.title = getPhrase("wcf.global.button.save");
+ let icon = document.createElement("fa-icon");
+ icon.setIcon("save");
+ saveButton.append(icon);
+
+ const cancelButton = document.createElement("button");
+ cancelButton.classList.add("jsCancelItem", "jsTooltip");
+ cancelButton.title = getPhrase("wcf.global.button.cancel");
+ icon = document.createElement("fa-icon");
+ icon.setIcon("times");
+ cancelButton.append(icon);
+
+ const endCallback = () => {
+ //remove temporary elements
+ input.remove();
+ saveButton.remove();
+ cancelButton.remove();
+ //display old elements
+ DomUtil.show(deleteButton);
+ DomUtil.show(button);
+ DomUtil.show(valueSpan);
+ };
+
+ const saveCallback = (saveEvent: Event) => {
+ saveEvent.preventDefault();
+
+ if (this.uiDisabled) {
+ return;
+ }
+ const newValue = input.value.trim();
+
+ if (newValue === "") {
+ DomUtil.innerError(input.parentElement!, getPhrase("wcf.global.form.error.empty"));
+ } else if (!this.items.has(newValue) || newValue === value) {
+ //remove error message
+ DomUtil.innerError(input.parentElement!, "");
+
+ //insert new value
+ button.closest("li")!.dataset.value = newValue;
+ valueSpan.textContent = newValue;
+ this.items.delete(value);
+ this.items.add(newValue);
+
+ endCallback();
+ } else {
+ DomUtil.innerError(
+ input.parentElement!,
+ getPhrase("wcf.acp.option.type.lineBreakSeparatedText.error.duplicate", {
+ item: newValue,
+ }),
+ true,
+ );
+ }
+
+ input.focus();
+ };
+
+ input.addEventListener("keydown", (event) => {
+ if (event.key === "Enter") {
+ saveCallback(event);
+ }
+ });
+
+ saveButton.addEventListener("click", (ev) => {
+ saveCallback(ev);
+ });
+
+ cancelButton.addEventListener("click", () => {
+ endCallback();
+ });
+
+ li.append(cancelButton);
+ li.append(saveButton);
+ li.append(input);
+ input.focus();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected insertItem(item: string): void {
+ this.items.add(item);
+
+ const itemElement = document.createElement("li");
+ itemElement.dataset.value = item;
+
+ const deleteButton = document.createElement("button");
+ deleteButton.type = "button";
+ deleteButton.classList.add("jsDeleteItem", "jsTooltip");
+ deleteButton.title = getPhrase("wcf.global.button.delete");
+ deleteButton.addEventListener("click", (ev) => {
+ this.deleteItem(ev);
+ });
+ let icon = document.createElement("fa-icon");
+ icon.setIcon("trash");
+ deleteButton.append(icon);
+ itemElement.append(deleteButton);
+
+ const editButton = document.createElement("button");
+ editButton.type = "button";
+ editButton.classList.add("jsEditItem", "jsTooltip");
+ editButton.title = getPhrase("wcf.global.button.edit");
+ editButton.addEventListener("click", (ev) => {
+ this.editItem(ev);
+ });
+ icon = document.createElement("fa-icon");
+ icon.setIcon("edit");
+ editButton.append(icon);
+ itemElement.append(editButton);
+
+ itemElement.append(" ");
+
+ const label = document.createElement("span");
+ label.innerText = item;
+ itemElement.append(label);
+
+ const nextElement = Array.from(this.itemList.children).find((el: HTMLElement) => el.dataset.value! > item);
+
+ if (nextElement) {
+ this.itemList.insertBefore(itemElement, nextElement);
+ } else {
+ this.itemList.append(itemElement);
+ }
+
+ this.showList();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected submit(): void {
+ if (this.submitField) {
+ this.submitField.value = Array.from(this.items).join("\n");
+ } else {
+ Array.from(this.items).forEach((value) => {
+ const input = document.createElement("input");
+ input.type = "hidden";
+ input.name = this.options.submitFieldName!;
+ input.value = value;
+ this.itemList.parentElement!.append(input);
+ });
+ }
+ }
+}
+
+export function getValues(elementId: string): Set<string> {
+ if (!_data.has(elementId)) {
+ throw new Error(`Element id '${elementId}' is unknown.`);
+ }
+
+ return _data.get(elementId)!.getItems();
+}
--- /dev/null
+/**
+ * Data handler for an multiline item list form builder field in an Ajax form.
+ *
+ * @author Olaf Braun
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.0
+ */
+
+import Field from "WoltLabSuite/Core/Form/Builder/Field/Field";
+import { FormBuilderData } from "WoltLabSuite/Core/Form/Builder/Data";
+import { getValues } from "./Controller/MultilineItemList";
+
+class MultilineItemList extends Field {
+ protected _getData(): FormBuilderData {
+ return {
+ [this._fieldId]: getValues(this._fieldId),
+ };
+ }
+}
+
+export = MultilineItemList;
--- /dev/null
+<ul class="scrollableCheckboxList" {*
+ *}id="lineBreakSeparatedTextOption_{$field->getPrefixedId()}"{*
+ *}{if $field->getValue()|empty} style="display: none"{/if}{*
+*}>
+ {foreach from=$field->getValue() item=value}
+ <li data-value="{$value}">
+ <button class="jsDeleteItem jsTooltip" type="button" title="{lang}wcf.global.button.delete{/lang}">
+ {icon name='trash'}
+ </button>
+ <button class="jsEditItem jsTooltip" type="button" title="{lang}wcf.global.button.edit{/lang}">
+ {icon name='edit'}
+ </button>
+ <span>{$value}</span>
+ </li>
+ {/foreach}
+</ul>
+
+<script data-relocate="true">
+ require(["WoltLabSuite/Core/Form/Builder/Field/Controller/MultilineItemList"], ({ MultilineItemList }) => {
+ {jsphrase name='wcf.acp.option.type.lineBreakSeparatedText.placeholder'}
+ {jsphrase name='wcf.acp.option.type.lineBreakSeparatedText.clearList.confirmMessage'}
+ {jsphrase name='wcf.global.button.save'}
+ {jsphrase name='wcf.global.button.cancel'}
+ {jsphrase name='wcf.global.button.edit'}
+ WoltLabLanguage.registerPhrase("wcf.acp.option.type.lineBreakSeparatedText.error.duplicate", '{jslang __literal=true}wcf.acp.option.type.lineBreakSeparatedText.error.duplicate{/jslang}');
+
+ new MultilineItemList(document.getElementById('lineBreakSeparatedTextOption_{@$field->getPrefixedId()|encodeJS}'), {
+ submitFieldName: '{@$field->getPrefixedId()|encodeJS}[]',
+ });
+ });
+ {if $field->isFilterable()}
+ require(["WoltLabSuite/Core/Ui/ItemList/Filter"], (UiItemListFilter) => {
+ {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'}
+
+ new UiItemListFilter('lineBreakSeparatedTextOption_{@$field->getPrefixedId()|encodeJS}');
+ });
+ {/if}
+</script>
--- /dev/null
+/**
+ * Handles the JavaScript part of a multiline item list form field.
+ *
+ * @author Olaf Braun
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.0
+ */
+define(["require", "exports", "tslib", "WoltLabSuite/Core/Ui/ItemList/LineBreakSeparatedText", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Dom/Util"], function (require, exports, tslib_1, UiItemList, Language_1, Util_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.getValues = exports.MultilineItemListFormField = void 0;
+ UiItemList = tslib_1.__importStar(UiItemList);
+ Util_1 = tslib_1.__importDefault(Util_1);
+ const _data = new Map();
+ class MultilineItemListFormField extends UiItemList.UiItemListLineBreakSeparatedText {
+ /**
+ @inheritDoc
+ */
+ constructor(itemList, options) {
+ super(itemList, options);
+ if (options.submitFieldName != null) {
+ _data.set(options.submitFieldName, this);
+ }
+ else {
+ _data.set(this.submitField.id, this);
+ }
+ }
+ getItems() {
+ return this.items;
+ }
+ /**
+ * @inheritDoc
+ */
+ initValues() {
+ super.initValues();
+ Array.from(this.itemList.children).forEach((el) => {
+ el.querySelector(".jsEditItem").addEventListener("click", (ev) => {
+ this.editItem(ev);
+ });
+ });
+ }
+ /**
+ * Begin the editing of an item.
+ */
+ editItem(event) {
+ if (this.uiDisabled) {
+ return;
+ }
+ const button = event.currentTarget;
+ const li = button.closest("li");
+ const deleteButton = li.querySelector(".jsDeleteItem");
+ const valueSpan = li.querySelector("span");
+ const value = button.closest("li").dataset.value;
+ //hide old buttons
+ Util_1.default.hide(deleteButton);
+ Util_1.default.hide(button);
+ Util_1.default.hide(valueSpan);
+ //insert temporary input field and buttons
+ const input = document.createElement("input");
+ input.type = "text";
+ input.value = value;
+ const saveButton = document.createElement("button");
+ saveButton.classList.add("jsSaveItem", "jsTooltip");
+ saveButton.title = (0, Language_1.getPhrase)("wcf.global.button.save");
+ let icon = document.createElement("fa-icon");
+ icon.setIcon("save");
+ saveButton.append(icon);
+ const cancelButton = document.createElement("button");
+ cancelButton.classList.add("jsCancelItem", "jsTooltip");
+ cancelButton.title = (0, Language_1.getPhrase)("wcf.global.button.cancel");
+ icon = document.createElement("fa-icon");
+ icon.setIcon("times");
+ cancelButton.append(icon);
+ const endCallback = () => {
+ //remove temporary elements
+ input.remove();
+ saveButton.remove();
+ cancelButton.remove();
+ //display old elements
+ Util_1.default.show(deleteButton);
+ Util_1.default.show(button);
+ Util_1.default.show(valueSpan);
+ };
+ const saveCallback = (saveEvent) => {
+ saveEvent.preventDefault();
+ if (this.uiDisabled) {
+ return;
+ }
+ const newValue = input.value.trim();
+ if (newValue === "") {
+ Util_1.default.innerError(input.parentElement, (0, Language_1.getPhrase)("wcf.global.form.error.empty"));
+ }
+ else if (!this.items.has(newValue) || newValue === value) {
+ //remove error message
+ Util_1.default.innerError(input.parentElement, "");
+ //insert new value
+ button.closest("li").dataset.value = newValue;
+ valueSpan.textContent = newValue;
+ this.items.delete(value);
+ this.items.add(newValue);
+ endCallback();
+ }
+ else {
+ Util_1.default.innerError(input.parentElement, (0, Language_1.getPhrase)("wcf.acp.option.type.lineBreakSeparatedText.error.duplicate", {
+ item: newValue,
+ }), true);
+ }
+ input.focus();
+ };
+ input.addEventListener("keydown", (event) => {
+ if (event.key === "Enter") {
+ saveCallback(event);
+ }
+ });
+ saveButton.addEventListener("click", (ev) => {
+ saveCallback(ev);
+ });
+ cancelButton.addEventListener("click", () => {
+ endCallback();
+ });
+ li.append(cancelButton);
+ li.append(saveButton);
+ li.append(input);
+ input.focus();
+ }
+ /**
+ * @inheritDoc
+ */
+ insertItem(item) {
+ this.items.add(item);
+ const itemElement = document.createElement("li");
+ itemElement.dataset.value = item;
+ const deleteButton = document.createElement("button");
+ deleteButton.type = "button";
+ deleteButton.classList.add("jsDeleteItem", "jsTooltip");
+ deleteButton.title = (0, Language_1.getPhrase)("wcf.global.button.delete");
+ deleteButton.addEventListener("click", (ev) => {
+ this.deleteItem(ev);
+ });
+ let icon = document.createElement("fa-icon");
+ icon.setIcon("trash");
+ deleteButton.append(icon);
+ itemElement.append(deleteButton);
+ const editButton = document.createElement("button");
+ editButton.type = "button";
+ editButton.classList.add("jsEditItem", "jsTooltip");
+ editButton.title = (0, Language_1.getPhrase)("wcf.global.button.edit");
+ editButton.addEventListener("click", (ev) => {
+ this.editItem(ev);
+ });
+ icon = document.createElement("fa-icon");
+ icon.setIcon("edit");
+ editButton.append(icon);
+ itemElement.append(editButton);
+ itemElement.append(" ");
+ const label = document.createElement("span");
+ label.innerText = item;
+ itemElement.append(label);
+ const nextElement = Array.from(this.itemList.children).find((el) => el.dataset.value > item);
+ if (nextElement) {
+ this.itemList.insertBefore(itemElement, nextElement);
+ }
+ else {
+ this.itemList.append(itemElement);
+ }
+ this.showList();
+ }
+ /**
+ * @inheritDoc
+ */
+ submit() {
+ if (this.submitField) {
+ this.submitField.value = Array.from(this.items).join("\n");
+ }
+ else {
+ Array.from(this.items).forEach((value) => {
+ const input = document.createElement("input");
+ input.type = "hidden";
+ input.name = this.options.submitFieldName;
+ input.value = value;
+ this.itemList.parentElement.append(input);
+ });
+ }
+ }
+ }
+ exports.MultilineItemListFormField = MultilineItemListFormField;
+ function getValues(elementId) {
+ if (!_data.has(elementId)) {
+ throw new Error(`Element id '${elementId}' is unknown.`);
+ }
+ return _data.get(elementId).getItems();
+ }
+ exports.getValues = getValues;
+});
--- /dev/null
+/**
+ * Data handler for an multiline item list form builder field in an Ajax form.
+ *
+ * @author Olaf Braun
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.0
+ */
+define(["require", "exports", "tslib", "WoltLabSuite/Core/Form/Builder/Field/Field", "./Controller/MultilineItemList"], function (require, exports, tslib_1, Field_1, MultilineItemList_1) {
+ "use strict";
+ Field_1 = tslib_1.__importDefault(Field_1);
+ class MultilineItemList extends Field_1.default {
+ _getData() {
+ return {
+ [this._fieldId]: (0, MultilineItemList_1.getValues)(this._fieldId),
+ };
+ }
+ }
+ return MultilineItemList;
+});
--- /dev/null
+<?php
+
+namespace wcf\system\form\builder\field;
+
+/**
+ * Implementation of a form field that allows entering a list of items.
+ *
+ * @author Olaf Braun
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.0
+ */
+class MultilineItemListFormField extends ItemListFormField implements INullableFormField
+{
+ use TNullableFormField;
+
+ /**
+ * @inheritDoc
+ */
+ protected $templateName = '__multilineItemListFormField';
+
+ /**
+ * @inheritDoc
+ */
+ protected $javaScriptDataHandlerModule = 'WoltLabSuite/Core/Form/Builder/Field/MultilineItemList';
+
+ /**
+ * @see TFilterableSelectionFormField::$filterable
+ */
+ protected bool $filterable = false;
+
+ /**
+ * @see TFilterableSelectionFormField::filterable()
+ */
+ public function filterable(bool $filterable = true): self
+ {
+ $this->filterable = $filterable;
+
+ return $this;
+ }
+
+ /**
+ * @see TFilterableSelectionFormField::isFilterable()
+ */
+ public function isFilterable(): bool
+ {
+ return $this->filterable;
+ }
+}