diff options
Diffstat (limited to 'ext/js/dom/selector-observer.js')
-rw-r--r-- | ext/js/dom/selector-observer.js | 113 |
1 files changed, 64 insertions, 49 deletions
diff --git a/ext/js/dom/selector-observer.js b/ext/js/dom/selector-observer.js index e0e3d4ff..2cf46543 100644 --- a/ext/js/dom/selector-observer.js +++ b/ext/js/dom/selector-observer.js @@ -18,55 +18,35 @@ /** * Class which is used to observe elements matching a selector in specific element. + * @template T */ export class SelectorObserver { /** - * @function OnAddedCallback - * @param {Element} element The element which was added. - * @returns {*} Custom data which is assigned to element and passed to callbacks. - */ - - /** - * @function OnRemovedCallback - * @param {Element} element The element which was removed. - * @param {*} data The custom data corresponding to the element. - */ - - /** - * @function OnChildrenUpdatedCallback - * @param {Element} element The element which had its children updated. - * @param {*} data The custom data corresponding to the element. - */ - - /** - * @function IsStaleCallback - * @param {Element} element The element which had its children updated. - * @param {*} data The custom data corresponding to the element. - * @returns {boolean} Whether or not the data is stale for the element. - */ - - /** * Creates a new instance. - * @param {object} details The configuration for the object. - * @param {string} details.selector A string CSS selector used to find elements. - * @param {?string} details.ignoreSelector A string CSS selector used to filter elements, or `null` for no filtering. - * @param {OnAddedCallback} details.onAdded A function which is invoked for each element that is added that matches the selector. - * @param {?OnRemovedCallback} details.onRemoved A function which is invoked for each element that is removed, or `null`. - * @param {?OnChildrenUpdatedCallback} details.onChildrenUpdated A function which is invoked for each element which has its children updated, or `null`. - * @param {?IsStaleCallback} details.isStale A function which checks if the data is stale for a given element, or `null`. - * If the element is stale, it will be removed and potentially re-added. + * @param {import('selector-observer').ConstructorDetails<T>} details The configuration for the object. */ constructor({selector, ignoreSelector=null, onAdded=null, onRemoved=null, onChildrenUpdated=null, isStale=null}) { + /** @type {string} */ this._selector = selector; + /** @type {?string} */ this._ignoreSelector = ignoreSelector; + /** @type {?import('selector-observer').OnAddedCallback<T>} */ this._onAdded = onAdded; + /** @type {?import('selector-observer').OnRemovedCallback<T>} */ this._onRemoved = onRemoved; + /** @type {?import('selector-observer').OnChildrenUpdatedCallback<T>} */ this._onChildrenUpdated = onChildrenUpdated; + /** @type {?import('selector-observer').IsStaleCallback<T>} */ this._isStale = isStale; + /** @type {?Element} */ this._observingElement = null; + /** @type {MutationObserver} */ this._mutationObserver = new MutationObserver(this._onMutation.bind(this)); + /** @type {Map<Node, import('selector-observer').Observer<T>>} */ this._elementMap = new Map(); // Map([element => observer]...) + /** @type {Map<Node, Set<import('selector-observer').Observer<T>>>} */ this._elementAncestorMap = new Map(); // Map([element => Set([observer]...)]...) + /** @type {boolean} */ this._isObserving = false; } @@ -100,9 +80,10 @@ export class SelectorObserver { subtree: true }); + const {parentNode} = element; this._onMutation([{ type: 'childList', - target: element.parentNode, + target: parentNode !== null ? parentNode : element, addedNodes: [element], removedNodes: [] }]); @@ -124,17 +105,19 @@ export class SelectorObserver { /** * Returns an iterable list of [element, data] pairs. - * @yields A sequence of [element, data] pairs. + * @yields {[element: Element, data: T]} A sequence of [element, data] pairs. + * @returns {Generator<[element: Element, data: T], void, unknown>} */ *entries() { - for (const [element, {data}] of this._elementMap) { + for (const {element, data} of this._elementMap.values()) { yield [element, data]; } } /** * Returns an iterable list of data for every element. - * @yields A sequence of data values. + * @yields {T} A sequence of data values. + * @returns {Generator<T, void, unknown>} */ *datas() { for (const {data} of this._elementMap.values()) { @@ -144,6 +127,9 @@ export class SelectorObserver { // Private + /** + * @param {(MutationRecord|import('selector-observer').MutationRecordLike)[]} mutationList + */ _onMutation(mutationList) { for (const mutation of mutationList) { switch (mutation.type) { @@ -157,6 +143,9 @@ export class SelectorObserver { } } + /** + * @param {MutationRecord|import('selector-observer').MutationRecordLike} record + */ _onChildListMutation({addedNodes, removedNodes, target}) { const selector = this._selector; const ELEMENT_NODE = Node.ELEMENT_NODE; @@ -171,10 +160,10 @@ export class SelectorObserver { for (const node of addedNodes) { if (node.nodeType !== ELEMENT_NODE) { continue; } - if (node.matches(selector)) { - this._createObserver(node); + if (/** @type {Element} */ (node).matches(selector)) { + this._createObserver(/** @type {Element} */ (node)); } - for (const childNode of node.querySelectorAll(selector)) { + for (const childNode of /** @type {Element} */ (node).querySelectorAll(selector)) { this._createObserver(childNode); } } @@ -183,7 +172,7 @@ export class SelectorObserver { this._onChildrenUpdated !== null && (addedNodes.length !== 0 || addedNodes.length !== 0) ) { - for (let node = target; node !== null; node = node.parentNode) { + for (let node = /** @type {?Node} */ (target); node !== null; node = node.parentNode) { const observer = this._elementMap.get(node); if (typeof observer !== 'undefined') { this._onObserverChildrenUpdated(observer); @@ -192,9 +181,12 @@ export class SelectorObserver { } } + /** + * @param {MutationRecord|import('selector-observer').MutationRecordLike} record + */ _onAttributeMutation({target}) { const selector = this._selector; - const observers = this._elementAncestorMap.get(target); + const observers = this._elementAncestorMap.get(/** @type {Element} */ (target)); if (typeof observers !== 'undefined') { for (const observer of observers) { const element = observer.element; @@ -208,15 +200,19 @@ export class SelectorObserver { } } - if (target.matches(selector)) { - this._createObserver(target); + if (/** @type {Element} */ (target).matches(selector)) { + this._createObserver(/** @type {Element} */ (target)); } } + /** + * @param {Element} element + */ _createObserver(element) { if (this._elementMap.has(element) || this._shouldIgnoreElement(element) || this._onAdded === null) { return; } const data = this._onAdded(element); + if (typeof data === 'undefined') { return; } const ancestors = this._getAncestors(element); const observer = {element, ancestors, data}; @@ -232,6 +228,9 @@ export class SelectorObserver { } } + /** + * @param {import('selector-observer').Observer<T>} observer + */ _removeObserver(observer) { const {element, ancestors, data} = observer; @@ -252,26 +251,42 @@ export class SelectorObserver { } } + /** + * @param {import('selector-observer').Observer<T>} observer + */ _onObserverChildrenUpdated(observer) { + if (this._onChildrenUpdated === null) { return; } this._onChildrenUpdated(observer.element, observer.data); } + /** + * @param {import('selector-observer').Observer<T>} observer + * @returns {boolean} + */ _isObserverStale(observer) { return (this._isStale !== null && this._isStale(observer.element, observer.data)); } + /** + * @param {Element} element + * @returns {boolean} + */ _shouldIgnoreElement(element) { return (this._ignoreSelector !== null && element.matches(this._ignoreSelector)); } + /** + * @param {Node} node + * @returns {Node[]} + */ _getAncestors(node) { const root = this._observingElement; const results = []; - while (true) { - results.push(node); - if (node === root) { break; } - node = node.parentNode; - if (node === null) { break; } + let n = /** @type {?Node} */ (node); + while (n !== null) { + results.push(n); + if (n === root) { break; } + n = n.parentNode; } return results; } |