diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index 0f7e442..7c3ad5f 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/README.md b/README.md index 25b2d31..d4a2a93 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This is a Web Component for generating input forms from [OGraf/GDD](https://ogra npm install ograf-form ``` -Or you can use a CDN: `https://cdn.jsdelivr.net/npm/ograf-form/dist/main.js);` +Or you can use a CDN: `https://cdn.jsdelivr.net/npm/ograf-form);` ### Example usage @@ -26,7 +26,7 @@ Or you can use a CDN: `https://cdn.jsdelivr.net/npm/ograf-form/dist/main.js);` @@ -42,15 +42,15 @@ Or you can use a CDN: `https://cdn.jsdelivr.net/npm/ograf-form/dist/main.js);` // Listen to changes: const form = document.getElementById("ograf-form"); const dataDiv = document.getElementById("data"); - form.addEventListener("onChange", (e) => { - console.log("Caught onChange event", e.detail.data); - // The onChange event is fired when a user changes a value in the form + form.addEventListener("change", (e) => { + console.log("Caught change event", e.target.value); + // The change event is fired when a user changes a value in the form // It does NOT fire on each key stroke. // This is a good time to update our data object: - dataDiv.innerHTML = JSON.stringify(e.detail.data, null, 2); + dataDiv.innerHTML = JSON.stringify(e.target.value, null, 2); }); - form.addEventListener("onKeyUp", (e) => { - console.log("Caught onKeyUp event", e.detail.data); + form.addEventListener("keyup", (e) => { + console.log("Caught keyup event", e.target.value); }); @@ -75,3 +75,8 @@ yarn release ``` + +## TODO + +- Support `patternProperties`, `additionalProperties`, `unevaluatedProperties` +- Support tuples (`prefixItems` / `items` as array) & `additionalItems` diff --git a/examples/html.html b/examples/html.html index 471be0a..8b453a8 100644 --- a/examples/html.html +++ b/examples/html.html @@ -1,11 +1,11 @@ - - + src="https://cdn.jsdelivr.net/npm/ograf-form" + > --> +
{ - console.log("Caught onChange event", e.detail.data); - // The onChange event is fired when a user changes a value in the form + form.addEventListener("change", (e) => { + console.log("Caught change event", e.target.value); + // The change event is fired when a user changes a value in the form // It does NOT fire on each key stroke. // This is a good time to update our data object: - dataDiv.innerHTML = JSON.stringify(e.detail.data, null, 2); + dataDiv.innerHTML = JSON.stringify(e.target.value, null, 2); }); - form.addEventListener("onKeyUp", (e) => { - console.log("Caught onKeyUp event", e.detail.data); + form.addEventListener("keyup", (e) => { + console.log("Caught keyup event", e.target.value); }); diff --git a/examples/javascript.html b/examples/javascript.html index 6e1ba42..a860b5d 100644 --- a/examples/javascript.html +++ b/examples/javascript.html @@ -9,7 +9,8 @@ const dataDiv = document.getElementById("data"); // Load the library, either from CDN or locally: - const OgrafForm = await import("https://cdn.jsdelivr.net/npm/ograf-form/dist/main.js"); + // const OgrafForm = await import("https://cdn.jsdelivr.net/npm/ograf-form"); + const OgrafForm = await import("/dist/main.js"); // const OgrafForm = await import("ograf-form"); // Define a OGraf/GDD JSON-schema: @@ -30,20 +31,20 @@ // Initialize the Form: const form = new OgrafForm.SuperFlyTvOgrafDataForm(); - form.addEventListener("onChange", (e) => { - console.log("Caught onChange event", JSON.stringify(e.detail)); - // The onChange event is fired when a user changes a value in the form + form.addEventListener("change", (e) => { + console.log("Caught change event", e.target.value, e.detail); + // The change event is fired when a user changes a value in the form // It does NOT fire on each key stroke. // This is a good time to update our data object: - dataDiv.innerHTML = JSON.stringify(e.detail.data, null, 2); + dataDiv.innerHTML = JSON.stringify(e.target.value, null, 2); }); - form.addEventListener("onKeyUp", (e) => { - console.log("Caught onKey event", JSON.stringify(e.detail)); - // The onKeyUp event is fired on each key stroke in the form. - // e.detail.data contains the current data. + form.addEventListener("keyup", (e) => { + console.log("Caught keyup event", e.target.value, e.detail); + // The keyup event is fired on each key stroke in the form. + // e.target.value contains the current data. }); form.schema = exampleSchema; - form.data = data; + form.value = data; dataDiv.innerHTML = JSON.stringify(data, null, 2); diff --git a/examples/react.jsx b/examples/react.jsx index a348377..e18bf60 100644 --- a/examples/react.jsx +++ b/examples/react.jsx @@ -19,17 +19,17 @@ export function OGrafForm({ schema, onDataChangeCallback }) { // Set up listener for when the data has changed in the form: React.useLayoutEffect(() => { if (formRef.current) { - const listener = (e) => onDataChange(e.detail.data); - formRef.current.addEventListener("onChange", listener); - return () => formRef.current.removeEventListener("onChange", listener); + const listener = (e) => onDataChange(e.target.value); + formRef.current.addEventListener("change", listener); + return () => formRef.current.removeEventListener("change", listener); } }); // (Optional) Set up listener for whenever user is editing (ie every key) // React.useLayoutEffect(() => { // if (formRef.current) { - // const listener = (e) => onDataChange(e.detail.data); - // formRef.current.addEventListener("onKeyUp", listener); - // return () => formRef.current.removeEventListener("onKeyUp", listener); + // const listener = (e) => onDataChange(e.target.value); + // formRef.current.addEventListener("keyup", listener); + // return () => formRef.current.removeEventListener("keyup", listener); // } // }); @@ -38,7 +38,7 @@ export function OGrafForm({ schema, onDataChangeCallback }) {
); diff --git a/lib/components/gdd-elements/gdd-elements/color-rrggbb.ts b/lib/components/gdd-elements/gdd-elements/color-rrggbb.ts index 4bc9084..d16ffb7 100644 --- a/lib/components/gdd-elements/gdd-elements/color-rrggbb.ts +++ b/lib/components/gdd-elements/gdd-elements/color-rrggbb.ts @@ -12,11 +12,11 @@ export class GDDColorRRGGBB extends GDDInputBase { this.elInput.onchange = (e) => { if (!e.target) return; - this.emitOnChange((e.target as any).value); + this.emitChangeEvent((e.target as any).value); }; } - this.elInput.value = this.data || ""; + this.elInput.value = this.value || ""; this._renderStyle(); return initialRender; } diff --git a/lib/components/gdd-elements/gdd-elements/color-rrggbbaa.ts b/lib/components/gdd-elements/gdd-elements/color-rrggbbaa.ts index b337147..48f4042 100644 --- a/lib/components/gdd-elements/gdd-elements/color-rrggbbaa.ts +++ b/lib/components/gdd-elements/gdd-elements/color-rrggbbaa.ts @@ -24,24 +24,30 @@ export class GDDColorRRGGBBAA extends GDDColorRRGGBB { this.elContentAlpha.onchange = (e) => { if (!e.target) return; if (!(e.target instanceof HTMLInputElement)) return; + e.stopPropagation(); const int = this._parseInput(e.target.value); if (int === null) { // invalid input, revert e.target.value = `${this._alpha}` || ""; } else { - if (this.data !== int) { + if (this.value !== int) { this._alpha = int; - this.emitOnChange(this.getRRGGBBAA(this._rrggbb, this._alpha)); + this.emitChangeEvent(this.getRRGGBBAA(this._rrggbb, this._alpha)); } e.target.value = `${int}`; } }; this.elContentAlpha.onkeydown = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + // Prevent form submission + } if (!e.target) return; - this.emitOnKeyDown( + + this.emitKeyDownEvent( e, (e.target as any).value, (e.target as any).value @@ -50,11 +56,12 @@ export class GDDColorRRGGBBAA extends GDDColorRRGGBB { this.elContentAlpha.onkeyup = (e) => { if (!e.target) return; if (!(e.target instanceof HTMLInputElement)) return; + e.stopPropagation(); const int = this._parseInput(e.target.value); if (int === null) return; - this.emitOnKeyUp( + this.emitKeyUpEvent( e, e.target.value, this.getRRGGBBAA(this._rrggbb, int) @@ -66,13 +73,13 @@ export class GDDColorRRGGBBAA extends GDDColorRRGGBB { this._rrggbb = (e.target as any).value.replace("#", ""); - this.emitOnChange(this.getRRGGBBAA(this._rrggbb, this._alpha)); + this.emitChangeEvent(this.getRRGGBBAA(this._rrggbb, this._alpha)); }; } if (!this.elContentAlpha) return initialRender; - const rrggbbaa = this.data || "#00000000"; + const rrggbbaa = this.value || "#00000000"; this._rrggbb = rrggbbaa.slice(1, 7); this._alpha = parseInt(rrggbbaa.slice(7, 9), 16) / 255; diff --git a/lib/components/gdd-elements/gdd-elements/file-path.ts b/lib/components/gdd-elements/gdd-elements/file-path.ts index 387e1b7..bafd927 100644 --- a/lib/components/gdd-elements/gdd-elements/file-path.ts +++ b/lib/components/gdd-elements/gdd-elements/file-path.ts @@ -15,24 +15,9 @@ export class GDDFilePath extends GDDInputBase { this.elInput.accept = schema.gddOptions.extensions.map((e) => `.${e}`).join(",") || "*"; this.elInput.name = this.path; - - // this.elInput.style.fontFamily = "monospace"; - - this.elInput.onchange = (e) => { - if (!e.target) return; - this.emitOnChange((e.target as any).value); - }; - this.elInput.onkeydown = (e) => { - if (!e.target) return; - this.emitOnKeyDown(e, (e.target as any).value, (e.target as any).value); - }; - this.elInput.onkeyup = (e) => { - if (!e.target) return; - this.emitOnKeyUp(e, (e.target as any).value, (e.target as any).value); - }; } - this.elInput.value = this.data || ""; + this.elInput.value = this.value || ""; this._renderStyle(); return initialRender; diff --git a/lib/components/gdd-elements/gdd-elements/multiline-text.ts b/lib/components/gdd-elements/gdd-elements/multiline-text.ts index 959b5ef..0af8f9f 100644 --- a/lib/components/gdd-elements/gdd-elements/multiline-text.ts +++ b/lib/components/gdd-elements/gdd-elements/multiline-text.ts @@ -12,23 +12,26 @@ export class GDDMultiLineText extends GDDElementBase { this.elInput.onchange = (e) => { if (!e.target) return; - this.emitOnChange((e.target as any).value); + if (!(e.target instanceof HTMLTextAreaElement)) return; + this.emitChangeEvent(e.target.value); this._renderStyle(); }; this.elInput.onkeydown = (e) => { if (!e.target) return; - this.emitOnKeyDown(e, (e.target as any).value, (e.target as any).value); + if (!(e.target instanceof HTMLTextAreaElement)) return; + this.emitKeyDownEvent(e, e.target.value, e.target.value); }; this.elInput.onkeyup = (e) => { if (!e.target) return; - this.emitOnKeyUp(e, (e.target as any).value, (e.target as any).value); + if (!(e.target instanceof HTMLTextAreaElement)) return; + this.emitKeyUpEvent(e, e.target.value, e.target.value); this._renderStyle(); }; } this.elInput.name = this.path; - this.elInput.value = this.data || ""; + this.elInput.value = this.value || ""; this._renderStyle(); return initialRender; diff --git a/lib/components/gdd-elements/gdd-elements/percentage.ts b/lib/components/gdd-elements/gdd-elements/percentage.ts index 29f6d7b..5d8bbd6 100644 --- a/lib/components/gdd-elements/gdd-elements/percentage.ts +++ b/lib/components/gdd-elements/gdd-elements/percentage.ts @@ -20,6 +20,10 @@ export class GDDPercentage extends GDDInputNumberBase { this.elInput.type = "text"; const orgOnKeyDown = this.elInput.onkeydown; this.elInput.onkeydown = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + // Prevent form submission + } if (!this.elInput) return; if (e.key === "ArrowUp" || e.key === "ArrowDown") { @@ -75,7 +79,7 @@ export class GDDPercentage extends GDDInputNumberBase { e.preventDefault(); if (this._isChanged) { - // because onChange is not emitted if we've only modified using arrow keys: + // because change is not emitted if we've only modified using arrow keys: this.elInput?.onchange?.(e); } this._isChanged = false; diff --git a/lib/components/gdd-elements/gdd-elements/select.ts b/lib/components/gdd-elements/gdd-elements/select.ts index 3c6c57c..07630e8 100644 --- a/lib/components/gdd-elements/gdd-elements/select.ts +++ b/lib/components/gdd-elements/gdd-elements/select.ts @@ -28,7 +28,7 @@ export class GDDSelect extends GDDElementBase { const option = this._options[(e.target as any).value]; if (option) { - this.emitOnChange(option.value); + this.emitChangeEvent(option.value); } }; } @@ -69,7 +69,7 @@ export class GDDSelect extends GDDElementBase { if (!(elOption instanceof HTMLOptionElement)) continue; const option = this._options[elOption.getAttribute("value") || ""]; if (!option) continue; - elOption.selected = isEqual(this.data, option.value); + elOption.selected = isEqual(this.value, option.value); } this._renderStyle(); diff --git a/lib/components/gdd-elements/index.ts b/lib/components/gdd-elements/index.ts index 72a8f98..69ea5f1 100644 --- a/lib/components/gdd-elements/index.ts +++ b/lib/components/gdd-elements/index.ts @@ -1,67 +1,16 @@ -import { GDDElementBase } from "./lib/_base.js"; -import { GDDArray } from "./json-elements/array.js"; -import { GDDBoolean } from "./json-elements/boolean.js"; -import { GDDInteger } from "./json-elements/integer.js"; -import { GDDObject } from "./json-elements/object.js"; -import { GDDString } from "./json-elements/string.js"; -import { GDDMultiLineText } from "./gdd-elements/multiline-text.js"; -import { GDDFilePath } from "./gdd-elements/file-path.js"; -import { GDDSelect } from "./gdd-elements/select.js"; -import { GDDColorRRGGBB } from "./gdd-elements/color-rrggbb.js"; -import { GDDColorRRGGBBAA } from "./gdd-elements/color-rrggbbaa.js"; -import { GDDPercentage } from "./gdd-elements/percentage.js"; -import { assertNever } from "../../lib/lib.js"; -import type { GDDSchema, GDDTypeName } from "../../lib/types.js"; -import { GDDNumber } from "./json-elements/number.js"; +export { getBasicType } from "./lib/lib.js"; -export { - GDDElementBase, - GDDObject, - GDDArray, - GDDString, - GDDInteger, - GDDBoolean, - GDDNumber, -}; +export { GDDElementBase } from "./lib/_base.js"; +export { GDDArray } from "./json-elements/array.js"; +export { GDDBoolean } from "./json-elements/boolean.js"; +export { GDDInteger } from "./json-elements/integer.js"; +export { GDDObject } from "./json-elements/object.js"; +export { GDDString } from "./json-elements/string.js"; +export { GDDNumber } from "./json-elements/number.js"; -export function getGDDElement(schema: GDDSchema, path: string): GDDElementBase { - const basicType = getBasicType(schema.type); - - const gddType = schema.gddType + "/"; - - let element: GDDElementBase; - - if (basicType === "string" && gddType.startsWith("multi-line(")) - element = new GDDMultiLineText(); - else if (basicType === "string" && gddType.startsWith("file-path/")) - element = new GDDFilePath(); - else if ( - (basicType === "string" || - basicType === "integer" || - basicType === "number") && - gddType.startsWith("select/") - ) - element = new GDDSelect(); - else if (basicType === "string" && gddType.startsWith("color-rrggbb/")) - element = new GDDColorRRGGBB(); - else if (basicType === "string" && gddType.startsWith("color-rrggbbaa/")) - element = new GDDColorRRGGBBAA(); - else if (basicType === "number" && gddType.startsWith("percentage/")) - element = new GDDPercentage(); - else if (basicType === "object") element = new GDDObject(); - else if (basicType === "array") element = new GDDArray(); - else if (basicType === "string") element = new GDDString(); - else if (basicType === "integer") element = new GDDInteger(); - else if (basicType === "boolean") element = new GDDBoolean(); - else if (basicType === "number") element = new GDDNumber(); - else { - assertNever(basicType); - throw new Error(`Unsupported GDD type: ${basicType}`); - } - - element.path = path; - return element; -} -export function getBasicType(schemaType: GDDSchema["type"]): GDDTypeName { - return Array.isArray(schemaType) ? schemaType[0] : schemaType; -} +export { GDDColorRRGGBB } from "./gdd-elements/color-rrggbb.js"; +export { GDDColorRRGGBBAA } from "./gdd-elements/color-rrggbbaa.js"; +export { GDDFilePath } from "./gdd-elements/file-path.js"; +export { GDDMultiLineText } from "./gdd-elements/multiline-text.js"; +export { GDDPercentage } from "./gdd-elements/percentage.js"; +export { GDDSelect } from "./gdd-elements/select.js"; diff --git a/lib/components/gdd-elements/json-elements/_inputBase.ts b/lib/components/gdd-elements/json-elements/_inputBase.ts index 09f5026..ab03614 100644 --- a/lib/components/gdd-elements/json-elements/_inputBase.ts +++ b/lib/components/gdd-elements/json-elements/_inputBase.ts @@ -10,11 +10,28 @@ export abstract class GDDInputBase extends GDDElementBase { this.elInput = document.createElement("input"); this.appendChild(this.elInput); + this.elInput.name = this.path; + this.elInput.onchange = (e) => { + if (!e.target) return; + if (!(e.target instanceof HTMLInputElement)) return; + e.stopPropagation(); + this.emitChangeEvent(e.target.value); + }; this.elInput.onkeydown = (e) => { if (e.key === "Enter") { e.preventDefault(); // Prevent form submission } + if (!e.target) return; + if (!(e.target instanceof HTMLInputElement)) return; + e.stopPropagation(); + this.emitKeyDownEvent(e, e.target.value, e.target.value); + }; + this.elInput.onkeyup = (e) => { + if (!e.target) return; + if (!(e.target instanceof HTMLInputElement)) return; + e.stopPropagation(); + this.emitKeyUpEvent(e, e.target.value, e.target.value); }; } @@ -26,4 +43,12 @@ export abstract class GDDInputBase extends GDDElementBase { } return initialRender; } + destroy(): void { + super.destroy(); + if (this.elInput) { + this.elInput.onchange = null; + this.elInput.onkeydown = null; + this.elInput.onkeyup = null; + } + } } diff --git a/lib/components/gdd-elements/json-elements/_inputNumberBase.ts b/lib/components/gdd-elements/json-elements/_inputNumberBase.ts index fece6ab..201de59 100644 --- a/lib/components/gdd-elements/json-elements/_inputNumberBase.ts +++ b/lib/components/gdd-elements/json-elements/_inputNumberBase.ts @@ -13,38 +13,43 @@ export abstract class GDDInputNumberBase extends GDDInputBase { this.elInput.onchange = (e) => { if (!e.target) return; if (!(e.target instanceof HTMLInputElement)) return; + e.stopPropagation(); const input = e.target.value; const value = this._parseInput(input); if (value === null) { // invalid input, revert - e.target.value = this._stringifyValue(this.data); + e.target.value = this._stringifyValue(this.value); } else { - if (this.data !== value) { - this.data = value; - this.emitOnChange(this.data); + if (this.value !== value) { + this.value = value; + this.emitChangeEvent(this.value); } e.target.value = this._stringifyValue(value); } }; this.elInput.onkeydown = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + // Prevent form submission + } if (!e.target) return; - if (!(e.target instanceof HTMLInputElement)) return; + if (!(e.target instanceof HTMLInputElement)) return; + e.stopPropagation(); const input = e.target.value; - - this.emitOnKeyDown(e, input, this._parseInput(input)); + this.emitKeyDownEvent(e, input, this._parseInput(input)); }; this.elInput.onkeyup = (e) => { if (!e.target) return; if (!(e.target instanceof HTMLInputElement)) return; + e.stopPropagation(); const input = e.target.value; - - this.emitOnKeyUp(e, input, this._parseInput(input)); + this.emitKeyUpEvent(e, input, this._parseInput(input)); }; } - this.elInput.value = this._stringifyValue(this.data); + this.elInput.value = this._stringifyValue(this.value); return initialRender; } diff --git a/lib/components/gdd-elements/json-elements/array.ts b/lib/components/gdd-elements/json-elements/array.ts index b3189e4..78127c0 100644 --- a/lib/components/gdd-elements/json-elements/array.ts +++ b/lib/components/gdd-elements/json-elements/array.ts @@ -1,7 +1,6 @@ import { getDefaultDataFromSchema } from "../../../lib/default-data.js"; -import { last } from "../../../lib/lib.js"; -import { GDDElementBase, isCustomEvent } from "../lib/_base.js"; -import { getGDDElement } from "../index.js"; +import { nthInIterable } from "../../../lib/lib.js"; +import { GDDElementBase, isCustomEventKey } from "../lib/_base.js"; import { renderContentError } from "../lib/lib.js"; export class GDDArray extends GDDElementBase { @@ -14,14 +13,20 @@ export class GDDArray extends GDDElementBase { if (!this.elContent) { initialRender = true; if (this.schema.items.type === "object" && this.schema.items.properties) { - this.elContent = new GDDTable(); + this.elContent = new GDDTable({ + path: this.path, + getGDDElement: this.getGDDElement, + }); } else { - this.elContent = new GDDSimpleArray(); + this.elContent = new GDDSimpleArray({ + path: this.path, + getGDDElement: this.getGDDElement, + }); } this.appendChild(this.elContent); } this.elContent.update({ - data: this.data, + value: this.value, schema: this.schema, renderOptions: this.renderOptions, }); @@ -34,24 +39,28 @@ export class GDDSimpleArray extends GDDElementBase { itemsContainer: HTMLDivElement; items: { container: HTMLDivElement; - element: GDDElementBase; + element: GDDArrayProperty; removeButton: HTMLButtonElement; + index: number; }[]; } | null = null; - private addItem() { + private addItem(index = -1): number | undefined { if (!this.schema?.items) return; const schemaItems = this.schema.items; const defaultData = getDefaultDataFromSchema(schemaItems); - this.data.push(defaultData); + if (index < 0) index = this.value.length - index; + + this.value.splice(index, 0, defaultData); this.render(); - this.emitOnChange(this.data); + this.emitChangeEvent(this.value); + return index; } private removeItem(e: Event, index: number) { - this.data.splice(index, 1); - this.emitOnKeyUp(e, "", this.data); - this.emitOnChange(this.data); + this.value.splice(index, 1); + this.emitKeyUpEvent(e, "", this.value); + this.emitChangeEvent(this.value); this.render(); } @@ -61,7 +70,7 @@ export class GDDSimpleArray extends GDDElementBase { if (!this.schema.items) return false; const schemaItems = this.schema.items; - if (!this.data) this.data = []; + if (!this.value) this.value = []; if (!this.content) { initialRender = true; @@ -80,91 +89,107 @@ export class GDDSimpleArray extends GDDElementBase { this.appendChild(this.content.addButton); } - for (const item of this.content.items.splice(this.data.length)) { + // Remove extra items + for (const item of this.content.items.splice(this.value.length)) { + item.element.destroy(); this.content.itemsContainer.removeChild(item.container); } - for (let i = 0; i < this.data.length; i++) { + for (let i = 0; i < this.value.length; i++) { + const itemPath = `${this.path}[${i}]`; if (!this.content.items[i]) { const container = document.createElement("div"); container.className = "gdd-simple-array-item"; const removeButton = document.createElement("button"); - removeButton.tabIndex = -1; + const element = new GDDArrayProperty({ + path: itemPath, + getGDDElement: this.getGDDElement, + }); - removeButton.onclick = (e: Event) => { - e.preventDefault(); - this.removeItem(e, i); + const contentItem = { + container, + removeButton, + element, + index: i, }; + this.content.items.push(contentItem); - const element = new GDDArrayProperty(); - element.path = `${this.path}[${i}]`; - - element.addEventListener("onChange", (e: Event) => { + contentItem.element.addListener("change", (e: Event) => { if (!(e instanceof CustomEvent)) return; e.stopPropagation(); - this.data[i] = e.detail.data; - this.emitOnChange(this.data); + this.value[contentItem.index] = e.detail.value; + + this.emitChangeEvent(this.value); }); - element.addEventListener("onKeyUp", (e: Event) => { - if (!isCustomEvent(e)) return; + contentItem.element.addListener("keydown", (e: Event) => { + if (!isCustomEventKey(e)) return; + keyDownValueStr = e.detail.valueStr; + }); + contentItem.element.addListener("keyup", (e: Event) => { + if (!isCustomEventKey(e)) return; if (e.detail.key === "Enter") { // Add another item - this.addItem(); - last( - this.content?.itemsContainer.querySelectorAll( - "input, textarea" - ) - )?.focus(); - // } else if (e.detail.key === "Backspace" && e.detail.inputStr === "") { - // // Remove the item - // this.removeItem(e, i); - - // last( - // this.content?.itemsContainer.querySelectorAll( - // "input, textarea" - // ) - // )?.focus(); - } else { e.stopPropagation(); - this.data[i] = e.detail.data; - this.emitOnKeyUp(e, "", this.data); - } - }); - element.addEventListener("onKeyDown", (e: Event) => { - if (!isCustomEvent(e)) return; - if (e.detail.key === "Backspace" && e.detail.inputStr === "") { + const newIndex = this.addItem(contentItem.index + 1); + + if (newIndex !== undefined) { + nthInIterable( + this.content?.itemsContainer.querySelectorAll( + "input, textarea" + ), + newIndex + )?.focus(); + } + } else if ( + e.detail.key === "Backspace" && + e.detail.valueStr === "" && + keyDownValueStr === "" + ) { + e.preventDefault(); + e.stopPropagation(); // Remove the item - this.removeItem(e, i); - last( + this.removeItem(e, contentItem.index); + + nthInIterable( this.content?.itemsContainer.querySelectorAll( "input, textarea" - ) + ), + Math.max(0, contentItem.index - 1) )?.focus(); + } else { + e.stopPropagation(); + this.value[contentItem.index] = e.detail.value; + this.emitKeyUpEvent(e, "", this.value); } }); + removeButton.tabIndex = -1; + removeButton.onclick = (e: Event) => { + e.preventDefault(); + this.removeItem(e, contentItem.index); + }; + + let keyDownValueStr = `${this.value[contentItem.index]}`; + container.appendChild(removeButton); container.appendChild(element); this.content.itemsContainer.appendChild(container); - - this.content.items.push({ - container, - removeButton, - element, - }); } const item = this.content.items[i]; + item.element.path = itemPath; + + item.index = i; item.removeButton.textContent = this.renderOptions.dictionary.removeItem; item.removeButton.title = this.renderOptions.dictionary.removeItemDescription; item.element.update({ - data: this.data[i], + value: this.value[i], schema: schemaItems, renderOptions: this.renderOptions, }); @@ -212,7 +237,7 @@ export class GDDTable extends GDDElementBase { if (!this.schema.items) return initialRender; const schemaItems = this.schema.items; - if (!this.data) this.data = []; + if (!this.value) this.value = []; if (!this.content) { initialRender = true; @@ -227,23 +252,23 @@ export class GDDTable extends GDDElementBase { this.content.elAddButton.onclick = (e) => { e.preventDefault(); - this.data.push(getDefaultDataFromSchema(schemaItems)); + this.value.push(getDefaultDataFromSchema(schemaItems)); this.render(); - this.emitOnChange(this.data); + this.emitChangeEvent(this.value); }; this.appendChild(this.content.elAddButton); } this._renderTopRow(this.content.elTrTop); - for (const elLine of this.content.rows.splice(this.data.length)) { + for (const elLine of this.content.rows.splice(this.value.length)) { // for (const row of elLine.rows) { // row.element.removeEventListener() // } this.content.elTable.removeChild(elLine.tr); } - for (let i = 0; i < this.data.length; i++) { + for (let i = 0; i < this.value.length; i++) { if (!this.content.rows[i]) { const tr = document.createElement("tr"); const removeButton = document.createElement("button"); @@ -257,9 +282,9 @@ export class GDDTable extends GDDElementBase { tdEnd.onclick = (e: Event) => { e.preventDefault(); - this.data.splice(i, 1); - this.emitOnKeyUp(e, "", this.data); - this.emitOnChange(this.data); + this.value.splice(i, 1); + this.emitKeyUpEvent(e, "", this.value); + this.emitChangeEvent(this.value); this.render(); }; @@ -280,19 +305,21 @@ export class GDDTable extends GDDElementBase { const properties = Object.entries(schemaItems.properties); for (const field of contentRow.fields.splice(properties.length)) { + field.element.destroy(); contentRow.tr.removeChild(field.td); - // field.element.removeEventListener() } for (let j = 0; j < properties.length; j++) { const key = properties[j][0]; const schema = properties[j][1]; - const data = this.data[i][key]; + const value = this.value[i][key]; if (!contentRow.fields[j]) { const td = document.createElement("td"); - const el = new GDDArrayProperty(); - el.path = `${this.path}[${i}].${key}`; + const el = new GDDArrayProperty({ + path: `${this.path}[${i}].${key}`, + getGDDElement: this.getGDDElement, + }); contentRow.fields.push({ td, element: el, @@ -300,25 +327,25 @@ export class GDDTable extends GDDElementBase { td.appendChild(el); contentRow.tr.appendChild(td); - el.addEventListener("onChange", (e: Event) => { + el.addListener("change", (e: Event) => { if (!(e instanceof CustomEvent)) return; e.stopPropagation(); - this.data[i][key] = e.detail.data; + this.value[i][key] = e.detail.value; - this.emitOnChange(this.data); + this.emitChangeEvent(this.value); }); - el.addEventListener("onKeyUp", (e: Event) => { + el.addListener("keyup", (e: Event) => { if (!(e instanceof CustomEvent)) return; e.stopPropagation(); - this.data[i][key] = e.detail.data; - this.emitOnKeyUp(e, "", this.data); + this.value[i][key] = e.detail.value; + this.emitKeyUpEvent(e, "", this.value); }); } const field = contentRow.fields[j]; field.element.update({ schema: schema, - data: data, + value: value, renderOptions: this.renderOptions, }); } @@ -329,12 +356,6 @@ export class GDDTable extends GDDElementBase { contentRow.removeButton.textContent = this.renderOptions.dictionary.removeRow; - - // this.content.rows[i].element.update({ - // schema: schemaItems, - // data: rowData, - // renderOptions: this.renderOptions, - // }); } this.content.elAddButton.textContent = this.renderOptions.dictionary.addRow; @@ -342,6 +363,16 @@ export class GDDTable extends GDDElementBase { this._renderStyle(); return initialRender; } + destroy(): void { + super.destroy(); + if (this.content) { + for (const row of this.content.rows) { + for (const field of row.fields) { + field.element.destroy(); + } + } + } + } private _renderStyle() { if (this.renderOptions.formStyle === "default") { } else { @@ -353,7 +384,7 @@ export class GDDTable extends GDDElementBase { elParent.innerHTML = ""; - if (this.data.length === 0) return; + if (this.value.length === 0) return; const elTh = document.createElement("th"); elParent.appendChild(elTh); @@ -372,56 +403,65 @@ export class GDDTable extends GDDElementBase { } export class GDDArrayProperty extends GDDElementBase { private content: { - content: GDDElementBase; + element: GDDElementBase; contentError: HTMLDivElement; } | null = null; - constructor() { - super(); - } connectedCallback(): void {} + destroy() { + super.destroy(); + if (this.content) { + this.content.element.destroy(); + } + } + render(): boolean { let initialRender = false; if (!this.schema) return initialRender; if (!this.content) { + const element = this.getGDDElement({ + schema: this.schema, + path: this.path, + getGDDElement: this.getGDDElement, + }); this.content = { - content: getGDDElement(this.schema, this.path), + element, contentError: document.createElement("div"), }; // Listen to events to update validation error: - this.content.content.addEventListener("onChange", (e: any) => { - if (isCustomEvent(e) && this.content && this.schema) { + this.content.element.addListener("change", (e: any) => { + if (isCustomEventKey(e) && this.content && this.schema) { renderContentError( this.content.contentError, this.schema, - e.detail.data + e.detail.value ); } }); - this.content.content.addEventListener("onKeyUp", (e: any) => { - if (isCustomEvent(e) && this.content && this.schema) { + this.content.element.addListener("keyup", (e: any) => { + if (isCustomEventKey(e) && this.content && this.schema) { renderContentError( this.content.contentError, this.schema, - e.detail.data + e.detail.value ); } }); - this.appendChild(this.content.content); + this.appendChild(this.content.element); } - this.content.content.update({ + this.content.element.update({ schema: this.schema, - data: this.data, + value: this.value, renderOptions: this.renderOptions, }); // this.content.label.textContent = this.schema.title || ""; // this.content.label.title = this.schema.description || ""; - renderContentError(this.content.contentError, this.schema, this.data); + renderContentError(this.content.contentError, this.schema, this.value); this._renderStyle(); return initialRender; diff --git a/lib/components/gdd-elements/json-elements/boolean.ts b/lib/components/gdd-elements/json-elements/boolean.ts index 3edcfd3..ba49f5d 100644 --- a/lib/components/gdd-elements/json-elements/boolean.ts +++ b/lib/components/gdd-elements/json-elements/boolean.ts @@ -12,18 +12,19 @@ export class GDDBoolean extends GDDInputBase { this.elInput.onchange = (e) => { if (!e.target) return; if (!(e.target instanceof HTMLInputElement)) return; + e.stopPropagation(); const checked = Boolean(e.target.checked); - if (this.data !== checked) { - this.data = checked; - this.emitOnKeyUp(e, "", checked); - this.emitOnChange(this.data); + if (this.value !== checked) { + this.value = checked; + this.emitKeyUpEvent(e, "", checked); + this.emitChangeEvent(this.value); } - e.target.checked = Boolean(this.data); + e.target.checked = Boolean(this.value); }; } - this.elInput.checked = Boolean(this.data); + this.elInput.checked = Boolean(this.value); this._renderStyle(); return initialRender; diff --git a/lib/components/gdd-elements/json-elements/object.ts b/lib/components/gdd-elements/json-elements/object.ts index 2462047..e4605a9 100644 --- a/lib/components/gdd-elements/json-elements/object.ts +++ b/lib/components/gdd-elements/json-elements/object.ts @@ -1,16 +1,11 @@ -import { GDDElementBase, isCustomEvent } from "../lib/_base.js"; -import { getBasicType, getGDDElement } from "../index.js"; -import { renderContentError } from "../lib/lib.js"; +import { GDDElementBase, isCustomEventKey } from "../lib/_base.js"; +import { getBasicType, renderContentError } from "../lib/lib.js"; export class GDDObject extends GDDElementBase { private content: Record = {}; - constructor() { - super(); - } - render(): boolean { - if (!this.data) this.data = {}; + if (!this.value) this.value = {}; const properties = Object.entries(this.schema?.properties ?? {}).sort( (a, b) => { @@ -27,39 +22,44 @@ export class GDDObject extends GDDElementBase { for (const [key, schema] of properties) { existingKeys.add(key); if (!this.content[key]) { - const el = new GDDObjectProperty(); - el.path = `${this.path}.${key}`; + const el = new GDDObjectProperty({ + path: `${this.path}.${key}`, + getGDDElement: this.getGDDElement, + }); this.content[key] = el; this.appendChild(el); - el.addEventListener("onChange", (e: Event) => { + el.addListener("change", (e: Event) => { if (!(e instanceof CustomEvent)) return; e.stopPropagation(); - this.data[key] = e.detail.data; + this.value[key] = e.detail.value; - this.emitOnChange(this.data); + this.emitChangeEvent(this.value); }); - el.addEventListener("onKeyUp", (e: Event) => { + el.addListener("keyup", (e: Event) => { if (!(e instanceof CustomEvent)) return; e.stopPropagation(); - this.data[key] = e.detail.data; - this.emitOnKeyUp(e, "", this.data); + this.value[key] = e.detail.value; + this.emitKeyUpEvent(e, "", this.value); }); } schema.title = schema.title || key; - const data = this.data[key]; + const value = this.value[key]; this.content[key].update({ schema, - data, + value, renderOptions: this.renderOptions, }); } + + // Remove items for (const [key, el] of Object.entries(this.content)) { if (existingKeys.has(key)) continue; // still exists // else remove: + el.destroy(); this.removeChild(el); delete this.content[key]; } @@ -77,6 +77,13 @@ export class GDDObject extends GDDElementBase { // No style } } + destroy(): void { + super.destroy(); + for (const el of Object.values(this.content)) { + el.destroy(); + } + this.content = {}; + } } export class GDDObjectProperty extends GDDElementBase { private content: { @@ -101,26 +108,30 @@ export class GDDObjectProperty extends GDDElementBase { // labelContainer: document.createElement("div"), label: document.createElement("label"), description: document.createElement("span"), - content: getGDDElement(this.schema, this.path), + content: this.getGDDElement({ + schema: this.schema, + path: this.path, + getGDDElement: this.getGDDElement, + }), contentError: document.createElement("div"), }; // Listen to events to update validation error: - this.content.content.addEventListener("onChange", (e: any) => { - if (isCustomEvent(e) && this.content && this.schema) { + this.content.content.addListener("change", (e: any) => { + if (isCustomEventKey(e) && this.content && this.schema) { renderContentError( this.content.contentError, this.schema, - e.detail.data + e.detail.value ); } }); - this.content.content.addEventListener("onKeyUp", (e: any) => { - if (isCustomEvent(e) && this.content && this.schema) { + this.content.content.addListener("keyup", (e: any) => { + if (isCustomEventKey(e) && this.content && this.schema) { renderContentError( this.content.contentError, this.schema, - e.detail.data + e.detail.value ); } }); @@ -140,7 +151,7 @@ export class GDDObjectProperty extends GDDElementBase { } this.content.content.update({ schema: this.schema, - data: this.data, + value: this.value, renderOptions: this.renderOptions, }); @@ -148,11 +159,17 @@ export class GDDObjectProperty extends GDDElementBase { this.content.label.title = this.schema.description || ""; this.content.description.textContent = this.schema.description || ""; - renderContentError(this.content.contentError, this.schema, this.data); + renderContentError(this.content.contentError, this.schema, this.value); this._renderStyle(); return initialRender; } + destroy(): void { + super.destroy(); + if (this.content) { + this.content.content.destroy(); + } + } private _renderStyle() { if (!this.content) return; if (!this.schema) return; diff --git a/lib/components/gdd-elements/json-elements/string.ts b/lib/components/gdd-elements/json-elements/string.ts index 794f7e8..418510d 100644 --- a/lib/components/gdd-elements/json-elements/string.ts +++ b/lib/components/gdd-elements/json-elements/string.ts @@ -7,22 +7,15 @@ export class GDDString extends GDDInputBase { if (!this.elInput) return initialRender; this.elInput.type = "text"; - this.elInput.name = this.path; this.elInput.onchange = (e) => { if (!e.target) return; - this.emitOnChange((e.target as any).value); - }; - this.elInput.onkeydown = (e) => { - if (!e.target) return; - this.emitOnKeyDown(e, (e.target as any).value, (e.target as any).value); - }; - this.elInput.onkeyup = (e) => { - if (!e.target) return; - this.emitOnKeyUp(e, (e.target as any).value, (e.target as any).value); + if (!(e.target instanceof HTMLInputElement)) return; + e.stopPropagation(); + this.emitChangeEvent(e.target.value); }; - this.elInput.value = this.data || ""; + this.elInput.value = this.value || ""; this._renderStyle(); return initialRender; } diff --git a/lib/components/gdd-elements/lib/_base.ts b/lib/components/gdd-elements/lib/_base.ts index 63cbaac..df99556 100644 --- a/lib/components/gdd-elements/lib/_base.ts +++ b/lib/components/gdd-elements/lib/_base.ts @@ -1,19 +1,33 @@ import type { GDDSchema } from "../../../lib/types.js"; -import { DEFAULT_DICTIONARY } from "./lib.js"; -import type { Options, RenderOptions } from "./lib.js"; export abstract class GDDElementBase extends HTMLElement { public path: string = ""; + protected getGDDElement: GetGDDElementFunction; + protected schema: GDDSchema | null = null; - protected data: any = null; + protected value: any = null; protected renderOptions: RenderOptions = { dictionary: DEFAULT_DICTIONARY, formStyle: "", }; + constructor(options: { path: string; getGDDElement: GetGDDElementFunction }) { + super(); + if (!options.path) throw new Error("Missing constructor path argument"); + if (!options.getGDDElement) + throw new Error("Missing constructor getGddElement argument"); + + this.path = options.path; + this.getGDDElement = options.getGDDElement; + } + + destroy(): void { + this.removeAllListeners(); + } + update(options: Options) { this.schema = options.schema; - this.data = options.data; + this.value = options.value; if (options.renderOptions) this.renderOptions = { ...this.renderOptions, @@ -23,57 +37,121 @@ export abstract class GDDElementBase extends HTMLElement { try { this.render(); } catch (error) { - console.error; + console.error(error); } } /** Returns true on initial render */ abstract render(): boolean; - static getOnChangeEvent(data: any) { - return new CustomEvent("onChange", { + static getChangeEvent(value: any): CustomEvent { + const detail: CustomEventChange["detail"] = { + value, + }; + + return new CustomEvent("change", { bubbles: true, cancelable: false, - detail: { data }, + detail, }); } + protected emitChangeEvent(value: any) { + this.dispatchEvent(GDDElementBase.getChangeEvent(value)); + } - protected emitOnChange(data: any) { - this.dispatchEvent(GDDElementBase.getOnChangeEvent(data)); + private listeners: { [key: string]: (...args: any[]) => void } = {}; + /** + * Like addEventListener, but keeps track of listeners to allow easy removal later + * @param type + * @param listener + */ + addListener(type: string, listener: (...args: any[]) => void) { + this.listeners[type] = listener; + this.addEventListener(type, listener); + } + removeAllListeners() { + for (const [key, listener] of Object.entries(this.listeners)) { + this.removeEventListener(key, listener); + } + this.listeners = {}; } - protected emitOnKeyDown(e: Event, inputStr: string, data: any) { - const detail: CustomEventDetail["detail"] = { - data, - inputStr, + static getKeyDownEvent(e: Event, valueStr: string, value: any): CustomEvent { + const detail: CustomEventKey["detail"] = { + value, + valueStr, key: (e as any).key, }; - const event = new CustomEvent("onKeyDown", { + return new CustomEvent("keydown", { bubbles: true, cancelable: false, detail, }); - this.dispatchEvent(event); } - protected emitOnKeyUp(e: Event, inputStr: string, data: any) { - const detail: CustomEventDetail["detail"] = { - data, - inputStr, + protected emitKeyDownEvent(e: Event, valueStr: string, value: any) { + this.dispatchEvent(GDDElementBase.getKeyDownEvent(e, valueStr, value)); + } + + static getKeyUpEvent(e: Event, valueStr: string, value: any) { + const detail: CustomEventKey["detail"] = { + value, + valueStr, key: (e as any).key, }; - const event = new CustomEvent("onKeyUp", { + return new CustomEvent("keyup", { bubbles: true, cancelable: false, detail, }); - this.dispatchEvent(event); } + protected emitKeyUpEvent(e: Event, valueStr: string, value: any) { + this.dispatchEvent(GDDElementBase.getKeyUpEvent(e, valueStr, value)); + } +} + +export type CustomEventChange = CustomEvent<{ + value: any; +}>; +export function isCustomEventChange(e: Event): e is CustomEventChange { + return e instanceof CustomEvent && e.detail.key == undefined; } -export type CustomEventDetail = CustomEvent<{ - data: any; - inputStr: string; +export type CustomEventKey = CustomEvent<{ + value: any; + valueStr: string; key: KeyboardEvent["key"]; }>; -export function isCustomEvent(e: Event): e is CustomEventDetail { - return e instanceof CustomEvent; +export function isCustomEventKey(e: Event): e is CustomEventKey { + return e instanceof CustomEvent && e.detail.key !== undefined; } + +/** + * Method that returns a WebComponent representing the GDD Schema at the given path + */ +export type GetGDDElementFunction = (props: { + /** The GDD Schema */ + schema: GDDSchema; + /** Breadcrumbs-style path to the property */ + path: string; + /** Reference to the GetGDDElementFunction to use for inner properties */ + getGDDElement: GetGDDElementFunction; +}) => GDDElementBase; + +export interface Options { + schema: GDDSchema; + value: any; + renderOptions?: Partial; +} +export interface RenderOptions { + formStyle: string | "" | "default"; + dictionary: Dictionary; +} + +export const DEFAULT_DICTIONARY = { + addRow: "Add Row", + removeRow: "Remove", + addItem: "Add", + removeItem: " - ", + addItemDescription: "Add item", + removeItemDescription: "Remove item", +}; +export type Dictionary = typeof DEFAULT_DICTIONARY; diff --git a/lib/components/gdd-elements/lib/lib.ts b/lib/components/gdd-elements/lib/lib.ts index fa65a89..3c0480f 100644 --- a/lib/components/gdd-elements/lib/lib.ts +++ b/lib/components/gdd-elements/lib/lib.ts @@ -1,32 +1,12 @@ -import type { GDDSchema } from "../../../lib/types.js"; +import type { GDDSchema, GDDTypeName } from "../../../lib/types.js"; import { validateDataSimple } from "../../../lib/validate-data.js"; -export interface Options { - schema: GDDSchema; - data: any; - renderOptions?: Partial; -} - -export const DEFAULT_DICTIONARY = { - addRow: "Add Row", - removeRow: "Remove", - addItem: "Add", - removeItem: " - ", - addItemDescription: "Add item", - removeItemDescription: "Remove item", -}; -export type Dictionary = typeof DEFAULT_DICTIONARY; -export interface RenderOptions { - formStyle: string | "" | "default"; - dictionary: Dictionary; -} - export function renderContentError( elError: HTMLDivElement, schema: GDDSchema, - data: any + value: any ): void { - const errors = validateDataSimple(schema, data, ""); + const errors = validateDataSimple(schema, value, ""); if (errors.errors.length === 0) { elError.style.display = "none"; @@ -75,3 +55,7 @@ export function getLeastSignificantPower(num: number): number { } return 1 / factor; } + +export function getBasicType(schemaType: GDDSchema["type"]): GDDTypeName { + return Array.isArray(schemaType) ? schemaType[0] : schemaType; +} diff --git a/lib/components/ograf-form.ts b/lib/components/ograf-form.ts index d8fcef7..3f79f99 100644 --- a/lib/components/ograf-form.ts +++ b/lib/components/ograf-form.ts @@ -1,9 +1,29 @@ -import { isEqual } from "../lib/lib.js"; -import type { GDDSchema } from "../lib/types.js"; -import { validateGDDSchema } from "../lib/validate-schema.js"; -import { getDefaultDataFromSchema } from "../main.js"; -import { GDDElementBase, getGDDElement } from "./gdd-elements/index.js"; -import type { Dictionary } from "./gdd-elements/lib/lib.js"; +import { assertNever, isEqual } from "../lib/lib.js"; +import { + DEFAULT_DICTIONARY, + type Dictionary, + type GetGDDElementFunction, +} from "./gdd-elements/lib/_base.js"; + +import type { GDDSchema } from "../main.js"; +import { + validateGDDSchema, + getBasicType, + getDefaultDataFromSchema, + GDDElementBase, + GDDMultiLineText, + GDDFilePath, + GDDSelect, + GDDColorRRGGBB, + GDDColorRRGGBBAA, + GDDPercentage, + GDDObject, + GDDArray, + GDDString, + GDDInteger, + GDDBoolean, + GDDNumber, +} from "../main.js"; export class SuperFlyTvOgrafDataForm extends HTMLElement { private elError = document.createElement("div"); @@ -19,21 +39,31 @@ export class SuperFlyTvOgrafDataForm extends HTMLElement { connectedCallback(): void { this.appendChild(this.elError); - // Check this.data attribute, set to default if not present: - if (this.schema !== null && this.data === null) { - const newData = getDefaultDataFromSchema(this.schema); + // Check this.value attribute, set to default value if not present: + if (this.schema !== null && this.value === null) { + const newValue = getDefaultDataFromSchema(this.schema); + + if (!isEqual(this.value, newValue)) { + this.value = newValue; - if (!isEqual(this.data, newData)) { - this.data = newData; - // Emit onChange event, so that any listeners are aware of the new data: - this.dispatchEvent(GDDElementBase.getOnChangeEvent(this.data)); + // Emit change event, so that any listeners are aware of the new value: + this.dispatchEvent(GDDElementBase.getChangeEvent(this.value)); } } this.render(); } + static get observedAttributes() { - return ["schema", "data", "formStyle", "dictionary"]; + return [ + "schema", + "value", + "formStyle", + "dictionary", + "onchange", + "onkeydown", + "onkeyup", + ]; } get schema(): GDDSchema | null { const schema = this.getAttribute("schema"); @@ -43,15 +73,15 @@ export class SuperFlyTvOgrafDataForm extends HTMLElement { set schema(value: GDDSchema) { this.setAttribute("schema", JSON.stringify(value)); } - get data(): any | null { - const data = this.getAttribute("data"); - if (data === "") return null; - if (typeof data === "string") return JSON.parse(data); - return data; + get value(): any | null { + const value = this.getAttribute("value"); + if (value === "") return undefined; + if (typeof value === "string") return JSON.parse(value); + return value; } - set data(value: any) { + set value(value: any) { this.setAttribute( - "data", + "value", typeof value === "string" ? value : JSON.stringify(value) ); } @@ -62,31 +92,77 @@ export class SuperFlyTvOgrafDataForm extends HTMLElement { set formStyle(formStyle: string) { this.setAttribute("formStyle", formStyle); } - get dictionary(): Dictionary | null { - const dictionary = this.getAttribute("dictionary"); + get dictionary(): Partial { + const dictionary = this.getAttribute("dictionary") ?? {}; if (typeof dictionary === "string") return JSON.parse(dictionary); return dictionary; } - set dictionary(value: Dictionary) { + set dictionary(value: Partial) { this.setAttribute( "dictionary", typeof value === "string" ? value : JSON.stringify(value) ); } + public getGDDElement?: GetGDDElementFunctionOptional; + public postRender?: () => void; + + private _getGDDElement: GetGDDElementFunction = (props): GDDElementBase => { + // First, use the custom getGDDElement function, if provided: + const element0 = this.getGDDElement?.(props); + if (element0) return element0; + + // Then, use the default implementation: + const basicType = getBasicType(props.schema.type); + + const options = { + path: props.path, + getGDDElement: props.getGDDElement, + }; + const gddType = props.schema.gddType + "/"; + + // Go through types in order of most specific to least specific: + + if (basicType === "string" && gddType.startsWith("multi-line/")) + return new GDDMultiLineText(options); + + if (basicType === "string" && gddType.startsWith("file-path/")) + return new GDDFilePath(options); + + if ( + (basicType === "string" || + basicType === "integer" || + basicType === "number") && + gddType.startsWith("select/") + ) + return new GDDSelect(options); + + if (basicType === "string" && gddType.startsWith("color-rrggbb/")) + return new GDDColorRRGGBB(options); + + if (basicType === "string" && gddType.startsWith("color-rrggbbaa/")) + return new GDDColorRRGGBBAA(options); + + if (basicType === "number" && gddType.startsWith("percentage/")) + return new GDDPercentage(options); + + // Finally, resort to one of the basic types: + if (basicType === "object") return new GDDObject(options); + if (basicType === "array") return new GDDArray(options); + if (basicType === "string") return new GDDString(options); + if (basicType === "integer") return new GDDInteger(options); + if (basicType === "boolean") return new GDDBoolean(options); + if (basicType === "number") return new GDDNumber(options); + + // else: + assertNever(basicType); + throw new Error(`Unsupported GDD type: ${basicType}`); + }; attributeChangedCallback(name: string, _oldValue: any, _newValue: any): void { - if (name === "data" || name === "schema") { + if (name === "value" || name === "schema") { this.render(); } } - onDataUpdated(data: unknown) { - const event = new CustomEvent("data-updated", { - bubbles: true, - cancelable: false, - detail: { data }, - }); - this.dispatchEvent(event); - } private render() { const error = this._getError(); @@ -110,10 +186,37 @@ export class SuperFlyTvOgrafDataForm extends HTMLElement { if (!this.elContent) { if (this.schema) { - this.elContent = getGDDElement(this.schema, "data"); + this.elContent = this._getGDDElement({ + schema: this.schema, + path: "data", + getGDDElement: this._getGDDElement, + }); + this.elForm.appendChild(this.elContent); - this.elContent.addEventListener("data-updated", (data) => { - this.onDataUpdated(data); + + this.elContent.addEventListener("change", (e) => { + if (!(e instanceof CustomEvent)) return; + + // Update this.value at this point, so that e.target.value is correct + // for any listeners further up the chain: + this.value = e.detail.value; + + // Re-emit the event, so that the target becomes this form: + e.stopPropagation(); + this.dispatchEvent(GDDElementBase.getChangeEvent(this.value)); + }); + this.elContent.addEventListener("keyup", (e) => { + if (!(e instanceof CustomEvent)) return; + + // Update this.value at this point, so that e.target.value is correct + // for any listeners further up the chain: + this.value = e.detail.value; + + // Re-emit the event, so that the target becomes this form: + e.stopPropagation(); + this.dispatchEvent( + GDDElementBase.getKeyUpEvent(e, e.detail.valueStr, this.value) + ); }); } } @@ -122,13 +225,16 @@ export class SuperFlyTvOgrafDataForm extends HTMLElement { if (this.elContent && this.schema) { this.elContent.update({ schema: this.schema, - data: this.data, + value: this.value, renderOptions: { formStyle, + dictionary: { ...DEFAULT_DICTIONARY, ...this.dictionary }, }, }); } this._renderStyle(formStyle); + + this.postRender?.(); } private _renderStyle(formStyle: string) { if (formStyle === "default") { @@ -150,4 +256,9 @@ export class SuperFlyTvOgrafDataForm extends HTMLElement { return null; } } + +export type GetGDDElementFunctionOptional = ( + props: Parameters[0] +) => GDDElementBase | undefined; + window.customElements.define("superflytv-ograf-form", SuperFlyTvOgrafDataForm); diff --git a/lib/lib/lib.ts b/lib/lib/lib.ts index 20ffba9..887586b 100644 --- a/lib/lib/lib.ts +++ b/lib/lib/lib.ts @@ -58,6 +58,15 @@ export function lookupKeys( return { existing, missing }; } +export function nthInIterable( + items: Iterable | ArrayLike | T[] | undefined, + index: number +): T | undefined { + if (!items) return undefined; + const array = Array.from(items); + + return array[index]; +} export function last( items: Iterable | ArrayLike | T[] | undefined ): T | undefined { diff --git a/lib/main.ts b/lib/main.ts index 211c416..726f666 100644 --- a/lib/main.ts +++ b/lib/main.ts @@ -1,9 +1,13 @@ import { SuperFlyTvOgrafDataForm } from "./components/ograf-form.js"; + import { getDefaultDataFromSchema } from "./lib/default-data.js"; import { validateDataSimple } from "./lib/validate-data.js"; import { validateGDDSchema } from "./lib/validate-schema.js"; export type { GDDSchema } from "./lib/types.js"; +export type { GetGDDElementFunctionOptional } from "./components/ograf-form.js"; +export * from "./components/gdd-elements/index.js"; + export { SuperFlyTvOgrafDataForm, getDefaultDataFromSchema, diff --git a/src/dev.ts b/src/dev.ts index 177bf2a..f101ef2 100644 --- a/src/dev.ts +++ b/src/dev.ts @@ -1,14 +1,19 @@ import "./style.css"; import * as examples from "./exampleSchemas.js"; -import { - SuperFlyTvOgrafDataForm, - getDefaultDataFromSchema, -} from "../lib/main.js"; +import { SuperFlyTvOgrafDataForm } from "../lib/main.js"; /* Note: This sets things up for Development! The actual components are in lib/* */ +// class TMP_GDD extends GDDElementBase { +// render(): boolean { +// this.innerHTML = "TMPGDD"; +// return false; +// } +// } +// window.customElements.define("tmp-gdd", TMP_GDD); + const app = document.querySelector("#app") as HTMLDivElement; function renderSchema(schema: any) { @@ -20,25 +25,54 @@ function renderSchema(schema: any) { app.appendChild(dataDiv); dataDiv.style.whiteSpace = "pre"; - const data = getDefaultDataFromSchema(schema); - const form = new SuperFlyTvOgrafDataForm(); - form.addEventListener("onChange", (e) => { + form.addEventListener("change", (e) => { + if (!e.target) return; if (!(e instanceof CustomEvent)) return; - console.log("Caught onChange event", JSON.stringify(e.detail)); + if (!(e.target instanceof SuperFlyTvOgrafDataForm)) { + throw new Error("Unexpected target"); + } + console.log("JS: Caught change event", e.target.value); - dataDiv.innerHTML = JSON.stringify(e.detail.data, null, 2); + dataDiv.innerHTML = JSON.stringify(e.target.value, null, 2); }); - form.addEventListener("onKeyUp", (e) => { + form.addEventListener("keyup", (e) => { if (!(e instanceof CustomEvent)) return; - console.log("Caught onKey event", JSON.stringify(e.detail)); - dataDiv.innerHTML = JSON.stringify(e.detail.data, null, 2); + if (!(e.target instanceof SuperFlyTvOgrafDataForm)) { + throw new Error("Unexpected target"); + } + console.log("JS: Caught keyup event", e.target.value); + + dataDiv.innerHTML = JSON.stringify(e.target.value, null, 2); }); form.schema = schema as any; - form.data = data; - - dataDiv.innerHTML = JSON.stringify(data, null, 2); + // form.value = value; + + // Example of custom getGDDElement function: + // form.getGDDElement = (props) => { + // const baseType = getBasicType(props.schema.type); + // if (baseType === "string") { + // return new TMP_GDD({ + // path: props.path, + // getGDDElement: props.getGDDElement, + // }); + // } + // }; + // Set dictionary + // form.dictionary = { + // addItem: "Ajouter", + // removeItem: "Supprimer", + // addRow: "Ajouter", + // removeRow: "Supprimer", + // }; + + // form.postRender = () => { + // form + // .querySelectorAll('input[type="text"]') + // .forEach((el: HTMLInputElement) => (el.style.backgroundColor = "#933")); + // }; + // dataDiv.innerHTML = JSON.stringify(value, null, 2); container.appendChild(form); } @@ -51,41 +85,22 @@ function renderHTML(schema: any) { app.appendChild(dataDiv); dataDiv.style.whiteSpace = "pre"; - // const data = getDefaultDataFromSchema(schema); - - // const form = new SuperFlyTvOgrafDataForm(); - // form.addEventListener("onChange", (e) => { - // if (!(e instanceof CustomEvent)) return; - // console.log("Caught onChange event", JSON.stringify(e.detail)); - - // dataDiv.innerHTML = JSON.stringify(e.detail.data, null, 2); - // }); - // form.addEventListener("onKeyUp", (e) => { - // if (!(e instanceof CustomEvent)) return; - // console.log("Caught onKey event", JSON.stringify(e.detail)); - - // dataDiv.innerHTML = JSON.stringify(e.detail.data, null, 2); - // }); - // form.schema = schema as any; - // form.data = data; - - // dataDiv.innerHTML = JSON.stringify(data, null, 2); - // container.appendChild(form); container.innerHTML = ` `; } -// renderSchema(manifest.schema); // renderSchema(examples.table); -// renderSchema(examples.nakedTable); +renderSchema(examples.nakedTable); // renderSchema(examples.deepObject); // renderSchema(examples.objectInArray); // renderSchema(examples.constraints); -renderSchema(examples.oneOfEach); -renderHTML(examples.oneOfEach); +// renderSchema(examples.oneOfEach); +renderHTML(examples.table);