您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Useful library for dealing with the DOM.
此脚本不应直接安装,它是供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.icu/scripts/405802/823982/Monkey%20DOM.js
// ==UserScript== // @name DOM // @namespace https://rafaelgssa.gitlab.io/monkey-scripts // @version 4.1.6 // @author rafaelgssa // @description Useful library for dealing with the DOM. // @match *://*/* // @require https://greasyfork.org/scripts/405813-monkey-utils/code/Monkey%20Utils.js // ==/UserScript== /* global Utils */ /** * @typedef {(element?: Element) => void} ElementCallback * * @typedef {InsertPosition | 'atouter' | 'atinner'} ExtendedInsertPosition * * @typedef {ElementArrayConstructor<ElementArrayBase, 8>} ElementArray Any higher than 8 is too deep and does not work. * * **The definition for ElementArrayConstructor is in DOM.d.ts, as it is too complex for JSDoc:** * declare type ElementArrayConstructor< * T extends [any, any] | ElementArrayChildrenBase | null, * N extends number * > = T extends [infer A, infer B] * ? { * done: [A, B, ElementArrayChildrenBase | null]; * recurse: [ * A, * B, * ( * | ElementArrayConstructor<ElementArrayBase, ElementArrayDepth[N]>[] * | ElementArrayChildrenBase * | null * ) * ]; * }[N extends 0 ? 'done' : 'recurse'] * : T extends ElementArrayChildrenBase | null * ? T * : never; * * @typedef {[never, 0, 1, 2, 3, 4, 5, 6, 7]} ElementArrayDepth * * @typedef {{ [K in ElementTag]: [K, ElementAttributes<K> | null] }[ElementTag] | ElementArrayChildrenBase | null} ElementArrayBase * * @typedef {keyof HTMLElementTagNameMap} ElementTag * * @typedef {Object} ExtendedElementBase * @property {Record<string, string>} attrs * @property {NodeCallback} ref * * @typedef {ElementArray[] | ElementArrayChildrenBase} ElementArrayChildren * * @typedef {Node | string} ElementArrayChildrenBase * * @typedef {Object} MutationTypes * @property {boolean} [attributes] * @property {boolean} [childList] * @property {boolean} [subtree] * * @typedef {(node: Node) => void} NodeCallback */ /** * @template {ElementTag} T * @typedef {{ * [K in keyof ExtendedElement<T>]?: { * [L in keyof ExtendedElement<T>[K]]?: ExtendedElement<T>[K][L] | null; * } | null; * }} ElementAttributes */ /** * @template {ElementTag} T * @typedef {HTMLElementTagNameMap[T] & ExtendedElementBase} ExtendedElement */ // eslint-disable-next-line const DOM = (() => { const _parser = new DOMParser(); /** * Waits for an element that is dynamically added to the DOM. * @param {string} selectors The selectors to query for the element. * @param {number} [timeout] How long to wait for the element in seconds. Defaults to 60 (1 minute). * @param {number} [frequency] How often to keep checking for the element in seconds. Defaults to 1. * @returns {Promise<Element | undefined>} The element, if found. */ const dynamicQuerySelector = (selectors, timeout = 60, frequency = 1) => { return new Promise((resolve) => _checkElementExists(selectors, resolve, timeout, frequency)); }; /** * @param {string} selectors * @param {ElementCallback} callback * @param {number} [timeout] * @param {number} [frequency] */ const _checkElementExists = (selectors, callback, timeout = 60, frequency = 1) => { const element = document.querySelector(selectors); if (element) { callback(element); } else if (timeout > 0) { window.setTimeout( _checkElementExists, frequency * 1000, selectors, callback, timeout - frequency, frequency ); } else { callback(); } }; /** * Inserts elements in reference to another element based on element arrays that are visually similar to JSX. * @param {Element} referenceEl The element to use as reference. * @param {ExtendedInsertPosition} position Where to insert the elements. * @param {ElementArray[]} arrays The arrays to use. * @returns {(HTMLElement | undefined)[]} The inserted elements from the root level, if successful. * * @example * // `pElement` will contain the P element. * // `elements` will be an array containing the DIV and SPAN elements, in this order, if successful. * let pElement; * const elements = DOM.insertElement(document.body, 'beforeend', [ * ['div', { className: 'hello', onclick: () => {} }, [ * 'Hello, ', // This is added as a text node. * ['p', { ref: (ref) => pElement = ref }, 'John'], * '!' // This is added as a text node. * ]], * ['span', null, 'How are you?'] * ]); * * @example * // Using array destructuring. * // `divElement` will contain the DIV element and `spanElement` will contain the SPAN element, if successful. * let pElement; * const [divElement, spanElement] = DOM.insertElement(document.body, 'beforeend', [ * ['div', { className: 'hello', onclick: () => {} }, [ * 'Hello, ', // This is added as a text node. * ['p', { ref: (ref) => pElement = ref }, 'John'], * '!' // This is added as a text node. * ]], * ['span', null, 'How are you?'] * ]); */ const insertElements = (referenceEl, position, arrays) => { const docFragment = _buildFragment(arrays); if (!docFragment) { return []; } const elements = /** @type {HTMLElement[]} */ (Array.from(docFragment.children)); const referenceElParent = referenceEl.parentElement; switch (position) { case 'beforebegin': if (referenceElParent) { referenceElParent.insertBefore(docFragment, referenceEl); } break; case 'afterbegin': referenceEl.insertBefore(docFragment, referenceEl.firstElementChild); break; case 'beforeend': referenceEl.appendChild(docFragment); break; case 'afterend': if (referenceElParent) { referenceElParent.insertBefore(docFragment, referenceEl.nextElementSibling); } break; case 'atouter': if (referenceElParent) { referenceElParent.insertBefore(docFragment, referenceEl.nextElementSibling); referenceEl.remove(); } break; case 'atinner': referenceEl.innerHTML = ''; referenceEl.appendChild(docFragment); break; // no default } if (docFragment.children.length > 0) { return []; } return elements; }; /** * Builds a document fragment from element arrays. * @param {ElementArray[]} arrays The arrays to use. * @returns {DocumentFragment | undefined} The built document fragment, if successful. */ const _buildFragment = (arrays) => { if (!Array.isArray(arrays)) { return; } const docFragment = document.createDocumentFragment(); // @ts-ignore const filteredArrays = arrays.filter(Utils.isSet); for (const array of filteredArrays) { const element = _buildElement(array); if (element) { docFragment.appendChild(element); } } return docFragment; }; /** * Builds an element from an element array. * @param {ElementArray} array The array to use. * @returns {Node | undefined} The built element, if successful. */ const _buildElement = (array) => { if (!array) { return; } if (array instanceof Node) { return array; } if (typeof array === 'string') { return document.createTextNode(array); } const [tag, attributes, children] = array; const element = document.createElement(tag); if (attributes) { _setElementAttributes(element, attributes); } if (children) { _appendElementChildren(element, children); } return element; }; /** * Sets attributes for an element. * @template {ElementTag} T * @param {HTMLElement} element * @param {ElementAttributes<T>} attributes */ const _setElementAttributes = (element, attributes) => { const filteredAttributes = Object.entries(attributes).filter(([, value]) => Utils.isSet(value)); for (const [key, value] of filteredAttributes) { if (key === 'attrs' && typeof value === 'object') { _setCustomElementAttributes(element, value); } else if (key === 'ref' && typeof value === 'function') { value(element); } else if (key.startsWith('on') && typeof value === 'function') { const eventType = key.slice(2); element.addEventListener(eventType, value); } else if (typeof value === 'object') { _setElementProperties(element, key, value); } else { // @ts-ignore element[key] = value; } } }; /** * Sets custom attributes for an element. * @template {ElementTag} T * @param {HTMLElement} element * @param {ElementAttributes<T>} attributes */ const _setCustomElementAttributes = (element, attributes) => { const filteredAttributes = Object.entries(attributes).filter(([, value]) => Utils.isSet(value)); for (const [key, value] of filteredAttributes) { element.setAttribute(key, value); } }; /** * Sets properties for the attribute of an element. * @param {HTMLElement} element * @param {string} attribute * @param {Object} properties */ const _setElementProperties = (element, attribute, properties) => { const filteredProperties = Object.entries(properties).filter(([, value]) => Utils.isSet(value)); for (const [key, value] of filteredProperties) { // @ts-ignore element[attribute][key] = value; } }; /** * Appends children to an element from an element array. * @param {HTMLElement} element * @param {ElementArrayChildren} children */ const _appendElementChildren = (element, children) => { const docFragment = _buildFragment(Array.isArray(children) ? children : [children]); if (docFragment) { element.appendChild(docFragment); } }; /** * Observes a node for mutations. * @param {Node} node The node to observe. * @param {MutationTypes | null} types The types of mutations to observe. Defaults to the child list of the node and all its descendants. * @param {NodeCallback} callback The callback to call with each updated / added node. * @returns {MutationObserver} The observer. */ const observeNode = (node, types, callback) => { const observer = new MutationObserver((mutations) => _processNodeMutations(mutations, callback) ); observer.observe( node, types || { childList: true, subtree: true, } ); return observer; }; /** * @param {MutationRecord[]} mutations * @param {NodeCallback} callback */ const _processNodeMutations = (mutations, callback) => { for (const mutation of mutations) { if (mutation.type === 'attributes') { callback(mutation.target); } else { mutation.addedNodes.forEach(callback); } } }; /** * Parses an HTML string into a DOM. * @param {string} html The HTML string to parse. * @returns {Document} The parsed DOM. */ const parse = (html) => { return _parser.parseFromString(html, 'text/html'); }; return { dynamicQuerySelector, insertElements, observeNode, parse, }; })();