/** * @callback CustomFormGroup * @param {DataField} field * @param {FormGroupConfig} groupConfig * @param {FormInputConfig} inputConfig * @returns {HTMLDivElement} */ /** * @callback CustomFormInput * @param {DataField} field * @param {FormInputConfig} config * @returns {HTMLElement|HTMLCollection} */ /** * @typedef {Object} FormGroupConfig * @property {string} label A text label to apply to the form group * @property {string} [units] An optional units string which is appended to the label * @property {HTMLElement|HTMLCollection} input An HTML element or collection of elements which provide the inputs * for the group * @property {string} [hint] Hint text displayed as part of the form group * @property {string} [rootId] Some parent CSS id within which field names are unique. If provided, * this root ID is used to automatically assign "id" attributes to input * elements and "for" attributes to corresponding labels * @property {string[]} [classes] An array of CSS classes applied to the form group element * @property {boolean} [stacked=false] Is the "stacked" class applied to the form group * @property {boolean} [localize=false] Should labels or other elements within this form group be * automatically localized? * @property {CustomFormGroup} [widget] A custom form group widget function which replaces the default * group HTML generation */ /** * @template FormInputValue * @typedef {Object} FormInputConfig * @property {string} name The name of the form element * @property {FormInputValue} [value] The current value of the form element * @property {boolean} [required=false] Is the field required? * @property {boolean} [disabled=false] Is the field disabled? * @property {boolean} [readonly=false] Is the field readonly? * @property {boolean} [autofocus=false] Is the field autofocused? * @property {boolean} [localize=false] Localize values of this field? * @property {Record} [dataset] Additional dataset attributes to assign to the input * @property {string} [placeholder] A placeholder value, if supported by the element type * @property {CustomFormInput} [input] */ /** * Create a standardized form field group. * @param {FormGroupConfig} config * @returns {HTMLDivElement} */ export function createFormGroup(config) { let {classes, hint, label, input, rootId, stacked, localize, units} = config; classes ||= []; if ( stacked ) classes.unshift("stacked"); classes.unshift("form-group"); // Assign identifiers to each input input = input instanceof HTMLCollection ? input : [input]; let labelFor; if ( rootId ) { for ( const [i, el] of input.entries() ) { const id = [rootId, el.name, input.length > 1 ? i : ""].filterJoin("-"); labelFor ||= id; el.setAttribute("id", id); } } // Create the group element const group = document.createElement("div"); group.className = classes.join(" "); // Label element const lbl = document.createElement("label"); lbl.innerText = localize ? game.i18n.localize(label) : label; if ( labelFor ) lbl.setAttribute("for", labelFor); if ( units ) lbl.insertAdjacentHTML("beforeend", ` (${game.i18n.localize(units)})`); group.prepend(lbl); // Form fields and inputs const fields = document.createElement("div"); fields.className = "form-fields"; fields.append(...input); group.append(fields); // Hint element if ( hint ) { const h = document.createElement("p"); h.className = "hint"; h.innerText = localize ? game.i18n.localize(hint) : hint; group.append(h); } return group; } /* ---------------------------------------- */ /** * Create an `` element for a BooleanField. * @param {FormInputConfig} config * @returns {HTMLInputElement} */ export function createCheckboxInput(config) { const input = document.createElement("input"); input.type = "checkbox"; input.name = config.name; if ( config.value ) input.setAttribute("checked", ""); setInputAttributes(input, config); return input; } /* ---------------------------------------- */ /** * @typedef {Object} EditorInputConfig * @property {string} [engine="prosemirror"] * @property {number} [height] * @property {boolean} [editable=true] * @property {boolean} [button=false] * @property {boolean} [collaborate=false] */ /** * Create a `
` element for a StringField. * @param {FormInputConfig & EditorInputConfig} config * @returns {HTMLDivElement} */ export function createEditorInput(config) { const {engine="prosemirror", editable=true, button=false, collaborate=false, height} = config; const editor = document.createElement("div"); editor.className = "editor"; if ( height !== undefined ) editor.style.height = `${height}px`; // Dataset attributes let dataset = { engine, collaborate }; if ( editable ) dataset.edit = config.name; dataset = Object.entries(dataset).map(([k, v]) => `data-${k}="${v}"`).join(" "); // Editor HTML let editorHTML = ""; if ( button && editable ) editorHTML += ''; editorHTML += `
${config.value ?? ""}
`; editor.innerHTML = editorHTML; return editor; } /* ---------------------------------------- */ /** * Create a `` element for a StringField. * @param {FormInputConfig & Omit} config * @returns {HTMLSelectElement} */ export function createMultiSelectInput(config) { const tagName = config.type === "checkboxes" ? "multi-checkbox" : "multi-select"; const select = document.createElement(tagName); select.name = config.name; setInputAttributes(select, config); const groups = prepareSelectOptionGroups(config); for ( const g of groups ) { let parent = select; if ( g.group ) parent = _appendOptgroup(g.group, select); for ( const o of g.options ) _appendOption(o, parent); } return select; } /* ---------------------------------------- */ /** * @typedef {Object} NumberInputConfig * @property {number} min * @property {number} max * @property {number|"any"} step * @property {"range"|"number"} [type] */ /** * Create an `` element for a NumberField. * @param {FormInputConfig & NumberInputConfig} config * @returns {HTMLInputElement} */ export function createNumberInput(config) { const input = document.createElement("input"); input.type = "number"; if ( config.name ) input.name = config.name; // Assign value let step = typeof config.step === "number" ? config.step : "any"; let value = config.value; if ( Number.isNumeric(value) ) { if ( typeof config.step === "number" ) value = value.toNearest(config.step); input.setAttribute("value", String(value)); } else input.setAttribute("value", ""); // Min, max, and step size if ( typeof config.min === "number" ) input.setAttribute("min", String(config.min)); if ( typeof config.max === "number" ) input.setAttribute("max", String(config.max)); input.setAttribute("step", String(step)); setInputAttributes(input, config); return input; } /* ---------------------------------------- */ /** * @typedef {Object} FormSelectOption * @property {string} [value] * @property {string} [label] * @property {string} [group] * @property {boolean} [disabled] * @property {boolean} [selected] * @property {boolean} [rule] */ /** * @typedef {Object} SelectInputConfig * @property {FormSelectOption[]} options * @property {string[]} [groups] An option to control the order and display of optgroup elements. The order of * strings defines the displayed order of optgroup elements. * A blank string may be used to define the position of ungrouped options. * If not defined, the order of groups corresponds to the order of options. * @property {string} [blank] * @property {string} [valueAttr] An alternative value key of the object passed to the options array * @property {string} [labelAttr] An alternative label key of the object passed to the options array * @property {boolean} [localize=false] Localize value labels * @property {boolean} [sort=false] Sort options alphabetically by label within groups * @property {"single"|"multi"|"checkboxes"} [type] Customize the type of select that is created */ /** * Create a `