From dd0021ce0ac4c787549f25fd8344cefc4b89c089 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 25 Apr 2024 11:24:25 +0200 Subject: [PATCH] Add new form field `MultilineItemListFormField` --- .../__multilineItemListFormField.tpl | 44 ++++ syncTemplates.json | 1 + .../Field/Controller/MultilineItemList.ts | 218 ++++++++++++++++++ .../Form/Builder/Field/MultilineItemList.ts | 22 ++ .../__multilineItemListFormField.tpl | 44 ++++ .../Field/Controller/MultilineItemList.js | 195 ++++++++++++++++ .../Form/Builder/Field/MultilineItemList.js | 20 ++ .../MultilineItemListFormField.class.php | 49 ++++ 8 files changed, 593 insertions(+) create mode 100644 com.woltlab.wcf/templates/__multilineItemListFormField.tpl create mode 100644 ts/WoltLabSuite/Core/Form/Builder/Field/Controller/MultilineItemList.ts create mode 100644 ts/WoltLabSuite/Core/Form/Builder/Field/MultilineItemList.ts create mode 100644 wcfsetup/install/files/acp/templates/__multilineItemListFormField.tpl create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/MultilineItemList.js create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/MultilineItemList.js create mode 100644 wcfsetup/install/files/lib/system/form/builder/field/MultilineItemListFormField.class.php diff --git a/com.woltlab.wcf/templates/__multilineItemListFormField.tpl b/com.woltlab.wcf/templates/__multilineItemListFormField.tpl new file mode 100644 index 0000000000..d4e664836f --- /dev/null +++ b/com.woltlab.wcf/templates/__multilineItemListFormField.tpl @@ -0,0 +1,44 @@ + + + diff --git a/syncTemplates.json b/syncTemplates.json index e5eb6103dd..ce8878c7c3 100644 --- a/syncTemplates.json +++ b/syncTemplates.json @@ -4,6 +4,7 @@ "wcfsetup/install/files/acp/templates" ], "templates": [ + "__multilineItemListFormField", "__aclFormField", "__booleanFormField", "__buttonFormField", diff --git a/ts/WoltLabSuite/Core/Form/Builder/Field/Controller/MultilineItemList.ts b/ts/WoltLabSuite/Core/Form/Builder/Field/Controller/MultilineItemList.ts new file mode 100644 index 0000000000..2634d09538 --- /dev/null +++ b/ts/WoltLabSuite/Core/Form/Builder/Field/Controller/MultilineItemList.ts @@ -0,0 +1,218 @@ +/** + * 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 + * @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(); + +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 { + 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(".jsDeleteItem")!; + const valueSpan = li.querySelector("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 { + if (!_data.has(elementId)) { + throw new Error(`Element id '${elementId}' is unknown.`); + } + + return _data.get(elementId)!.getItems(); +} diff --git a/ts/WoltLabSuite/Core/Form/Builder/Field/MultilineItemList.ts b/ts/WoltLabSuite/Core/Form/Builder/Field/MultilineItemList.ts new file mode 100644 index 0000000000..8e8939c421 --- /dev/null +++ b/ts/WoltLabSuite/Core/Form/Builder/Field/MultilineItemList.ts @@ -0,0 +1,22 @@ +/** + * 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 + * @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; diff --git a/wcfsetup/install/files/acp/templates/__multilineItemListFormField.tpl b/wcfsetup/install/files/acp/templates/__multilineItemListFormField.tpl new file mode 100644 index 0000000000..d4e664836f --- /dev/null +++ b/wcfsetup/install/files/acp/templates/__multilineItemListFormField.tpl @@ -0,0 +1,44 @@ +
    getValue()|empty} style="display: none"{/if}{* +*}> + {foreach from=$field->getValue() item=value} +
  • + + + {$value} +
  • + {/foreach} +
+ + diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/MultilineItemList.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/MultilineItemList.js new file mode 100644 index 0000000000..1a5f5f5761 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/MultilineItemList.js @@ -0,0 +1,195 @@ +/** + * 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 + * @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; +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/MultilineItemList.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/MultilineItemList.js new file mode 100644 index 0000000000..703cec1def --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/MultilineItemList.js @@ -0,0 +1,20 @@ +/** + * 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 + * @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; +}); diff --git a/wcfsetup/install/files/lib/system/form/builder/field/MultilineItemListFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/MultilineItemListFormField.class.php new file mode 100644 index 0000000000..b00023ea48 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/field/MultilineItemListFormField.class.php @@ -0,0 +1,49 @@ + + * @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; + } +} -- 2.20.1