diff --git a/CHANGELOG.md b/CHANGELOG.md index a7c333b..7e3bc98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed -- Fix `noImplicitAny` TypeScript errors in `x-template.js` (#357). -- Fix `noImplicitAny` TypeScript errors in `x-parser.js` (#357). -- Re-enable strict type errors for `strictNullChecks`, - `useUnknownInCatchVariables`, and `strictFunctionTypes` (#357). +- Make codebase work with TypeScript’s new `strict: true` default (#357). - Bump devDependencies (eslint 10, typescript 6, eslint-plugin-jsdoc 62, etc.). ## [2.0.2] - 2026-03-10 diff --git a/tsconfig.json b/tsconfig.json index e11d765..f95eeb3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,5 @@ "declarationMap": true, "noEmitOnError": true, "module": "NodeNext", // Approximates browser target. - // TODO: #357: Fix implicit any types. - "noImplicitAny": false } } diff --git a/types/x-element.d.ts b/types/x-element.d.ts index 8049dbc..8d54207 100644 --- a/types/x-element.d.ts +++ b/types/x-element.d.ts @@ -127,50 +127,245 @@ export default class XElement extends HTMLElement { * @returns {templateCallback} */ static template(html: (strings: TemplateStringsArray, ...values: unknown[]) => unknown): (properties: object, host: HTMLElement) => any; + /** + * Called once per class — kicked off from "static get observedAttributes". + * @param {any} constructor + */ static #analyzeConstructor(constructor: any): void; + /** + * Called during constructor analysis. + * @param {any} constructor + * @param {any} properties + * @param {any} entries + */ static #validateProperties(constructor: any, properties: any, entries: any): void; - static #validateProperty(constructor: any, key: any, property: any): void; - static #validatePropertyAttribute(constructor: any, key: any, property: any, attribute: any): void; - static #propertyIsCyclic(property: any, inputMap: any, seen?: Set): true | undefined; + /** + * @param {any} constructor + * @param {string} key + * @param {any} property + */ + static #validateProperty(constructor: any, key: string, property: any): void; + /** + * @param {any} constructor + * @param {string} key + * @param {any} property + * @param {string} attribute + */ + static #validatePropertyAttribute(constructor: any, key: string, property: any, attribute: string): void; + /** + * Determines if computed property inputs form a cycle. + * @param {any} property + * @param {any} inputMap + * @param {Set} [seen] + * @returns {boolean | undefined} + */ + static #propertyIsCyclic(property: any, inputMap: any, seen?: Set): boolean | undefined; + /** + * @param {any} constructor + * @param {any} listeners + * @param {any} entries + */ static #validateListeners(constructor: any, listeners: any, entries: any): void; - static #mutateProperty(constructor: any, propertyMap: any, key: any, property: any): void; + /** + * Called once per-property during constructor analysis. + * @param {any} constructor + * @param {any} propertyMap + * @param {string} key + * @param {any} property + */ + static #mutateProperty(constructor: any, propertyMap: any, key: string, property: any): void; + /** + * Wrapper to improve ergonomics of coalescing nullish, initial value. + * @param {any} constructor + * @param {any} property + */ static #addPropertyInitial(constructor: any, property: any): void; + /** + * Wrapper to improve ergonomics of coalescing nullish, default value. + * @param {any} constructor + * @param {any} property + */ static #addPropertyDefault(constructor: any, property: any): void; + /** + * Wrapper to improve ergonomics of syncing attributes back to properties. + * @param {any} constructor + * @param {any} property + */ static #addPropertySync(constructor: any, property: any): void; + /** + * Wrapper to centralize logic needed to perform reflection. + * @param {any} constructor + * @param {any} property + */ static #addPropertyReflect(constructor: any, property: any): void; + /** + * Wrapper to prevent repeated compute callbacks. + * @param {any} constructor + * @param {any} property + */ static #addPropertyCompute(constructor: any, property: any): void; + /** + * Wrapper to provide last value to observe callbacks. + * @param {any} constructor + * @param {any} property + */ static #addPropertyObserve(constructor: any, property: any): void; + /** + * Called once per-host during construction. + * @param {any} host + */ static #constructHost(host: any): void; + /** + * Called during host construction. + * @param {any} host + * @param {any} property + * @param {any} hostInfo + */ static #defineProperty(host: any, property: any, hostInfo: any): void; + /** + * Called during host construction. + * @param {any} host + * @returns {any} + */ static #createInternal(host: any): any; + /** + * Only available in template callback. Provides getter for all properties. + * Called during host construction. + * @param {any} host + * @returns {any} + */ static #createProperties(host: any): any; + /** + * Called once per-host from initial "connectedCallback". + * @param {any} host + */ static #connectHost(host: any): void; + /** @param {any} host */ static #disconnectHost(host: any): void; + /** + * @param {any} host + * @returns {boolean} + */ static #initializeHost(host: any): boolean; + /** + * Prevent shadowing from properties added to element instance pre-upgrade. + * @param {any} host + */ static #upgradeOwnProperties(host: any): void; + /** + * Called during host initialization. + * @param {any} host + * @param {any} property + * @param {any} hostInfo + * @returns {{ value: any, found: boolean }} + */ static #getPreUpgradePropertyValue(host: any, property: any, hostInfo: any): { value: any; found: boolean; }; - static #addListener(host: any, element: any, type: any, callback: any, options: any): void; + /** + * @param {any} host + * @param {any} element + * @param {any} type + * @param {any} callback + * @param {any} [options] + */ + static #addListener(host: any, element: any, type: any, callback: any, options?: any): void; + /** @param {any} host */ static #addListeners(host: any): void; - static #removeListener(host: any, element: any, type: any, callback: any, options: any): void; + /** + * @param {any} host + * @param {any} element + * @param {any} type + * @param {any} callback + * @param {any} [options] + */ + static #removeListener(host: any, element: any, type: any, callback: any, options?: any): void; + /** @param {any} host */ static #removeListeners(host: any): void; + /** + * @param {any} host + * @param {any} listener + * @returns {any} + */ static #getListener(host: any, listener: any): any; + /** @param {any} host */ static #updateHost(host: any): void; + /** + * Used to improve error messaging by appending DOM path information. + * @param {any} host + * @returns {string} + */ static #toPathString(host: any): string; + /** + * @param {any} host + * @param {any} property + */ static #invalidateProperty(host: any, property: any): void; + /** + * @param {any} host + * @param {any} property + * @returns {any} + */ static #getPropertyValue(host: any, property: any): any; + /** + * @param {any} host + * @param {any} property + */ static #validatePropertyMutable(host: any, property: any): void; + /** + * @param {any} host + * @param {any} property + * @param {any} value + */ static #validatePropertyValue(host: any, property: any, value: any): void; + /** + * @param {any} host + * @param {any} property + * @param {any} value + */ static #setPropertyValue(host: any, property: any, value: any): void; - static #serializeProperty(host: any, property: any, value: any): any; + /** + * @param {any} host + * @param {any} property + * @param {any} value + * @returns {string | undefined} + */ + static #serializeProperty(host: any, property: any, value: any): string | undefined; + /** + * @param {any} host + * @param {any} property + * @param {any} value + * @returns {any} + */ static #deserializeProperty(host: any, property: any, value: any): any; + /** + * Public properties which are serializable or typeless have attributes. + * @param {any} property + * @returns {boolean} + */ static #propertyHasAttribute(property: any): boolean; - static #getTypeName(value: any): string; - static #notNullish(value: any): boolean; - static #typeIsWrong(type: any, value: any): boolean; - static #camelToKebab(camel: any): any; + /** + * @param {unknown} value + * @returns {string} + */ + static #getTypeName(value: unknown): string; + /** + * @param {unknown} value + * @returns {boolean} + */ + static #notNullish(value: unknown): boolean; + /** + * @param {any} type + * @param {unknown} value + * @returns {boolean} + */ + static #typeIsWrong(type: any, value: unknown): boolean; + /** + * @param {string} camel + * @returns {string | undefined} + */ + static #camelToKebab(camel: string): string | undefined; static #constructors: WeakMap; static #hosts: WeakMap; static #propertyKeys: Set; diff --git a/types/x-element.d.ts.map b/types/x-element.d.ts.map index c2c584f..56e48c0 100644 --- a/types/x-element.d.ts.map +++ b/types/x-element.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"x-element.d.ts","sourceRoot":"","sources":["../x-element.js"],"names":[],"mappings":"AAEA,uDAAuD;AACvD;IACE;;;OAGG;IACH,iCAFa,MAAM,EAAE,CAKpB;IAGD;;;;;;;;;;;;;;;OAeG;IACH,qBAFa,aAAa,EAAE,CAI3B;IAED;;;;;;OAMG;IAEH;;;;;;;;;;;;;OAaG;IAEH;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,yBAFa;QAAC,CAAC,GAAG,EAAE,MAAM;mBA/BZ,CAAC,KAAK,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,GAAG,SAAS;;;iCAGvC,OAAO,EAAE,KAAK,OAAO;8BAXlC,WAAW,SACX,OAAO,YACP,OAAO;;;;sBAcJ,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC;sBACzB,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC;UAsBF;KAAC,CAIrC;IAED;;;;;OAKG;IAEH;;;;;;;;;;;;;OAaG;IACH,wBAFa;QAAC,CAAC,GAAG,EAAE,MAAM,UAhBf,WAAW,SACX,KAAK,SAeoC;KAAC,CAIpD;IAED;;;;;OAKG;IACH,8BAHW,WAAW,GACT,WAAW,GAAC,UAAU,CAIlC;IAED;;;;;OAKG;IAEH;;;;;;;;;;;OAWG;IACH,sBAHW,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,OAAO,gBAbhE,MAAM,QACN,WAAW,SAiBrB;IAqJD,mDA6BC;IAGD,kFAqCC;IAED,0EA2FC;IAED,mGAMC;IAGD,0FAaC;IAED,gFAQC;IAGD,0FAiBC;IAGD,kEAUC;IAGD,kEAkBC;IAGD,+DAUC;IAGD,kEAeC;IAGD,kEAsBC;IAGD,kEAYC;IAGD,uCAmDC;IAGD,sEAwBC;IAGD,uCA6CC;IAID,yCA6BC;IAGD,qCAMC;IAED,wCAEC;IAED,2CA4BC;IAGD,8CAMC;IAGD;;;MAqBC;IAED,2FAGC;IAED,sCAMC;IAED,8FAGC;IAED,yCAMC;IAED,mDAMC;IAED,oCAYC;IAGD,wCAeC;IAED,2DAcC;IAED,wDAGC;IAED,gEAUC;IAED,0EAQC;IAED,qEAQC;IAED,qEAOC;IAED,uEAoBC;IAGD,qDAEC;IAED,wCAEC;IAED,wCAEC;IAED,oDAOC;IAED,sCAKC;IAED,4CAAqC;IACrC,qCAA8B;IAC9B,kCAA8I;IAC9I,2FAA+D;IAC/D,+BAA4B;IAC5B,wCAAqF;IAp3BrF;;OAEG;IACH,0BAEC;IAQD;;;;;OAKG;IACH,oCAJW,MAAM,YACN,MAAM,GAAC,IAAI,SACX,MAAM,GAAC,IAAI,QAOrB;IAED;;OAEG;IACH,wBAAoB;IAEpB;;OAEG;IACH,6BAEC;IAED;;;;OAIG;IACH,eAYC;IAED;;;;OAIG;IAEH;;;;;;;OAOG;IACH,gBALW,WAAW,QACX,MAAM,oBAPN,KAAK,oBASL,MAAM,QAoBhB;IAED;;;;;;OAMG;IACH,kBALW,WAAW,QACX,MAAM,oBAlCN,KAAK,oBAoCL,MAAM,QAoBhB;IAED;;;OAGG;IACH,qBAFW,KAAK,QAMf;IAED;;;;;OAKG;IACH,gBAFa,MAAM,CAIlB;CA6uBF"} \ No newline at end of file +{"version":3,"file":"x-element.d.ts","sourceRoot":"","sources":["../x-element.js"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/x-element.js b/x-element.js index b8879a9..addec66 100644 --- a/x-element.js +++ b/x-element.js @@ -284,7 +284,10 @@ export default class XElement extends HTMLElement { return XElement.#hosts.get(this).internal; } - // Called once per class — kicked off from "static get observedAttributes". + /** + * Called once per class — kicked off from "static get observedAttributes". + * @param {any} constructor + */ static #analyzeConstructor(constructor) { const { styles, properties, listeners } = constructor; const propertiesEntries = Object.entries(properties); @@ -294,8 +297,8 @@ export default class XElement extends HTMLElement { const propertyMap = new Map(propertiesEntries); const internalPropertyMap = new Map(); // Use a normal object for better autocomplete when debugging in console. - const propertiesTarget = {}; - const internalTarget = {}; + const propertiesTarget = /** @type {Record} */ ({}); + const internalTarget = /** @type {Record} */ ({}); const attributeMap = new Map(); for (const [key, property] of propertyMap) { // We mutate (vs copy) to allow cross-referencing property objects. @@ -316,7 +319,12 @@ export default class XElement extends HTMLElement { }); } - // Called during constructor analysis. + /** + * Called during constructor analysis. + * @param {any} constructor + * @param {any} properties + * @param {any} entries + */ static #validateProperties(constructor, properties, entries) { const path = `${constructor.name}.properties`; for (const [key, property] of entries) { @@ -341,9 +349,9 @@ export default class XElement extends HTMLElement { attributes.add(attribute); } if (property.input) { - inputMap.set(property, property.input.map(inputKey => properties[inputKey])); - for (const [index, inputKey] of Object.entries(property.input)) { - if (XElement.#typeIsWrong(Object, properties[inputKey])) { + inputMap.set(property, property.input.map(/** @type {(inputKey: string) => any} */ inputKey => properties[inputKey])); + for (const [index, inputKey] of Object.entries(/** @type {string[]} */ (property.input))) { + if (XElement.#typeIsWrong(Object, properties[/** @type {string} */ (inputKey)])) { throw new Error(`${path}.${key}.input[${index}] has an unexpected item ("${inputKey}" has not been declared).`); } } @@ -356,6 +364,11 @@ export default class XElement extends HTMLElement { } } + /** + * @param {any} constructor + * @param {string} key + * @param {any} property + */ static #validateProperty(constructor, key, property) { const path = `${constructor.name}.properties.${key}`; if (key.includes('-')) { @@ -449,6 +462,12 @@ export default class XElement extends HTMLElement { } } + /** + * @param {any} constructor + * @param {string} key + * @param {any} property + * @param {string} attribute + */ static #validatePropertyAttribute(constructor, key, property, attribute) { const path = `${constructor.name}.properties`; // Attribute names are case-insensitive — lowercase to properly check for duplicates. @@ -457,7 +476,13 @@ export default class XElement extends HTMLElement { } } - // Determines if computed property inputs form a cycle. + /** + * Determines if computed property inputs form a cycle. + * @param {any} property + * @param {any} inputMap + * @param {Set} [seen] + * @returns {boolean | undefined} + */ static #propertyIsCyclic(property, inputMap, seen = new Set()) { if (inputMap.has(property)) { for (const input of inputMap.get(property)) { @@ -473,6 +498,11 @@ export default class XElement extends HTMLElement { } } + /** + * @param {any} constructor + * @param {any} listeners + * @param {any} entries + */ static #validateListeners(constructor, listeners, entries) { const path = `${constructor.name}.listeners`; for (const [type, listener] of entries) { @@ -483,13 +513,19 @@ export default class XElement extends HTMLElement { } } - // Called once per-property during constructor analysis. + /** + * Called once per-property during constructor analysis. + * @param {any} constructor + * @param {any} propertyMap + * @param {string} key + * @param {any} property + */ static #mutateProperty(constructor, propertyMap, key, property) { property.key = key; property.attribute = XElement.#propertyHasAttribute(property) ? property.attribute ?? XElement.#camelToKebab(key) : undefined; - property.input = new Set((property.input ?? []).map(inputKey => propertyMap.get(inputKey))); + property.input = new Set((property.input ?? []).map(/** @type {(inputKey: string) => any} */ inputKey => propertyMap.get(inputKey))); property.output = property.output ?? new Set(); for (const input of property.input) { input.output = input.output ?? new Set(); @@ -503,26 +539,34 @@ export default class XElement extends HTMLElement { XElement.#addPropertyObserve(constructor, property); } - // Wrapper to improve ergonomics of coalescing nullish, initial value. + /** + * Wrapper to improve ergonomics of coalescing nullish, initial value. + * @param {any} constructor + * @param {any} property + */ static #addPropertyInitial(constructor, property) { // Should take `value` in and spit the initial or value out. if (Reflect.has(property, 'initial')) { const initialValue = property.initial; const isFunction = XElement.#typeIsWrong(Function, initialValue) === false; - property.initial = value => + property.initial = /** @type {(value: any) => any} */ value => value ?? (isFunction ? initialValue.call(constructor) : initialValue); } else { - property.initial = value => value; + property.initial = /** @type {(value: any) => any} */ value => value; } } - // Wrapper to improve ergonomics of coalescing nullish, default value. + /** + * Wrapper to improve ergonomics of coalescing nullish, default value. + * @param {any} constructor + * @param {any} property + */ static #addPropertyDefault(constructor, property) { // Should take `value` in and spit the default or value out. if (Reflect.has(property, 'default')) { const { key, default: defaultValue } = property; const isFunction = XElement.#typeIsWrong(Function, defaultValue) === false; - const getOrCreateDefault = host => { + const getOrCreateDefault = /** @type {(host: any) => any} */ host => { const { defaultMap } = XElement.#hosts.get(host); if (!defaultMap.has(key)) { const value = isFunction ? defaultValue.call(constructor) : defaultValue; @@ -531,16 +575,20 @@ export default class XElement extends HTMLElement { } return defaultMap.get(key); }; - property.default = (host, value) => value ?? getOrCreateDefault(host); + property.default = (/** @type {any} */ host, /** @type {any} */ value) => value ?? getOrCreateDefault(host); } else { - property.default = (host, value) => value; + property.default = (/** @type {any} */ host, /** @type {any} */ value) => value; } } - // Wrapper to improve ergonomics of syncing attributes back to properties. + /** + * Wrapper to improve ergonomics of syncing attributes back to properties. + * @param {any} constructor + * @param {any} property + */ static #addPropertySync(constructor, property) { if (XElement.#propertyHasAttribute(property)) { - property.sync = (host, value, oldValue) => { + property.sync = (/** @type {any} */ host, /** @type {any} */ value, /** @type {any} */ oldValue) => { const { initialized, reflecting } = XElement.#hosts.get(host); if (reflecting === false && initialized && value !== oldValue) { const deserialization = XElement.#deserializeProperty(host, property, value); @@ -550,10 +598,14 @@ export default class XElement extends HTMLElement { } } - // Wrapper to centralize logic needed to perform reflection. + /** + * Wrapper to centralize logic needed to perform reflection. + * @param {any} constructor + * @param {any} property + */ static #addPropertyReflect(constructor, property) { if (property.reflect) { - property.reflect = host => { + property.reflect = /** @param {any} host */ host => { const value = XElement.#getPropertyValue(host, property); const serialization = XElement.#serializeProperty(host, property, value); const hostInfo = XElement.#hosts.get(host); @@ -568,11 +620,15 @@ export default class XElement extends HTMLElement { } } - // Wrapper to prevent repeated compute callbacks. + /** + * Wrapper to prevent repeated compute callbacks. + * @param {any} constructor + * @param {any} property + */ static #addPropertyCompute(constructor, property) { const { compute } = property; if (compute) { - property.compute = host => { + property.compute = /** @type {(host: any) => any} */ host => { const { computeMap, valueMap } = XElement.#hosts.get(host); const saved = computeMap.get(property); if (saved.valid === false) { @@ -593,11 +649,15 @@ export default class XElement extends HTMLElement { } } - // Wrapper to provide last value to observe callbacks. + /** + * Wrapper to provide last value to observe callbacks. + * @param {any} constructor + * @param {any} property + */ static #addPropertyObserve(constructor, property) { const { observe } = property; if (observe) { - property.observe = host => { + property.observe = /** @param {any} host */ host => { const saved = XElement.#hosts.get(host).observeMap.get(property); const value = XElement.#getPropertyValue(host, property); if (Object.is(value, saved.value) === false) { @@ -608,7 +668,10 @@ export default class XElement extends HTMLElement { } } - // Called once per-host during construction. + /** + * Called once per-host during construction. + * @param {any} host + */ static #constructHost(host) { const invalidProperties = new Set(); // The weak map prevents memory leaks. E.g., adding anonymous listeners. @@ -662,7 +725,12 @@ export default class XElement extends HTMLElement { } } - // Called during host construction. + /** + * Called during host construction. + * @param {any} host + * @param {any} property + * @param {any} hostInfo + */ static #defineProperty(host, property, hostInfo) { const { valueMap } = hostInfo; const { key } = property; @@ -689,13 +757,17 @@ export default class XElement extends HTMLElement { }); } - // Called during host construction. + /** + * Called during host construction. + * @param {any} host + * @returns {any} + */ static #createInternal(host) { const { propertyMap, internalPropertyMap, internalTarget } = XElement.#constructors.get(host.constructor); // Everything but "get", "set", "has", and "ownKeys" are considered invalid. // Note that impossible traps like "apply" or "construct" are not guarded. const invalid = () => { throw new Error('Invalid use of internal proxy.'); }; - const get = (target, key) => { + const get = (/** @type {any} */ target, /** @type {string} */ key) => { const internalProperty = internalPropertyMap.get(key); if (internalProperty?.internal) { return XElement.#getPropertyValue(host, internalProperty); @@ -709,7 +781,7 @@ export default class XElement extends HTMLElement { } } }; - const set = (target, key, value) => { + const set = (/** @type {any} */ target, /** @type {string} */ key, /** @type {any} */ value) => { const internalProperty = internalPropertyMap.get(key); if (internalProperty && Reflect.has(internalProperty, 'compute') === false) { XElement.#setPropertyValue(host, internalProperty, value); @@ -726,7 +798,7 @@ export default class XElement extends HTMLElement { } } }; - const has = (target, key) => internalPropertyMap.has(key); + const has = (/** @type {any} */ target, /** @type {string} */ key) => internalPropertyMap.has(key); const ownKeys = () => [...internalPropertyMap.keys()]; const handler = { defineProperty: invalid, deleteProperty: invalid, get, @@ -737,13 +809,17 @@ export default class XElement extends HTMLElement { return new Proxy(internalTarget, handler); } - // Only available in template callback. Provides getter for all properties. - // Called during host construction. + /** + * Only available in template callback. Provides getter for all properties. + * Called during host construction. + * @param {any} host + * @returns {any} + */ static #createProperties(host) { const { propertyMap, propertiesTarget } = XElement.#constructors.get(host.constructor); // Everything but "get", "set", "has", and "ownKeys" are considered invalid. const invalid = () => { throw new Error('Invalid use of properties proxy.'); }; - const get = (target, key) => { + const get = (/** @type {any} */ target, /** @type {string} */ key) => { if (propertyMap.has(key)) { return XElement.#getPropertyValue(host, propertyMap.get(key)); } else { @@ -751,7 +827,7 @@ export default class XElement extends HTMLElement { throw new Error(`Property "${path}" does not exist.`); } }; - const set = (target, key) => { + const set = (/** @type {any} */ target, /** @type {string} */ key) => { const path = `${host.constructor.name}.properties.${key}`; if (propertyMap.has(key)) { throw new Error(`Cannot set "${path}" via "properties".`); @@ -759,7 +835,7 @@ export default class XElement extends HTMLElement { throw new Error(`Property "${path}" does not exist.`); } }; - const has = (target, key) => propertyMap.has(key); + const has = (/** @type {any} */ target, /** @type {string} */ key) => propertyMap.has(key); const ownKeys = () => [...propertyMap.keys()]; const handler = { defineProperty: invalid, deleteProperty: invalid, get, @@ -770,7 +846,10 @@ export default class XElement extends HTMLElement { return new Proxy(propertiesTarget, handler); } - // Called once per-host from initial "connectedCallback". + /** + * Called once per-host from initial "connectedCallback". + * @param {any} host + */ static #connectHost(host) { const initialized = XElement.#initializeHost(host); XElement.#addListeners(host); @@ -779,10 +858,15 @@ export default class XElement extends HTMLElement { } } + /** @param {any} host */ static #disconnectHost(host) { XElement.#removeListeners(host); } + /** + * @param {any} host + * @returns {boolean} + */ static #initializeHost(host) { const hostInfo = XElement.#hosts.get(host); const { computeMap, initialized, invalidProperties, valueMap } = hostInfo; @@ -813,7 +897,10 @@ export default class XElement extends HTMLElement { return false; } - // Prevent shadowing from properties added to element instance pre-upgrade. + /** + * Prevent shadowing from properties added to element instance pre-upgrade. + * @param {any} host + */ static #upgradeOwnProperties(host) { for (const key of Reflect.ownKeys(host)) { const value = Reflect.get(host, key); @@ -822,7 +909,13 @@ export default class XElement extends HTMLElement { } } - // Called during host initialization. + /** + * Called during host initialization. + * @param {any} host + * @param {any} property + * @param {any} hostInfo + * @returns {{ value: any, found: boolean }} + */ static #getPreUpgradePropertyValue(host, property, hostInfo) { // Process possible sources of initial state, with this priority: // 1. imperative, e.g. `element.prop = 'value';` @@ -846,11 +939,19 @@ export default class XElement extends HTMLElement { return { value, found }; } + /** + * @param {any} host + * @param {any} element + * @param {any} type + * @param {any} callback + * @param {any} [options] + */ static #addListener(host, element, type, callback, options) { callback = XElement.#getListener(host, callback); element.addEventListener(type, callback, options); } + /** @param {any} host */ static #addListeners(host) { const { listenerMap } = XElement.#constructors.get(host.constructor); const { renderRoot } = XElement.#hosts.get(host); @@ -859,11 +960,19 @@ export default class XElement extends HTMLElement { } } + /** + * @param {any} host + * @param {any} element + * @param {any} type + * @param {any} callback + * @param {any} [options] + */ static #removeListener(host, element, type, callback, options) { callback = XElement.#getListener(host, callback); element.removeEventListener(type, callback, options); } + /** @param {any} host */ static #removeListeners(host) { const { listenerMap } = XElement.#constructors.get(host.constructor); const { renderRoot } = XElement.#hosts.get(host); @@ -872,6 +981,11 @@ export default class XElement extends HTMLElement { } } + /** + * @param {any} host + * @param {any} listener + * @returns {any} + */ static #getListener(host, listener) { const { listenerMap } = XElement.#hosts.get(host); if (listenerMap.has(listener) === false) { @@ -880,6 +994,7 @@ export default class XElement extends HTMLElement { return listenerMap.get(listener); } + /** @param {any} host */ static #updateHost(host) { // Order of operations: compute, reflect, render, then observe. const { invalidProperties } = XElement.#hosts.get(host); @@ -894,7 +1009,11 @@ export default class XElement extends HTMLElement { } } - // Used to improve error messaging by appending DOM path information. + /** + * Used to improve error messaging by appending DOM path information. + * @param {any} host + * @returns {string} + */ static #toPathString(host) { const path = []; let reference = host; @@ -912,6 +1031,10 @@ export default class XElement extends HTMLElement { .join(' < '); } + /** + * @param {any} host + * @param {any} property + */ static #invalidateProperty(host, property) { const { invalidProperties, computeMap } = XElement.#hosts.get(host); for (const output of property.output) { @@ -928,11 +1051,20 @@ export default class XElement extends HTMLElement { } } + /** + * @param {any} host + * @param {any} property + * @returns {any} + */ static #getPropertyValue(host, property) { const { valueMap } = XElement.#hosts.get(host); return property.compute?.(host) ?? valueMap.get(property); } + /** + * @param {any} host + * @param {any} property + */ static #validatePropertyMutable(host, property) { const { compute, readOnly, key } = property; if (compute) { @@ -945,6 +1077,11 @@ export default class XElement extends HTMLElement { } } + /** + * @param {any} host + * @param {any} property + * @param {any} value + */ static #validatePropertyValue(host, property, value) { if (property.type && XElement.#notNullish(value)) { if (XElement.#typeIsWrong(property.type, value)) { @@ -955,6 +1092,11 @@ export default class XElement extends HTMLElement { } } + /** + * @param {any} host + * @param {any} property + * @param {any} value + */ static #setPropertyValue(host, property, value) { const { valueMap } = XElement.#hosts.get(host); if (Object.is(value, valueMap.get(property)) === false) { @@ -965,6 +1107,12 @@ export default class XElement extends HTMLElement { } } + /** + * @param {any} host + * @param {any} property + * @param {any} value + * @returns {string | undefined} + */ static #serializeProperty(host, property, value) { if (XElement.#notNullish(value)) { if (property.type === Boolean) { @@ -974,6 +1122,12 @@ export default class XElement extends HTMLElement { } } + /** + * @param {any} host + * @param {any} property + * @param {any} value + * @returns {any} + */ static #deserializeProperty(host, property, value) { if (property.type === Boolean) { // Per HTML spec, every value other than null is considered true. @@ -996,19 +1150,36 @@ export default class XElement extends HTMLElement { } } - // Public properties which are serializable or typeless have attributes. + /** + * Public properties which are serializable or typeless have attributes. + * @param {any} property + * @returns {boolean} + */ static #propertyHasAttribute(property) { return !property.internal && (XElement.#serializableTypes.has(property.type) || !property.type); } + /** + * @param {unknown} value + * @returns {string} + */ static #getTypeName(value) { return Object.prototype.toString.call(value).slice(8, -1); } + /** + * @param {unknown} value + * @returns {boolean} + */ static #notNullish(value) { return value !== undefined && value !== null; } + /** + * @param {any} type + * @param {unknown} value + * @returns {boolean} + */ static #typeIsWrong(type, value) { // Because `instanceof` fails on primitives (`'' instanceof String === false`) // and `Object.prototype.toString` cannot handle inheritance, we use both. @@ -1018,6 +1189,10 @@ export default class XElement extends HTMLElement { ); } + /** + * @param {string} camel + * @returns {string | undefined} + */ static #camelToKebab(camel) { if (XElement.#caseMap.has(camel) === false) { XElement.#caseMap.set(camel, camel.replace(/([A-Z])/g, '-$1').toLowerCase());