Initial
This commit is contained in:
34
resources/app/client-esm/applications/elements/_module.mjs
Normal file
34
resources/app/client-esm/applications/elements/_module.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Custom HTMLElement implementations for use in template rendering.
|
||||
* @module elements
|
||||
*/
|
||||
|
||||
import HTMLDocumentTagsElement from "./document-tags.mjs";
|
||||
import HTMLFilePickerElement from "./file-picker.mjs";
|
||||
import HTMLHueSelectorSlider from "./hue-slider.mjs";
|
||||
import {HTMLMultiSelectElement, HTMLMultiCheckboxElement} from "./multi-select.mjs";
|
||||
import HTMLStringTagsElement from "./string-tags.mjs";
|
||||
import HTMLColorPickerElement from "./color-picker.mjs";
|
||||
import HTMLRangePickerElement from "./range-picker.mjs";
|
||||
import HTMLProseMirrorElement from "./prosemirror-editor.mjs";
|
||||
|
||||
export {default as AbstractFormInputElement} from "./form-element.mjs";
|
||||
export {default as HTMLColorPickerElement} from "./color-picker.mjs";
|
||||
export {default as HTMLDocumentTagsElement} from "./document-tags.mjs";
|
||||
export {default as HTMLFilePickerElement} from "./file-picker.mjs";
|
||||
export {default as HTMLHueSelectorSlider} from "./hue-slider.mjs"
|
||||
export {default as HTMLRangePickerElement} from "./range-picker.mjs"
|
||||
export {default as HTMLStringTagsElement} from "./string-tags.mjs"
|
||||
export {default as HTMLProseMirrorElement} from "./prosemirror-editor.mjs";
|
||||
export * from "./multi-select.mjs";
|
||||
|
||||
// Define custom elements
|
||||
window.customElements.define(HTMLColorPickerElement.tagName, HTMLColorPickerElement);
|
||||
window.customElements.define(HTMLDocumentTagsElement.tagName, HTMLDocumentTagsElement);
|
||||
window.customElements.define(HTMLFilePickerElement.tagName, HTMLFilePickerElement);
|
||||
window.customElements.define(HTMLHueSelectorSlider.tagName, HTMLHueSelectorSlider);
|
||||
window.customElements.define(HTMLMultiSelectElement.tagName, HTMLMultiSelectElement);
|
||||
window.customElements.define(HTMLMultiCheckboxElement.tagName, HTMLMultiCheckboxElement);
|
||||
window.customElements.define(HTMLRangePickerElement.tagName, HTMLRangePickerElement);
|
||||
window.customElements.define(HTMLStringTagsElement.tagName, HTMLStringTagsElement);
|
||||
window.customElements.define(HTMLProseMirrorElement.tagName, HTMLProseMirrorElement);
|
||||
103
resources/app/client-esm/applications/elements/color-picker.mjs
Normal file
103
resources/app/client-esm/applications/elements/color-picker.mjs
Normal file
@@ -0,0 +1,103 @@
|
||||
import AbstractFormInputElement from "./form-element.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("../forms/fields.mjs").FormInputConfig} FormInputConfig
|
||||
*/
|
||||
|
||||
/**
|
||||
* A custom HTMLElement used to select a color using a linked pair of input fields.
|
||||
* @extends {AbstractFormInputElement<string>}
|
||||
*/
|
||||
export default class HTMLColorPickerElement extends AbstractFormInputElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._setValue(this.getAttribute("value")); // Initialize existing color value
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static tagName = "color-picker";
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The button element to add a new document.
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
#colorSelector;
|
||||
|
||||
/**
|
||||
* The input element to define a Document UUID.
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
#colorString;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_buildElements() {
|
||||
|
||||
// Create string input element
|
||||
this.#colorString = this._primaryInput = document.createElement("input");
|
||||
this.#colorString.type = "text";
|
||||
this.#colorString.placeholder = this.getAttribute("placeholder") || "";
|
||||
this._applyInputAttributes(this.#colorString);
|
||||
|
||||
// Create color selector element
|
||||
this.#colorSelector = document.createElement("input");
|
||||
this.#colorSelector.type = "color";
|
||||
this._applyInputAttributes(this.#colorSelector);
|
||||
return [this.#colorString, this.#colorSelector];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_refresh() {
|
||||
if ( !this.#colorString ) return; // Not yet connected
|
||||
this.#colorString.value = this._value;
|
||||
this.#colorSelector.value = this._value || this.#colorString.placeholder || "#000000";
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_activateListeners() {
|
||||
const onChange = this.#onChangeInput.bind(this);
|
||||
this.#colorString.addEventListener("change", onChange);
|
||||
this.#colorSelector.addEventListener("change", onChange);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle changes to one of the inputs of the color picker element.
|
||||
* @param {InputEvent} event The originating input change event
|
||||
*/
|
||||
#onChangeInput(event) {
|
||||
event.stopPropagation();
|
||||
this.value = event.currentTarget.value;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_toggleDisabled(disabled) {
|
||||
this.#colorString.toggleAttribute("disabled", disabled);
|
||||
this.#colorSelector.toggleAttribute("disabled", disabled);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a HTMLColorPickerElement using provided configuration data.
|
||||
* @param {FormInputConfig} config
|
||||
* @returns {HTMLColorPickerElement}
|
||||
*/
|
||||
static create(config) {
|
||||
const picker = document.createElement(HTMLColorPickerElement.tagName);
|
||||
picker.name = config.name;
|
||||
picker.setAttribute("value", config.value ?? "");
|
||||
foundry.applications.fields.setInputAttributes(picker, config);
|
||||
return picker;
|
||||
}
|
||||
}
|
||||
344
resources/app/client-esm/applications/elements/document-tags.mjs
Normal file
344
resources/app/client-esm/applications/elements/document-tags.mjs
Normal file
@@ -0,0 +1,344 @@
|
||||
import AbstractFormInputElement from "./form-element.mjs";
|
||||
import HTMLStringTagsElement from "./string-tags.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("../forms/fields.mjs").FormInputConfig} FormInputConfig
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} DocumentTagsInputConfig
|
||||
* @property {string} [type] A specific document type in CONST.ALL_DOCUMENT_TYPES
|
||||
* @property {boolean} [single] Only allow referencing a single document. In this case the submitted form value will
|
||||
* be a single UUID string rather than an array
|
||||
* @property {number} [max] Only allow attaching a maximum number of documents
|
||||
*/
|
||||
|
||||
/**
|
||||
* A custom HTMLElement used to render a set of associated Documents referenced by UUID.
|
||||
* @extends {AbstractFormInputElement<string|string[]|null>}
|
||||
*/
|
||||
export default class HTMLDocumentTagsElement extends AbstractFormInputElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._initializeTags();
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static tagName = "document-tags";
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @type {Record<string, string>}
|
||||
* @protected
|
||||
*/
|
||||
_value = {};
|
||||
|
||||
/**
|
||||
* The button element to add a new document.
|
||||
* @type {HTMLButtonElement}
|
||||
*/
|
||||
#button;
|
||||
|
||||
/**
|
||||
* The input element to define a Document UUID.
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
#input;
|
||||
|
||||
/**
|
||||
* The list of tagged documents.
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
#tags;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Restrict this element to documents of a particular type.
|
||||
* @type {string|null}
|
||||
*/
|
||||
get type() {
|
||||
return this.getAttribute("type");
|
||||
}
|
||||
|
||||
set type(value) {
|
||||
if ( !value ) return this.removeAttribute("type");
|
||||
if ( !CONST.ALL_DOCUMENT_TYPES.includes(value) ) {
|
||||
throw new Error(`"${value}" is not a valid Document type in CONST.ALL_DOCUMENT_TYPES`);
|
||||
}
|
||||
this.setAttribute("type", value);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Restrict to only allow referencing a single Document instead of an array of documents.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get single() {
|
||||
return this.hasAttribute("single");
|
||||
}
|
||||
|
||||
set single(value) {
|
||||
this.toggleAttribute("single", value === true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Allow a maximum number of documents to be tagged to the element.
|
||||
* @type {number}
|
||||
*/
|
||||
get max() {
|
||||
const max = parseInt(this.getAttribute("max"));
|
||||
return isNaN(max) ? Infinity : max;
|
||||
}
|
||||
|
||||
set max(value) {
|
||||
if ( Number.isInteger(value) && (value > 0) ) this.setAttribute("max", String(value));
|
||||
else this.removeAttribute("max");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Initialize innerText or an initial value attribute of the element as a serialized JSON array.
|
||||
* @protected
|
||||
*/
|
||||
_initializeTags() {
|
||||
const initial = this.getAttribute("value") || this.innerText || "";
|
||||
const tags = initial ? initial.split(",") : [];
|
||||
for ( const t of tags ) {
|
||||
try {
|
||||
this.#add(t);
|
||||
} catch(err) {
|
||||
this._value[t] = `${t} [INVALID]`; // Display invalid UUIDs as a raw string
|
||||
}
|
||||
}
|
||||
this.innerText = "";
|
||||
this.removeAttribute("value");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_buildElements() {
|
||||
|
||||
// Create tags list
|
||||
this.#tags = document.createElement("div");
|
||||
this.#tags.className = "tags input-element-tags";
|
||||
|
||||
// Create input element
|
||||
this.#input = this._primaryInput = document.createElement("input");
|
||||
this.#input.type = "text";
|
||||
this.#input.placeholder = game.i18n.format("HTMLDocumentTagsElement.PLACEHOLDER", {
|
||||
type: game.i18n.localize(this.type ? getDocumentClass(this.type).metadata.label : "DOCUMENT.Document")});
|
||||
|
||||
// Create button
|
||||
this.#button = document.createElement("button");
|
||||
this.#button.type = "button"
|
||||
this.#button.className = "icon fa-solid fa-file-plus";
|
||||
this.#button.dataset.tooltip = game.i18n.localize("ELEMENTS.DOCUMENT_TAGS.Add");
|
||||
this.#button.setAttribute("aria-label", this.#button.dataset.tooltip);
|
||||
return [this.#tags, this.#input, this.#button];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_refresh() {
|
||||
if ( !this.#tags ) return; // Not yet connected
|
||||
const tags = Object.entries(this._value).map(([k, v]) => this.constructor.renderTag(k, v, this.editable));
|
||||
this.#tags.replaceChildren(...tags);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create an HTML string fragment for a single document tag.
|
||||
* @param {string} uuid The document UUID
|
||||
* @param {string} name The document name
|
||||
* @param {boolean} [editable=true] Is the tag editable?
|
||||
* @returns {HTMLDivElement}
|
||||
*/
|
||||
static renderTag(uuid, name, editable=true) {
|
||||
const div = HTMLStringTagsElement.renderTag(uuid, TextEditor.truncateText(name, {maxLength: 32}), editable);
|
||||
div.classList.add("document-tag");
|
||||
div.querySelector("span").dataset.tooltip = uuid;
|
||||
if ( editable ) {
|
||||
const t = game.i18n.localize("ELEMENTS.DOCUMENT_TAGS.Remove");
|
||||
const a = div.querySelector("a");
|
||||
a.dataset.tooltip = t;
|
||||
a.ariaLabel = t;
|
||||
}
|
||||
return div;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_activateListeners() {
|
||||
this.#button.addEventListener("click", () => this.#tryAdd(this.#input.value));
|
||||
this.#tags.addEventListener("click", this.#onClickTag.bind(this));
|
||||
this.#input.addEventListener("keydown", this.#onKeydown.bind(this));
|
||||
this.addEventListener("drop", this.#onDrop.bind(this));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Remove a single coefficient by clicking on its tag.
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
#onClickTag(event) {
|
||||
if ( !event.target.classList.contains("remove") ) return;
|
||||
const tag = event.target.closest(".tag");
|
||||
delete this._value[tag.dataset.key];
|
||||
this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add a new document tag by pressing the ENTER key in the UUID input field.
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
#onKeydown(event) {
|
||||
if ( event.key !== "Enter" ) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.#tryAdd(this.#input.value);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle data dropped onto the form element.
|
||||
* @param {DragEvent} event
|
||||
*/
|
||||
#onDrop(event) {
|
||||
event.preventDefault();
|
||||
const dropData = TextEditor.getDragEventData(event);
|
||||
if ( dropData.uuid ) this.#tryAdd(dropData.uuid);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add a Document to the tagged set using the value of the input field.
|
||||
* @param {string} uuid The UUID to attempt to add
|
||||
*/
|
||||
#tryAdd(uuid) {
|
||||
try {
|
||||
this.#add(uuid);
|
||||
this._refresh();
|
||||
} catch(err) {
|
||||
ui.notifications.error(err.message);
|
||||
}
|
||||
this.#input.value = "";
|
||||
this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
|
||||
this.#input.focus();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Validate that the tagged document is allowed to be added to this field.
|
||||
* Subclasses may impose more strict validation as to which types of documents are allowed.
|
||||
* @param {foundry.abstract.Document|object} document A candidate document or compendium index entry to tag
|
||||
* @throws {Error} An error if the candidate document is not allowed
|
||||
*/
|
||||
_validateDocument(document) {
|
||||
const {type, max} = this;
|
||||
if ( type && (document.documentName !== type) ) throw new Error(`Incorrect document type "${document.documentName}"`
|
||||
+ ` provided to document tag field which requires "${type}" documents.`);
|
||||
const n = Object.keys(this._value).length;
|
||||
if ( n >= max ) throw new Error(`You may only attach at most ${max} Documents to the "${this.name}" field`);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add a new UUID to the tagged set, throwing an error if the UUID is not valid.
|
||||
* @param {string} uuid The UUID to add
|
||||
* @throws {Error} If the UUID is not valid
|
||||
*/
|
||||
#add(uuid) {
|
||||
|
||||
// Require the UUID to exist
|
||||
let record;
|
||||
const {id} = foundry.utils.parseUuid(uuid);
|
||||
if ( id ) record = fromUuidSync(uuid);
|
||||
else if ( this.type ) {
|
||||
const collection = game.collections.get(this.type);
|
||||
record = collection.get(uuid);
|
||||
}
|
||||
if ( !record ) throw new Error(`Invalid document UUID "${uuid}" provided to document tag field.`);
|
||||
|
||||
// Require a certain type of document
|
||||
this._validateDocument(record);
|
||||
|
||||
// Replace singleton
|
||||
if ( this.single ) {
|
||||
for ( const k of Object.keys(this._value) ) delete this._value[k];
|
||||
}
|
||||
|
||||
// Record the document
|
||||
this._value[uuid] = record.name;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Form Handling */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getValue() {
|
||||
const uuids = Object.keys(this._value);
|
||||
if ( this.single ) return uuids[0] ?? null;
|
||||
else return uuids;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_setValue(value) {
|
||||
this._value = {};
|
||||
if ( !value ) return;
|
||||
if ( typeof value === "string" ) value = [value];
|
||||
for ( const uuid of value ) this.#add(uuid);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_toggleDisabled(disabled) {
|
||||
this.#input?.toggleAttribute("disabled", disabled);
|
||||
this.#button?.toggleAttribute("disabled", disabled);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a HTMLDocumentTagsElement using provided configuration data.
|
||||
* @param {FormInputConfig & DocumentTagsInputConfig} config
|
||||
* @returns {HTMLDocumentTagsElement}
|
||||
*/
|
||||
static create(config) {
|
||||
const tags = /** @type {HTMLDocumentTagsElement} */ document.createElement(HTMLDocumentTagsElement.tagName);
|
||||
tags.name = config.name;
|
||||
|
||||
// Coerce value to an array
|
||||
let values;
|
||||
if ( config.value instanceof Set ) values = Array.from(config.value);
|
||||
else if ( !Array.isArray(config.value) ) values = [config.value];
|
||||
else values = config.value;
|
||||
|
||||
tags.setAttribute("value", values);
|
||||
tags.type = config.type;
|
||||
tags.max = config.max;
|
||||
tags.single = config.single;
|
||||
foundry.applications.fields.setInputAttributes(tags, config);
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
158
resources/app/client-esm/applications/elements/file-picker.mjs
Normal file
158
resources/app/client-esm/applications/elements/file-picker.mjs
Normal file
@@ -0,0 +1,158 @@
|
||||
import AbstractFormInputElement from "./form-element.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("../forms/fields.mjs").FormInputConfig} FormInputConfig
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} FilePickerInputConfig
|
||||
* @property {FilePickerOptions.type} [type]
|
||||
* @property {string} [placeholder]
|
||||
* @property {boolean} [noupload]
|
||||
*/
|
||||
|
||||
/**
|
||||
* A custom HTML element responsible for rendering a file input field and associated FilePicker button.
|
||||
* @extends {AbstractFormInputElement<string>}
|
||||
*/
|
||||
export default class HTMLFilePickerElement extends AbstractFormInputElement {
|
||||
|
||||
/** @override */
|
||||
static tagName = "file-picker";
|
||||
|
||||
/**
|
||||
* The file path selected.
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
input;
|
||||
|
||||
/**
|
||||
* A button to open the file picker interface.
|
||||
* @type {HTMLButtonElement}
|
||||
*/
|
||||
button;
|
||||
|
||||
/**
|
||||
* A reference to the FilePicker application instance originated by this element.
|
||||
* @type {FilePicker}
|
||||
*/
|
||||
picker;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A type of file which can be selected in this field.
|
||||
* @see {@link FilePicker.FILE_TYPES}
|
||||
* @type {FilePickerOptions.type}
|
||||
*/
|
||||
get type() {
|
||||
return this.getAttribute("type") ?? "any";
|
||||
}
|
||||
|
||||
set type(value) {
|
||||
if ( !FilePicker.FILE_TYPES.includes(value) ) throw new Error(`Invalid type "${value}" provided which must be a `
|
||||
+ "value in FilePicker.TYPES");
|
||||
this.setAttribute("type", value);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prevent uploading new files as part of this element's FilePicker dialog.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get noupload() {
|
||||
return this.hasAttribute("noupload");
|
||||
}
|
||||
|
||||
set noupload(value) {
|
||||
this.toggleAttribute("noupload", value === true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_buildElements() {
|
||||
|
||||
// Initialize existing value
|
||||
this._value ??= this.getAttribute("value") || this.innerText || "";
|
||||
this.removeAttribute("value");
|
||||
|
||||
// Create an input field
|
||||
const elements = [];
|
||||
this.input = this._primaryInput = document.createElement("input");
|
||||
this.input.className = "image";
|
||||
this.input.type = "text";
|
||||
this.input.placeholder = this.getAttribute("placeholder") ?? "path/to/file.ext";
|
||||
elements.push(this.input);
|
||||
|
||||
// Disallow browsing for some users
|
||||
if ( game.world && !game.user.can("FILES_BROWSE") ) return elements;
|
||||
|
||||
// Create a FilePicker button
|
||||
this.button = document.createElement("button");
|
||||
this.button.className = "fa-solid fa-file-import fa-fw";
|
||||
this.button.type = "button";
|
||||
this.button.dataset.tooltip = game.i18n.localize("FILES.BrowseTooltip");
|
||||
this.button.setAttribute("aria-label", this.button.dataset.tooltip);
|
||||
this.button.tabIndex = -1;
|
||||
elements.push(this.button);
|
||||
return elements;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_refresh() {
|
||||
this.input.value = this._value;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_toggleDisabled(disabled) {
|
||||
this.input.disabled = disabled;
|
||||
if ( this.button ) this.button.disabled = disabled;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_activateListeners() {
|
||||
this.input.addEventListener("input", () => this._value = this.input.value);
|
||||
this.button?.addEventListener("click", this.#onClickButton.bind(this));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle clicks on the button element to render the FilePicker UI.
|
||||
* @param {PointerEvent} event The initiating click event
|
||||
*/
|
||||
#onClickButton(event) {
|
||||
event.preventDefault();
|
||||
this.picker = new FilePicker({
|
||||
type: this.type,
|
||||
current: this.value,
|
||||
allowUpload: !this.noupload,
|
||||
callback: src => this.value = src
|
||||
});
|
||||
return this.picker.browse();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a HTMLFilePickerElement using provided configuration data.
|
||||
* @param {FormInputConfig<string> & FilePickerInputConfig} config
|
||||
*/
|
||||
static create(config) {
|
||||
const picker = document.createElement(this.tagName);
|
||||
picker.name = config.name;
|
||||
picker.setAttribute("value", config.value || "");
|
||||
picker.type = config.type;
|
||||
picker.noupload = config.noupload;
|
||||
foundry.applications.fields.setInputAttributes(picker, config);
|
||||
return picker;
|
||||
}
|
||||
}
|
||||
214
resources/app/client-esm/applications/elements/form-element.mjs
Normal file
214
resources/app/client-esm/applications/elements/form-element.mjs
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* An abstract custom HTMLElement designed for use with form inputs.
|
||||
* @abstract
|
||||
* @template {any} FormInputValueType
|
||||
*
|
||||
* @fires {Event} input An "input" event when the value of the input changes
|
||||
* @fires {Event} change A "change" event when the value of the element changes
|
||||
*/
|
||||
export default class AbstractFormInputElement extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._internals = this.attachInternals();
|
||||
}
|
||||
|
||||
/**
|
||||
* The HTML tag name used by this element.
|
||||
* @type {string}
|
||||
*/
|
||||
static tagName;
|
||||
|
||||
/**
|
||||
* Declare that this custom element provides form element functionality.
|
||||
* @type {boolean}
|
||||
*/
|
||||
static formAssociated = true;
|
||||
|
||||
/**
|
||||
* Attached ElementInternals which provides form handling functionality.
|
||||
* @type {ElementInternals}
|
||||
* @protected
|
||||
*/
|
||||
_internals;
|
||||
|
||||
/**
|
||||
* The primary input (if any). Used to determine what element should receive focus when an associated label is clicked
|
||||
* on.
|
||||
* @type {HTMLElement}
|
||||
* @protected
|
||||
*/
|
||||
_primaryInput;
|
||||
|
||||
/**
|
||||
* The form this element belongs to.
|
||||
* @type {HTMLFormElement}
|
||||
*/
|
||||
get form() {
|
||||
return this._internals.form;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Element Properties */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The input element name.
|
||||
* @type {string}
|
||||
*/
|
||||
get name() {
|
||||
return this.getAttribute("name");
|
||||
}
|
||||
|
||||
set name(value) {
|
||||
this.setAttribute("name", value);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The value of the input element.
|
||||
* @type {FormInputValueType}
|
||||
*/
|
||||
get value() {
|
||||
return this._getValue();
|
||||
}
|
||||
|
||||
set value(value) {
|
||||
this._setValue(value);
|
||||
this.dispatchEvent(new Event("input", {bubbles: true, cancelable: true}));
|
||||
this.dispatchEvent(new Event("change", {bubbles: true, cancelable: true}));
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* The underlying value of the element.
|
||||
* @type {FormInputValueType}
|
||||
* @protected
|
||||
*/
|
||||
_value;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Return the value of the input element which should be submitted to the form.
|
||||
* @returns {FormInputValueType}
|
||||
* @protected
|
||||
*/
|
||||
_getValue() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Translate user-provided input value into the format that should be stored.
|
||||
* @param {FormInputValueType} value A new value to assign to the element
|
||||
* @throws {Error} An error if the provided value is invalid
|
||||
* @protected
|
||||
*/
|
||||
_setValue(value) {
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is this element disabled?
|
||||
* @type {boolean}
|
||||
*/
|
||||
get disabled() {
|
||||
return this.hasAttribute("disabled");
|
||||
}
|
||||
|
||||
set disabled(value) {
|
||||
this.toggleAttribute("disabled", value);
|
||||
this._toggleDisabled(!this.editable);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is this field editable? The field can be neither disabled nor readonly.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get editable() {
|
||||
return !(this.hasAttribute("disabled") || this.hasAttribute("readonly"));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Special behaviors that the subclass should implement when toggling the disabled state of the input.
|
||||
* @param {boolean} disabled The new disabled state
|
||||
* @protected
|
||||
*/
|
||||
_toggleDisabled(disabled) {}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Element Lifecycle */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Initialize the custom element, constructing its HTML.
|
||||
*/
|
||||
connectedCallback() {
|
||||
const elements = this._buildElements();
|
||||
this.replaceChildren(...elements);
|
||||
this._refresh();
|
||||
this._toggleDisabled(!this.editable);
|
||||
this.addEventListener("click", this._onClick.bind(this));
|
||||
this._activateListeners();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create the HTML elements that should be included in this custom element.
|
||||
* Elements are returned as an array of ordered children.
|
||||
* @returns {HTMLElement[]}
|
||||
* @protected
|
||||
*/
|
||||
_buildElements() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Refresh the active state of the custom element.
|
||||
* @protected
|
||||
*/
|
||||
_refresh() {}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Apply key attributes on the containing custom HTML element to input elements contained within it.
|
||||
* @internal
|
||||
*/
|
||||
_applyInputAttributes(input) {
|
||||
input.toggleAttribute("required", this.hasAttribute("required"));
|
||||
input.toggleAttribute("disabled", this.hasAttribute("disabled"));
|
||||
input.toggleAttribute("readonly", this.hasAttribute("readonly"));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Activate event listeners which add dynamic behavior to the custom element.
|
||||
* @protected
|
||||
*/
|
||||
_activateListeners() {}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Special handling when the custom element is clicked. This should be implemented to transfer focus to an
|
||||
* appropriate internal element.
|
||||
* @param {PointerEvent} event
|
||||
* @protected
|
||||
*/
|
||||
_onClick(event) {
|
||||
if ( event.target === this ) this._primaryInput?.focus?.();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import AbstractFormInputElement from "./form-element.mjs";
|
||||
|
||||
/**
|
||||
* A class designed to standardize the behavior for a hue selector UI component.
|
||||
* @extends {AbstractFormInputElement<number>}
|
||||
*/
|
||||
export default class HTMLHueSelectorSlider extends AbstractFormInputElement {
|
||||
|
||||
/** @override */
|
||||
static tagName = "hue-slider";
|
||||
|
||||
/**
|
||||
* The color range associated with this element.
|
||||
* @type {HTMLInputElement|null}
|
||||
*/
|
||||
#input;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_buildElements() {
|
||||
|
||||
// Initialize existing value
|
||||
this._setValue(this.getAttribute("value"));
|
||||
|
||||
// Build elements
|
||||
this.#input = this._primaryInput = document.createElement("input");
|
||||
this.#input.className = "color-range";
|
||||
this.#input.type = "range";
|
||||
this.#input.min = "0";
|
||||
this.#input.max = "360";
|
||||
this.#input.step = "1";
|
||||
this.#input.disabled = this.disabled;
|
||||
this.#input.value = this._value * 360;
|
||||
return [this.#input];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Refresh the active state of the custom element.
|
||||
* @protected
|
||||
*/
|
||||
_refresh() {
|
||||
this.#input.style.setProperty("--color-thumb", Color.fromHSL([this._value, 1, 0.5]).css);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Activate event listeners which add dynamic behavior to the custom element.
|
||||
* @protected
|
||||
*/
|
||||
_activateListeners() {
|
||||
this.#input.oninput = this.#onInputColorRange.bind(this);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update the thumb and the value.
|
||||
* @param {FormDataEvent} event
|
||||
*/
|
||||
#onInputColorRange(event) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
this.value = this.#input.value / 360;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Form Handling
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_setValue(value) {
|
||||
value = Number(value);
|
||||
if ( !value.between(0, 1) ) throw new Error("The value of a hue-slider must be on the range [0,1]");
|
||||
this._value = value;
|
||||
this.setAttribute("value", String(value));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_toggleDisabled(disabled) {
|
||||
this.#input.disabled = disabled;
|
||||
}
|
||||
}
|
||||
361
resources/app/client-esm/applications/elements/multi-select.mjs
Normal file
361
resources/app/client-esm/applications/elements/multi-select.mjs
Normal file
@@ -0,0 +1,361 @@
|
||||
import AbstractFormInputElement from "./form-element.mjs";
|
||||
import HTMLStringTagsElement from "./string-tags.mjs";
|
||||
|
||||
/**
|
||||
* An abstract base class designed to standardize the behavior for a multi-select UI component.
|
||||
* Multi-select components return an array of values as part of form submission.
|
||||
* Different implementations may provide different experiences around how inputs are presented to the user.
|
||||
* @extends {AbstractFormInputElement<Set<string>>}
|
||||
*/
|
||||
export class AbstractMultiSelectElement extends AbstractFormInputElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._value = new Set();
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Predefined <option> and <optgroup> elements which were defined in the original HTML.
|
||||
* @type {(HTMLOptionElement|HTMLOptGroupElement)[]}
|
||||
* @protected
|
||||
*/
|
||||
_options;
|
||||
|
||||
/**
|
||||
* An object which maps option values to displayed labels.
|
||||
* @type {Record<string, string>}
|
||||
* @protected
|
||||
*/
|
||||
_choices = {};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Preserve existing <option> and <optgroup> elements which are defined in the original HTML.
|
||||
* @protected
|
||||
*/
|
||||
_initialize() {
|
||||
this._options = [...this.children];
|
||||
for ( const option of this.querySelectorAll("option") ) {
|
||||
if ( !option.value ) continue; // Skip predefined options which are already blank
|
||||
this._choices[option.value] = option.innerText;
|
||||
if ( option.selected ) {
|
||||
this._value.add(option.value);
|
||||
option.selected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mark a choice as selected.
|
||||
* @param {string} value The value to add to the chosen set
|
||||
*/
|
||||
select(value) {
|
||||
const exists = this._value.has(value);
|
||||
if ( !exists ) {
|
||||
if ( !(value in this._choices) ) {
|
||||
throw new Error(`"${value}" is not an option allowed by this multi-select element`);
|
||||
}
|
||||
this._value.add(value);
|
||||
this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mark a choice as un-selected.
|
||||
* @param {string} value The value to delete from the chosen set
|
||||
*/
|
||||
unselect(value) {
|
||||
const exists = this._value.has(value);
|
||||
if ( exists ) {
|
||||
this._value.delete(value);
|
||||
this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Form Handling */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getValue() {
|
||||
return Array.from(this._value);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_setValue(value) {
|
||||
if ( !Array.isArray(value) ) {
|
||||
throw new Error("The value assigned to a multi-select element must be an array.");
|
||||
}
|
||||
if ( value.some(v => !(v in this._choices)) ) {
|
||||
throw new Error("The values assigned to a multi-select element must all be valid options.");
|
||||
}
|
||||
this._value.clear();
|
||||
for ( const v of value ) this._value.add(v);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Provide a multi-select workflow using a select element as the input mechanism.
|
||||
*
|
||||
* @example Multi-Select HTML Markup
|
||||
* ```html
|
||||
* <multi-select name="select-many-things">
|
||||
* <optgroup label="Basic Options">
|
||||
* <option value="foo">Foo</option>
|
||||
* <option value="bar">Bar</option>
|
||||
* <option value="baz">Baz</option>
|
||||
* </optgroup>
|
||||
* <optgroup label="Advanced Options">
|
||||
* <option value="fizz">Fizz</option>
|
||||
* <option value="buzz">Buzz</option>
|
||||
* </optgroup>
|
||||
* </multi-select>
|
||||
* ```
|
||||
*/
|
||||
export class HTMLMultiSelectElement extends AbstractMultiSelectElement {
|
||||
|
||||
/** @override */
|
||||
static tagName = "multi-select";
|
||||
|
||||
/**
|
||||
* A select element used to choose options.
|
||||
* @type {HTMLSelectElement}
|
||||
*/
|
||||
#select;
|
||||
|
||||
/**
|
||||
* A display element which lists the chosen options.
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
#tags;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_buildElements() {
|
||||
|
||||
// Create select element
|
||||
this.#select = this._primaryInput = document.createElement("select");
|
||||
this.#select.insertAdjacentHTML("afterbegin", '<option value=""></option>');
|
||||
this.#select.append(...this._options);
|
||||
this.#select.disabled = !this.editable;
|
||||
|
||||
// Create a div element for display
|
||||
this.#tags = document.createElement("div");
|
||||
this.#tags.className = "tags input-element-tags";
|
||||
return [this.#tags, this.#select];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_refresh() {
|
||||
|
||||
// Update the displayed tags
|
||||
const tags = Array.from(this._value).map(id => {
|
||||
return HTMLStringTagsElement.renderTag(id, this._choices[id], this.editable);
|
||||
});
|
||||
this.#tags.replaceChildren(...tags);
|
||||
|
||||
// Disable selected options
|
||||
for ( const option of this.#select.querySelectorAll("option") ) {
|
||||
option.disabled = this._value.has(option.value);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_activateListeners() {
|
||||
this.#select.addEventListener("change", this.#onChangeSelect.bind(this));
|
||||
this.#tags.addEventListener("click", this.#onClickTag.bind(this));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle changes to the Select input, marking the selected option as a chosen value.
|
||||
* @param {Event} event The change event on the select element
|
||||
*/
|
||||
#onChangeSelect(event) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
const select = event.currentTarget;
|
||||
if ( !select.value ) return; // Ignore selection of the blank value
|
||||
this.select(select.value);
|
||||
select.value = "";
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle click events on a tagged value, removing it from the chosen set.
|
||||
* @param {PointerEvent} event The originating click event on a chosen tag
|
||||
*/
|
||||
#onClickTag(event) {
|
||||
event.preventDefault();
|
||||
if ( !event.target.classList.contains("remove") ) return;
|
||||
if ( !this.editable ) return;
|
||||
const tag = event.target.closest(".tag");
|
||||
this.unselect(tag.dataset.key);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_toggleDisabled(disabled) {
|
||||
this.#select.toggleAttribute("disabled", disabled);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a HTMLMultiSelectElement using provided configuration data.
|
||||
* @param {FormInputConfig<string[]> & Omit<SelectInputConfig, "blank">} config
|
||||
* @returns {HTMLMultiSelectElement}
|
||||
*/
|
||||
static create(config) {
|
||||
return foundry.applications.fields.createMultiSelectInput(config);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Provide a multi-select workflow as a grid of input checkbox elements.
|
||||
*
|
||||
* @example Multi-Checkbox HTML Markup
|
||||
* ```html
|
||||
* <multi-checkbox name="check-many-boxes">
|
||||
* <optgroup label="Basic Options">
|
||||
* <option value="foo">Foo</option>
|
||||
* <option value="bar">Bar</option>
|
||||
* <option value="baz">Baz</option>
|
||||
* </optgroup>
|
||||
* <optgroup label="Advanced Options">
|
||||
* <option value="fizz">Fizz</option>
|
||||
* <option value="buzz">Buzz</option>
|
||||
* </optgroup>
|
||||
* </multi-checkbox>
|
||||
* ```
|
||||
*/
|
||||
export class HTMLMultiCheckboxElement extends AbstractMultiSelectElement {
|
||||
|
||||
/** @override */
|
||||
static tagName = "multi-checkbox";
|
||||
|
||||
/**
|
||||
* The checkbox elements used to select inputs
|
||||
* @type {HTMLInputElement[]}
|
||||
*/
|
||||
#checkboxes;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_buildElements() {
|
||||
this.#checkboxes = [];
|
||||
const children = [];
|
||||
for ( const option of this._options ) {
|
||||
if ( option instanceof HTMLOptGroupElement ) children.push(this.#buildGroup(option));
|
||||
else children.push(this.#buildOption(option));
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Translate an input <optgroup> element into a <fieldset> of checkboxes.
|
||||
* @param {HTMLOptGroupElement} optgroup The originally configured optgroup
|
||||
* @returns {HTMLFieldSetElement} The created fieldset grouping
|
||||
*/
|
||||
#buildGroup(optgroup) {
|
||||
|
||||
// Create fieldset group
|
||||
const group = document.createElement("fieldset");
|
||||
group.classList.add("checkbox-group");
|
||||
const legend = document.createElement("legend");
|
||||
legend.innerText = optgroup.label;
|
||||
group.append(legend);
|
||||
|
||||
// Add child options
|
||||
for ( const option of optgroup.children ) {
|
||||
if ( option instanceof HTMLOptionElement ) {
|
||||
group.append(this.#buildOption(option));
|
||||
}
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Build an input <option> element into a <label class="checkbox"> element.
|
||||
* @param {HTMLOptionElement} option The originally configured option
|
||||
* @returns {HTMLLabelElement} The created labeled checkbox element
|
||||
*/
|
||||
#buildOption(option) {
|
||||
const label = document.createElement("label");
|
||||
label.classList.add("checkbox");
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.value = option.value;
|
||||
checkbox.checked = this._value.has(option.value);
|
||||
checkbox.disabled = this.disabled;
|
||||
label.append(checkbox, option.innerText);
|
||||
this.#checkboxes.push(checkbox);
|
||||
return label;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_refresh() {
|
||||
for ( const checkbox of this.#checkboxes ) {
|
||||
checkbox.checked = this._value.has(checkbox.value);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_activateListeners() {
|
||||
for ( const checkbox of this.#checkboxes ) {
|
||||
checkbox.addEventListener("change", this.#onChangeCheckbox.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle changes to a checkbox input, marking the selected option as a chosen value.
|
||||
* @param {Event} event The change event on the checkbox input element
|
||||
*/
|
||||
#onChangeCheckbox(event) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
const checkbox = event.currentTarget;
|
||||
if ( checkbox.checked ) this.select(checkbox.value);
|
||||
else this.unselect(checkbox.value);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_toggleDisabled(disabled) {
|
||||
for ( const checkbox of this.#checkboxes ) {
|
||||
checkbox.disabled = disabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import AbstractFormInputElement from "./form-element.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("../forms/fields.mjs").FormInputConfig} FormInputConfig
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ProseMirrorInputConfig
|
||||
* @property {boolean} toggled Is this editor toggled (true) or always active (false)
|
||||
* @property {string} [enriched] If the editor is toggled, provide the enrichedHTML which is displayed while
|
||||
* the editor is not active.
|
||||
* @property {boolean} collaborate Does this editor instance support collaborative editing?
|
||||
* @property {boolean} compact Should the editor be presented in compact mode?
|
||||
* @property {string} documentUUID A Document UUID. Required for collaborative editing
|
||||
*/
|
||||
|
||||
/**
|
||||
* A custom HTML element responsible displaying a ProseMirror rich text editor.
|
||||
* @extends {AbstractFormInputElement<string>}
|
||||
*/
|
||||
export default class HTMLProseMirrorElement extends AbstractFormInputElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Initialize raw content
|
||||
this._setValue(this.getAttribute("value") || "");
|
||||
this.removeAttribute("value");
|
||||
|
||||
// Initialize enriched content
|
||||
this.#toggled = this.hasAttribute("toggled");
|
||||
this.#enriched = this.innerHTML;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static tagName = "prose-mirror";
|
||||
|
||||
/**
|
||||
* Is the editor in active edit mode?
|
||||
* @type {boolean}
|
||||
*/
|
||||
#active = false;
|
||||
|
||||
/**
|
||||
* The ProseMirror editor instance.
|
||||
* @type {ProseMirrorEditor}
|
||||
*/
|
||||
#editor;
|
||||
|
||||
/**
|
||||
* Current editor contents
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
#content;
|
||||
|
||||
/**
|
||||
* Does this editor function via a toggle button? Or is it always active?
|
||||
* @type {boolean}
|
||||
*/
|
||||
#toggled;
|
||||
|
||||
/**
|
||||
* Enriched content which is optionally used if the editor is toggled.
|
||||
* @type {string}
|
||||
*/
|
||||
#enriched;
|
||||
|
||||
/**
|
||||
* An optional edit button which activates edit mode for the editor
|
||||
* @type {HTMLButtonElement|null}
|
||||
*/
|
||||
#button = null;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Actions to take when the custom element is removed from the document.
|
||||
*/
|
||||
disconnectedCallback() {
|
||||
this.#editor?.destroy();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_buildElements() {
|
||||
this.classList.add("editor", "prosemirror", "inactive");
|
||||
const elements = [];
|
||||
this.#content = document.createElement("div");
|
||||
this.#content.className = "editor-content";
|
||||
elements.push(this.#content);
|
||||
if ( this.#toggled ) {
|
||||
this.#button = document.createElement("button");
|
||||
this.#button.type = "button";
|
||||
this.#button.className = "icon toggle";
|
||||
this.#button.innerHTML = `<i class="fa-solid fa-edit"></i>`;
|
||||
elements.push(this.#button);
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_refresh() {
|
||||
if ( this.#active ) return; // It is not safe to replace the content while the editor is active
|
||||
if ( this.#toggled ) this.#content.innerHTML = this.#enriched ?? this._value;
|
||||
else this.#content.innerHTML = this._value;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_activateListeners() {
|
||||
if ( this.#toggled ) this.#button.addEventListener("click", this.#onClickButton.bind(this));
|
||||
else this.#activateEditor();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getValue() {
|
||||
if ( this.#active ) return ProseMirror.dom.serializeString(this.#editor.view.state.doc.content);
|
||||
return this._value;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Activate the ProseMirror editor.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async #activateEditor() {
|
||||
|
||||
// If the editor was toggled, replace with raw editable content
|
||||
if ( this.#toggled ) this.#content.innerHTML = this._value;
|
||||
|
||||
// Create the TextEditor instance
|
||||
const document = await fromUuid(this.dataset.documentUuid ?? this.dataset.documentUUID);
|
||||
this.#editor = await TextEditor.create({
|
||||
engine: "prosemirror",
|
||||
plugins: this._configurePlugins(),
|
||||
fieldName: this.name,
|
||||
collaborate: this.hasAttribute("collaborate"),
|
||||
target: this.#content,
|
||||
document
|
||||
}, this._getValue());
|
||||
|
||||
// Toggle active state
|
||||
this.#active = true;
|
||||
if ( this.#button ) this.#button.disabled = true;
|
||||
this.classList.add("active");
|
||||
this.classList.remove("inactive");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Configure ProseMirror editor plugins.
|
||||
* @returns {Record<string, ProseMirror.Plugin>}
|
||||
* @protected
|
||||
*/
|
||||
_configurePlugins() {
|
||||
return {
|
||||
menu: ProseMirror.ProseMirrorMenu.build(ProseMirror.defaultSchema, {
|
||||
compact: this.hasAttribute("compact"),
|
||||
destroyOnSave: this.#toggled,
|
||||
onSave: this.#save.bind(this)
|
||||
}),
|
||||
keyMaps: ProseMirror.ProseMirrorKeyMaps.build(ProseMirror.defaultSchema, {
|
||||
onSave: this.#save.bind(this)
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle clicking the editor activation button.
|
||||
* @param {PointerEvent} event The triggering event.
|
||||
*/
|
||||
#onClickButton(event) {
|
||||
event.preventDefault();
|
||||
this.#activateEditor();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle saving the editor content.
|
||||
* Store new parsed HTML into the _value attribute of the element.
|
||||
* If the editor is toggled, also deactivate editing mode.
|
||||
*/
|
||||
#save() {
|
||||
const value = ProseMirror.dom.serializeString(this.#editor.view.state.doc.content);
|
||||
if ( value !== this._value ) {
|
||||
this._setValue(value);
|
||||
this.dispatchEvent(new Event("change", {bubbles: true, cancelable: true}));
|
||||
}
|
||||
|
||||
// Deactivate a toggled editor
|
||||
if ( this.#toggled ) {
|
||||
this.#button.disabled = this.disabled;
|
||||
this.#active = false;
|
||||
this.#editor.destroy();
|
||||
this.classList.remove("active");
|
||||
this.classList.add("inactive");
|
||||
this.replaceChildren(this.#button, this.#content);
|
||||
this._refresh();
|
||||
this.dispatchEvent(new Event("close", {bubbles: true, cancelable: true}));
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_toggleDisabled(disabled) {
|
||||
if ( this.#toggled ) this.#button.disabled = disabled;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a HTMLProseMirrorElement using provided configuration data.
|
||||
* @param {FormInputConfig & ProseMirrorInputConfig} config
|
||||
* @returns {HTMLProseMirrorElement}
|
||||
*/
|
||||
static create(config) {
|
||||
const editor = document.createElement(HTMLProseMirrorElement.tagName);
|
||||
editor.name = config.name;
|
||||
|
||||
// Configure editor properties
|
||||
editor.toggleAttribute("collaborate", config.collaborate ?? false);
|
||||
editor.toggleAttribute("compact", config.compact ?? false);
|
||||
editor.toggleAttribute("toggled", config.toggled ?? false);
|
||||
if ( "documentUUID" in config ) Object.assign(editor.dataset, {
|
||||
documentUuid: config.documentUUID,
|
||||
documentUUID: config.documentUUID
|
||||
});
|
||||
if ( Number.isNumeric(config.height) ) editor.style.height = `${config.height}px`;
|
||||
|
||||
// Un-enriched content gets temporarily assigned to the value property of the element
|
||||
editor.setAttribute("value", config.value);
|
||||
|
||||
// Enriched content gets temporarily assigned as the innerHTML of the element
|
||||
if ( config.toggled && config.enriched ) editor.innerHTML = config.enriched;
|
||||
return editor;
|
||||
}
|
||||
}
|
||||
166
resources/app/client-esm/applications/elements/range-picker.mjs
Normal file
166
resources/app/client-esm/applications/elements/range-picker.mjs
Normal file
@@ -0,0 +1,166 @@
|
||||
import AbstractFormInputElement from "./form-element.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("../forms/fields.mjs").FormInputConfig} FormInputConfig
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} RangePickerInputConfig
|
||||
* @property {number} min
|
||||
* @property {number} max
|
||||
* @property {number} [step]
|
||||
*/
|
||||
|
||||
/**
|
||||
* A custom HTML element responsible selecting a value on a range slider with a linked number input field.
|
||||
* @extends {AbstractFormInputElement<number>}
|
||||
*/
|
||||
export default class HTMLRangePickerElement extends AbstractFormInputElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.#min = Number(this.getAttribute("min")) ?? 0;
|
||||
this.#max = Number(this.getAttribute("max")) ?? 1;
|
||||
this.#step = Number(this.getAttribute("step")) || undefined;
|
||||
this._setValue(Number(this.getAttribute("value"))); // Initialize existing value
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static tagName = "range-picker";
|
||||
|
||||
/**
|
||||
* The range input.
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
#rangeInput;
|
||||
|
||||
/**
|
||||
* The number input.
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
#numberInput;
|
||||
|
||||
/**
|
||||
* The minimum allowed value for the range.
|
||||
* @type {number}
|
||||
*/
|
||||
#min;
|
||||
|
||||
/**
|
||||
* The maximum allowed value for the range.
|
||||
* @type {number}
|
||||
*/
|
||||
#max;
|
||||
|
||||
/**
|
||||
* A required step size for the range.
|
||||
* @type {number}
|
||||
*/
|
||||
#step;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The value of the input element.
|
||||
* @type {number}
|
||||
*/
|
||||
get valueAsNumber() {
|
||||
return this._getValue();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_buildElements() {
|
||||
|
||||
// Create range input element
|
||||
const r = this.#rangeInput = document.createElement("input");
|
||||
r.type = "range";
|
||||
r.min = String(this.#min);
|
||||
r.max = String(this.#max);
|
||||
r.step = String(this.#step ?? 0.1);
|
||||
this._applyInputAttributes(r);
|
||||
|
||||
// Create the number input element
|
||||
const n = this.#numberInput = this._primaryInput = document.createElement("input");
|
||||
n.type = "number";
|
||||
n.min = String(this.#min);
|
||||
n.max = String(this.#max);
|
||||
n.step = this.#step ?? "any";
|
||||
this._applyInputAttributes(n);
|
||||
return [this.#rangeInput, this.#numberInput];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_setValue(value) {
|
||||
value = Math.clamp(value, this.#min, this.#max);
|
||||
if ( this.#step ) value = value.toNearest(this.#step);
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_refresh() {
|
||||
if ( !this.#rangeInput ) return; // Not yet connected
|
||||
this.#rangeInput.valueAsNumber = this.#numberInput.valueAsNumber = this._value;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_activateListeners() {
|
||||
const onChange = this.#onChangeInput.bind(this);
|
||||
this.#rangeInput.addEventListener("input", this.#onDragSlider.bind(this));
|
||||
this.#rangeInput.addEventListener("change", onChange);
|
||||
this.#numberInput.addEventListener("change", onChange);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update display of the number input as the range slider is actively changed.
|
||||
* @param {InputEvent} event The originating input event
|
||||
*/
|
||||
#onDragSlider(event) {
|
||||
event.preventDefault();
|
||||
this.#numberInput.valueAsNumber = this.#rangeInput.valueAsNumber;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle changes to one of the inputs of the range picker element.
|
||||
* @param {InputEvent} event The originating input change event
|
||||
*/
|
||||
#onChangeInput(event) {
|
||||
event.stopPropagation();
|
||||
this.value = event.currentTarget.valueAsNumber;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_toggleDisabled(disabled) {
|
||||
this.#rangeInput.toggleAttribute("disabled", disabled);
|
||||
this.#numberInput.toggleAttribute("disabled", disabled);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a HTMLRangePickerElement using provided configuration data.
|
||||
* @param {FormInputConfig & RangePickerInputConfig} config
|
||||
* @returns {HTMLRangePickerElement}
|
||||
*/
|
||||
static create(config) {
|
||||
const picker = document.createElement(HTMLRangePickerElement.tagName);
|
||||
picker.name = config.name;
|
||||
for ( const attr of ["value", "min", "max", "step"] ) {
|
||||
if ( attr in config ) picker.setAttribute(attr, config[attr]);
|
||||
}
|
||||
foundry.applications.fields.setInputAttributes(picker, config);
|
||||
return picker;
|
||||
}
|
||||
}
|
||||
275
resources/app/client-esm/applications/elements/string-tags.mjs
Normal file
275
resources/app/client-esm/applications/elements/string-tags.mjs
Normal file
@@ -0,0 +1,275 @@
|
||||
import AbstractFormInputElement from "./form-element.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("../forms/fields.mjs").FormInputConfig} FormInputConfig
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} StringTagsInputConfig
|
||||
* @property {boolean} slug Automatically slugify provided strings?
|
||||
*/
|
||||
|
||||
/**
|
||||
* A custom HTML element which allows for arbitrary assignment of a set of string tags.
|
||||
* This element may be used directly or subclassed to impose additional validation or functionality.
|
||||
* @extends {AbstractFormInputElement<Set<string>>}
|
||||
*/
|
||||
export default class HTMLStringTagsElement extends AbstractFormInputElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.#slug = this.hasAttribute("slug");
|
||||
this._value = new Set();
|
||||
this._initializeTags();
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static tagName = "string-tags";
|
||||
|
||||
static icons = {
|
||||
add: "fa-solid fa-tag",
|
||||
remove: "fa-solid fa-times"
|
||||
}
|
||||
|
||||
static labels = {
|
||||
add: "ELEMENTS.TAGS.Add",
|
||||
remove: "ELEMENTS.TAGS.Remove",
|
||||
placeholder: ""
|
||||
}
|
||||
|
||||
/**
|
||||
* The button element to add a new tag.
|
||||
* @type {HTMLButtonElement}
|
||||
*/
|
||||
#button;
|
||||
|
||||
/**
|
||||
* The input element to enter a new tag.
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
#input;
|
||||
|
||||
/**
|
||||
* The tags list of assigned tags.
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
#tags;
|
||||
|
||||
/**
|
||||
* Automatically slugify all strings provided to the element?
|
||||
* @type {boolean}
|
||||
*/
|
||||
#slug;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Initialize innerText or an initial value attribute of the element as a comma-separated list of currently assigned
|
||||
* string tags.
|
||||
* @protected
|
||||
*/
|
||||
_initializeTags() {
|
||||
const initial = this.getAttribute("value") || this.innerText || "";
|
||||
const tags = initial ? initial.split(",") : [];
|
||||
for ( let tag of tags ) {
|
||||
tag = tag.trim();
|
||||
if ( tag ) {
|
||||
if ( this.#slug ) tag = tag.slugify({strict: true});
|
||||
try {
|
||||
this._validateTag(tag);
|
||||
} catch ( err ) {
|
||||
console.warn(err.message);
|
||||
continue;
|
||||
}
|
||||
this._value.add(tag);
|
||||
}
|
||||
}
|
||||
this.innerText = "";
|
||||
this.removeAttribute("value");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Subclasses may impose more strict validation on what tags are allowed.
|
||||
* @param {string} tag A candidate tag
|
||||
* @throws {Error} An error if the candidate tag is not allowed
|
||||
* @protected
|
||||
*/
|
||||
_validateTag(tag) {
|
||||
if ( !tag ) throw new Error(game.i18n.localize("ELEMENTS.TAGS.ErrorBlank"));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_buildElements() {
|
||||
|
||||
// Create tags list
|
||||
const tags = document.createElement("div");
|
||||
tags.className = "tags input-element-tags";
|
||||
this.#tags = tags;
|
||||
|
||||
// Create input element
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.placeholder = game.i18n.localize(this.constructor.labels.placeholder);
|
||||
this.#input = this._primaryInput = input;
|
||||
|
||||
// Create button
|
||||
const button = document.createElement("button");
|
||||
button.type = "button"
|
||||
button.className = `icon ${this.constructor.icons.add}`;
|
||||
button.dataset.tooltip = this.constructor.labels.add;
|
||||
button.ariaLabel = game.i18n.localize(this.constructor.labels.add);
|
||||
this.#button = button;
|
||||
return [this.#tags, this.#input, this.#button];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_refresh() {
|
||||
const tags = this.value.map(tag => this.constructor.renderTag(tag, tag, this.editable));
|
||||
this.#tags.replaceChildren(...tags);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Render the tagged string as an HTML element.
|
||||
* @param {string} tag The raw tag value
|
||||
* @param {string} [label] An optional tag label
|
||||
* @param {boolean} [editable=true] Is the tag editable?
|
||||
* @returns {HTMLDivElement} A rendered HTML element for the tag
|
||||
*/
|
||||
static renderTag(tag, label, editable=true) {
|
||||
const div = document.createElement("div");
|
||||
div.className = "tag";
|
||||
div.dataset.key = tag;
|
||||
const span = document.createElement("span");
|
||||
span.textContent = label ?? tag;
|
||||
div.append(span);
|
||||
if ( editable ) {
|
||||
const t = game.i18n.localize(this.labels.remove);
|
||||
const a = `<a class="remove ${this.icons.remove}" data-tooltip="${t}" aria-label="${t}"></a>`;
|
||||
div.insertAdjacentHTML("beforeend", a);
|
||||
}
|
||||
return div;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_activateListeners() {
|
||||
this.#button.addEventListener("click", this.#addTag.bind(this));
|
||||
this.#tags.addEventListener("click", this.#onClickTag.bind(this));
|
||||
this.#input.addEventListener("keydown", this.#onKeydown.bind(this));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Remove a tag from the set when its removal button is clicked.
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
#onClickTag(event) {
|
||||
if ( !event.target.classList.contains("remove") ) return;
|
||||
const tag = event.target.closest(".tag");
|
||||
this._value.delete(tag.dataset.key);
|
||||
this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add a tag to the set when the ENTER key is pressed in the text input.
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
#onKeydown(event) {
|
||||
if ( event.key !== "Enter" ) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.#addTag();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add a new tag to the set upon user input.
|
||||
*/
|
||||
#addTag() {
|
||||
let tag = this.#input.value.trim();
|
||||
if ( this.#slug ) tag = tag.slugify({strict: true});
|
||||
|
||||
// Validate the proposed code
|
||||
try {
|
||||
this._validateTag(tag);
|
||||
} catch(err) {
|
||||
ui.notifications.error(err.message);
|
||||
this.#input.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure uniqueness
|
||||
if ( this._value.has(tag) ) {
|
||||
const message = game.i18n.format("ELEMENTS.TAGS.ErrorNonUnique", {tag});
|
||||
ui.notifications.error(message);
|
||||
this.#input.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Add hex
|
||||
this._value.add(tag);
|
||||
this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
|
||||
this.#input.value = "";
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Form Handling */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getValue() {
|
||||
return Array.from(this._value);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_setValue(value) {
|
||||
this._value.clear();
|
||||
const toAdd = [];
|
||||
for ( let v of value ) {
|
||||
if ( this.#slug ) v = v.slugify({strict: true});
|
||||
this._validateTag(v);
|
||||
toAdd.push(v);
|
||||
}
|
||||
for ( const v of toAdd ) this._value.add(v);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_toggleDisabled(disabled) {
|
||||
this.#input.toggleAttribute("disabled", disabled);
|
||||
this.#button.toggleAttribute("disabled", disabled);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a HTMLStringTagsElement using provided configuration data.
|
||||
* @param {FormInputConfig & StringTagsInputConfig} config
|
||||
*/
|
||||
static create(config) {
|
||||
const tags = document.createElement(this.tagName);
|
||||
tags.name = config.name;
|
||||
const value = Array.from(config.value || []).join(",");
|
||||
tags.toggleAttribute("slug", !!config.slug)
|
||||
tags.setAttribute("value", value);
|
||||
foundry.applications.fields.setInputAttributes(tags, config);
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user