diff --git a/src/controller-registry.js b/src/controller-registry.js index 8945dc3..160f087 100644 --- a/src/controller-registry.js +++ b/src/controller-registry.js @@ -1,14 +1,17 @@ +/** @typedef {Promise & {signal: AbortSignal}} PromiseWithSignal */ +/** @typedef {(element: HTMLElement, detach: PromiseWithSignal) => void} Callback */ + export class ControllerList { /** @type {HTMLElement} */ #element - /** @type {String} */ + /** @type {string} */ #attribute /** - * @param {HTMLElement} element - * @param {String} attribute - * */ + * @param {HTMLElement} element + * @param {string} attribute + * */ constructor(element, attribute="controller") { this.#element = element this.#attribute = attribute @@ -22,24 +25,24 @@ export class ControllerList { this.#element.setAttribute(this.#attribute, [...set].join(" ")) } - /** @param {String} name */ + /** @param {string} name */ contains(name) { return this.#set.has(name) } - /** @param {String} name */ + /** @param {string} name */ add(name) { this.toggle(name, true) } - /** @param {String} name */ + /** @param {string} name */ remove(name) { this.toggle(name, false) } /** - * @param {String} name - * @param {String} replacement + * @param {string} name + * @param {string} replacement */ replace(name, replacement) { const set = this.#set @@ -54,7 +57,7 @@ export class ControllerList { } /** - * @param {String} name + * @param {string} name * @param {Boolean} force */ toggle(name, force) { @@ -81,32 +84,34 @@ export class ControllerRegistry { #observer = new MutationObserver(mutations => { for (const mutation of mutations) { if (mutation.type === "childList") { - for (const node of mutation.addedNodes) { + for (const node of mutation.addedNodes) if (node instanceof HTMLElement) { this.upgrade(node) - this.#update(node) } - } else { + for (const node of mutation.removedNodes) if (node instanceof HTMLElement) { + this.#downgrade(node) + } + } else if (mutation.target instanceof HTMLElement) { this.#update(mutation.target) } } }) - /** @type {WeakMap>} */ + /** @type {WeakMap>} */ #attached = new WeakMap() - /** @type {Map>} */ + /** @type {Map>} */ #waiting = new Map() - /** @type {Mapvoid>} */ + /** @type {Map} */ #defined = new Map() #attribute - /** @typedef {HTMLElement} Root */ + /** @typedef {Document|DocumentFragment|HTMLElement} Root */ /** * @param {Root} root - * @param {String} attribute + * @param {string} attribute */ constructor(root, attribute="controller") { this.#attribute = attribute @@ -118,12 +123,25 @@ export class ControllerRegistry { * @param {Root} root */ upgrade(root) { - root.querySelectorAll(`[${this.#attribute}]`).forEach(element => this.#update(element)) + if (root instanceof HTMLElement) this.#update(root) + for (const element of root.querySelectorAll(`[${this.#attribute}]`)) { + this.#update(/** @type {HTMLElement} */(element)) + } } /** - * @param {String} name - * @param {Function} callback + * @param {Root} root + */ + #downgrade(root) { + if (root instanceof HTMLElement) this.#clear(root) + for (const element of root.querySelectorAll(`[${this.#attribute}]`)) { + this.#clear(/** @type {HTMLElement} */(element)) + } + } + + /** + * @param {string} name + * @param {Callback} callback */ define(name, callback) { if (("function" == typeof callback) && callback.prototype) { @@ -141,16 +159,24 @@ export class ControllerRegistry { const waitingList = this.#waiting.get(name) if (waitingList) for (const element of waitingList) { - try { this.#attach(element, name) } catch(e) { console.error(e) } + this.#attach(element, name) } this.#waiting.delete(name) } + /** Gets a controller associated with a given name + * @param {string} name + */ get(name) { return this.#defined.get(name) } + /** @type {WeakMap} */ #listMap = new WeakMap() + /** + * @param {HTMLElement} element + * @return {ControllerList} + */ list(element) { if (this.#listMap.has(element)) { return this.#listMap.get(element) @@ -161,29 +187,53 @@ export class ControllerRegistry { } } + /** @param {HTMLElement} element */ + attached(element) { + const attached = this.#attached.get(element) + if (attached) + return [...attached.entries().filter(pair => pair[1]).map(pair => pair[0])] + else + return [] + } + getName(controller) { - // Return name of controller + // TODO: Return name of controller } whenDefined(name) { - // Return a promise + // TODO: Return a promise } + /** @param {HTMLElement} element */ #update(element) { const names = this.#getControllerNames(element) const attached = this.#attached.get(element) if (attached) { const current = new Set(names) - for (const [name, abortController] of attached) { + for (const [name] of attached) { if (!current.has(name)) { - try { this.#detach(element, name) } catch(e) { console.error(e) } + this.#detach(element, name) } } } - for (const name of names) try { this.#attach(element, name) } catch(e) { console.error(e) } + for (const name of names) this.#attach(element, name) } + /** @param {HTMLElement} element */ + #clear(element) { + const attached = this.#attached.get(element) + if (attached) { + for (const [name] of attached) { + this.#detach(element, name) + } + } + } + + /** + * @param {HTMLElement} element + * @param {string} name + */ #attach(element, name) { if (!this.#attached.has(element)) this.#attached.set(element, new Map()) const attached = this.#attached.get(element) @@ -193,10 +243,14 @@ export class ControllerRegistry { if (callback) { if (attached.has("name") && attached.get("name")) return console.warn(`Controller ${name} already fully attached`, element) const abortController = new AbortController() - attached.set(name, abortController) const promise = new Promise(resolve => abortController.signal.addEventListener("abort", () => resolve())) Object.defineProperty(promise, "signal", {value: abortController.signal}) - callback(element, promise) + try { + callback(element, /** @type {PromiseWithSignal} */(promise)) + attached.set(name, abortController) + } catch(error) { + console.error(error) + } } else { if (attached.has("name")) return console.warn(`Controller ${name} already attached`, element) attached.set(name, undefined) @@ -209,6 +263,10 @@ export class ControllerRegistry { } } + /** + * @param {HTMLElement} element + * @param {string} name + */ #detach(element, name) { const references = this.#attached.get(element) if (!references || !references.has(name)) return console.warn(`Controller ${name} not attached`, element) @@ -217,6 +275,7 @@ export class ControllerRegistry { references.delete(name) } + /** @param {HTMLElement} element */ #getControllerNames(element) { return new Set(element.getAttribute(this.#attribute)?.split(" ") ?? []) }