Enable detaching controllers on DOM removal
This commit is contained in:
parent
7e766a288b
commit
bfb9b560ca
1 changed files with 88 additions and 29 deletions
|
@ -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<HTMLElement,Map<String,Object>>} */
|
||||
/** @type {WeakMap<HTMLElement,Map<string,Object>>} */
|
||||
#attached = new WeakMap()
|
||||
|
||||
/** @type {Map<String,Set<HTMLElement>>} */
|
||||
/** @type {Map<string,Set<HTMLElement>>} */
|
||||
#waiting = new Map()
|
||||
|
||||
/** @type {Map<String,(element: HTMLElement, signal: AbortSignal)=>void>} */
|
||||
/** @type {Map<string,Callback>} */
|
||||
#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<HTMLElement,ControllerList>} */
|
||||
#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(" ") ?? [])
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue