Add new form field `MultilineItemListFormField`
authorCyperghost <olaf_schmitz_1@t-online.de>
Thu, 25 Apr 2024 09:24:25 +0000 (11:24 +0200)
committerCyperghost <olaf_schmitz_1@t-online.de>
Thu, 25 Apr 2024 09:24:25 +0000 (11:24 +0200)
com.woltlab.wcf/templates/__multilineItemListFormField.tpl [new file with mode: 0644]
syncTemplates.json
ts/WoltLabSuite/Core/Form/Builder/Field/Controller/MultilineItemList.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/Form/Builder/Field/MultilineItemList.ts [new file with mode: 0644]
wcfsetup/install/files/acp/templates/__multilineItemListFormField.tpl [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/MultilineItemList.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/MultilineItemList.js [new file with mode: 0644]
wcfsetup/install/files/lib/system/form/builder/field/MultilineItemListFormField.class.php [new file with mode: 0644]

diff --git a/com.woltlab.wcf/templates/__multilineItemListFormField.tpl b/com.woltlab.wcf/templates/__multilineItemListFormField.tpl
new file mode 100644 (file)
index 0000000..d4e6648
--- /dev/null
@@ -0,0 +1,44 @@
+<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>
index e5eb6103ddae92ec85606aceb93fb9b253ace6ff..ce8878c7c3b7450426f1079253b6f9914ec0088f 100644 (file)
@@ -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 (file)
index 0000000..2634d09
--- /dev/null
@@ -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 <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();
+}
diff --git a/ts/WoltLabSuite/Core/Form/Builder/Field/MultilineItemList.ts b/ts/WoltLabSuite/Core/Form/Builder/Field/MultilineItemList.ts
new file mode 100644 (file)
index 0000000..8e8939c
--- /dev/null
@@ -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 <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;
diff --git a/wcfsetup/install/files/acp/templates/__multilineItemListFormField.tpl b/wcfsetup/install/files/acp/templates/__multilineItemListFormField.tpl
new file mode 100644 (file)
index 0000000..d4e6648
--- /dev/null
@@ -0,0 +1,44 @@
+<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>
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 (file)
index 0000000..1a5f5f5
--- /dev/null
@@ -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 <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;
+});
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 (file)
index 0000000..703cec1
--- /dev/null
@@ -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 <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;
+});
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 (file)
index 0000000..b00023e
--- /dev/null
@@ -0,0 +1,49 @@
+<?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;
+    }
+}