mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
301 lines
9.8 KiB
JavaScript
301 lines
9.8 KiB
JavaScript
/*
|
|
* Copyright (C) 2023-2025 Yomitan Authors
|
|
* Copyright (C) 2020-2022 Yomichan Authors
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/**
|
|
* Class which is used to observe elements matching a selector in specific element.
|
|
* @template [T=unknown]
|
|
*/
|
|
export class SelectorObserver {
|
|
/**
|
|
* Creates a new instance.
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not an element is currently being observed.
|
|
* @returns {boolean} `true` if an element is being observed, `false` otherwise.
|
|
*/
|
|
get isObserving() {
|
|
return this._observingElement !== null;
|
|
}
|
|
|
|
/**
|
|
* Starts DOM mutation observing the target element.
|
|
* @param {Element} element The element to observe changes in.
|
|
* @param {boolean} [attributes] A boolean for whether or not attribute changes should be observed.
|
|
* @throws {Error} An error if element is null.
|
|
* @throws {Error} An error if an element is already being observed.
|
|
*/
|
|
observe(element, attributes = false) {
|
|
if (element === null) {
|
|
throw new Error('Invalid element');
|
|
}
|
|
if (this.isObserving) {
|
|
throw new Error('Instance is already observing an element');
|
|
}
|
|
|
|
this._observingElement = element;
|
|
this._mutationObserver.observe(element, {
|
|
attributes: !!attributes,
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
|
|
const {parentNode} = element;
|
|
this._onMutation([{
|
|
type: 'childList',
|
|
target: parentNode !== null ? parentNode : element,
|
|
addedNodes: [element],
|
|
removedNodes: [],
|
|
}]);
|
|
}
|
|
|
|
/**
|
|
* Stops observing the target element.
|
|
*/
|
|
disconnect() {
|
|
if (!this.isObserving) { return; }
|
|
|
|
this._mutationObserver.disconnect();
|
|
this._observingElement = null;
|
|
|
|
for (const observer of this._elementMap.values()) {
|
|
this._removeObserver(observer);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an iterable list 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.values()) {
|
|
yield [element, data];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an iterable list of data for every element.
|
|
* @yields {T} A sequence of data values.
|
|
* @returns {Generator<T, void, unknown>}
|
|
*/
|
|
*datas() {
|
|
for (const {data} of this._elementMap.values()) {
|
|
yield data;
|
|
}
|
|
}
|
|
|
|
// Private
|
|
|
|
/**
|
|
* @param {(MutationRecord|import('selector-observer').MutationRecordLike)[]} mutationList
|
|
*/
|
|
_onMutation(mutationList) {
|
|
for (const mutation of mutationList) {
|
|
switch (mutation.type) {
|
|
case 'childList':
|
|
this._onChildListMutation(mutation);
|
|
break;
|
|
case 'attributes':
|
|
this._onAttributeMutation(mutation);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {MutationRecord|import('selector-observer').MutationRecordLike} record
|
|
*/
|
|
_onChildListMutation({addedNodes, removedNodes, target}) {
|
|
const selector = this._selector;
|
|
const ELEMENT_NODE = Node.ELEMENT_NODE;
|
|
|
|
for (const node of removedNodes) {
|
|
const observers = this._elementAncestorMap.get(node);
|
|
if (typeof observers === 'undefined') { continue; }
|
|
for (const observer of observers) {
|
|
this._removeObserver(observer);
|
|
}
|
|
}
|
|
|
|
for (const node of addedNodes) {
|
|
if (node.nodeType !== ELEMENT_NODE) { continue; }
|
|
if (/** @type {Element} */ (node).matches(selector)) {
|
|
this._createObserver(/** @type {Element} */ (node));
|
|
}
|
|
for (const childNode of /** @type {Element} */ (node).querySelectorAll(selector)) {
|
|
this._createObserver(childNode);
|
|
}
|
|
}
|
|
|
|
if (
|
|
this._onChildrenUpdated !== null &&
|
|
(removedNodes.length > 0 || addedNodes.length > 0)
|
|
) {
|
|
for (let node = /** @type {?Node} */ (target); node !== null; node = node.parentNode) {
|
|
const observer = this._elementMap.get(node);
|
|
if (typeof observer !== 'undefined') {
|
|
this._onObserverChildrenUpdated(observer);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {MutationRecord|import('selector-observer').MutationRecordLike} record
|
|
*/
|
|
_onAttributeMutation({target}) {
|
|
const selector = this._selector;
|
|
const observers = this._elementAncestorMap.get(/** @type {Element} */ (target));
|
|
if (typeof observers !== 'undefined') {
|
|
for (const observer of observers) {
|
|
const element = observer.element;
|
|
if (
|
|
!element.matches(selector) ||
|
|
this._shouldIgnoreElement(element) ||
|
|
this._isObserverStale(observer)
|
|
) {
|
|
this._removeObserver(observer);
|
|
}
|
|
}
|
|
}
|
|
|
|
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};
|
|
|
|
this._elementMap.set(element, observer);
|
|
|
|
for (const ancestor of ancestors) {
|
|
let observers = this._elementAncestorMap.get(ancestor);
|
|
if (typeof observers === 'undefined') {
|
|
observers = new Set();
|
|
this._elementAncestorMap.set(ancestor, observers);
|
|
}
|
|
observers.add(observer);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import('selector-observer').Observer<T>} observer
|
|
*/
|
|
_removeObserver(observer) {
|
|
const {element, ancestors, data} = observer;
|
|
|
|
this._elementMap.delete(element);
|
|
|
|
for (const ancestor of ancestors) {
|
|
const observers = this._elementAncestorMap.get(ancestor);
|
|
if (typeof observers === 'undefined') { continue; }
|
|
|
|
observers.delete(observer);
|
|
if (observers.size === 0) {
|
|
this._elementAncestorMap.delete(ancestor);
|
|
}
|
|
}
|
|
|
|
if (this._onRemoved !== null) {
|
|
this._onRemoved(element, data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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 = [];
|
|
let n = /** @type {?Node} */ (node);
|
|
while (n !== null) {
|
|
results.push(n);
|
|
if (n === root) { break; }
|
|
n = n.parentNode;
|
|
}
|
|
return results;
|
|
}
|
|
}
|