您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
simple toolkit to help me create userscripts
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.org/scripts/526417/1666689/USToolkit.js
// ==UserScript== // @name USToolkit // @namespace https://greasyfork.org/pt-BR/users/821661 // @version 0.0.7 // @run-at document-start // @match https://*/* // @author hdyzen // @description simple toolkit to create userscripts (alpha) // @license MIT // ==/UserScript== /** * Some functions are strongly inspired by: * github.com/violentmonkey/ * github.com/gorhill/uBlock/ * */ (() => { /** * Sets up a MutationObserver to watch for DOM changes and executes a callback function. * @param {function(MutationRecord[]): (boolean|void)} func The callback function to execute on mutation. * It receives an array of MutationRecord objects. If the function returns `true`, the observer is disconnected. * @param {MutationObserverInit} [options={ childList: true, subtree: true }] The options object for the MutationObserver. * @param {Node} [scope=document] The target node to observe. * @returns {MutationObserver} The created MutationObserver instance. */ function observe(func, options = { childList: true, subtree: true }, scope = document) { const observer = new MutationObserver((mut) => { const shouldDisconnect = func(mut); if (shouldDisconnect === true) { observer.disconnect(); } }); observer.observe(scope, options); return observer; } class OnElements { #rules = new Map(); #observedRoots = new WeakSet(); #combinedSelector = ""; #originalAttachShadow = unsafeWindow.Element.prototype.attachShadow; #isObserving = false; #activeObservers = new Set(); #observerOptions = { childList: true, subtree: true, attributes: true }; #deep; #root; constructor({ root = document, deep = false, observeOptions } = {}) { this.#root = root; this.#deep = deep; this.#observerOptions = observeOptions || this.#observerOptions; } add(selector, callback) { if (!this.#rules.has(selector)) { this.#rules.set(selector, new Set()); } this.#rules.get(selector).add(callback); this.#updateCombinedSelector(); return this; } once(selector, callback) { const onceFn = (element) => { callback(element); this.remove(selector, callback); }; this.add(selector, onceFn); return this; } per(selector, callback) { const executedElements = new WeakSet(); const perElementFn = (element) => { if (executedElements.has(element)) return; callback(element); executedElements.add(element); }; this.add(selector, perElementFn); return this; } remove(selector, callback) { if (!this.#rules.has(selector)) return; if (!callback) { this.#rules.delete(selector); this.#updateCombinedSelector(); return; } const rule = this.#rules.get(selector); rule.delete(callback); if (rule.size === 0) { this.#rules.delete(selector); } this.#updateCombinedSelector(); return this; } start() { if (this.#isObserving) return; if (this.#deep === true) this.#patchAttachShadow(); if (this.#deep === true) this.#observerExistentShadows(); this.#observe(this.#root); this.#isObserving = true; } stop() { if (!this.#isObserving) return; this.#activeObservers.forEach((observer) => { observer.disconnect(); }); this.#activeObservers.clear(); unsafeWindow.Element.prototype.attachShadow = this.#originalAttachShadow; this.#isObserving = false; } #observe(root) { if (this.#observedRoots.has(root)) { return; } this.#processExistingElements(root); const processedInBatch = new Set(); const processElement = (node) => { if (processedInBatch.has(node)) return; this.#routeElement(node); processedInBatch.add(node); }; const observer = new MutationObserver((mutations) => { if (!this.#combinedSelector) return; for (const mutation of mutations) { if (mutation.type === "attributes" && this.#combinedSelector && mutation.target.matches(this.#combinedSelector)) { processElement(mutation.target); continue; } for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue; if (this.#combinedSelector && node.matches(this.#combinedSelector)) { processElement(node); } if (this.#combinedSelector) { node.querySelectorAll(this.#combinedSelector).forEach((el) => { processElement(el); }); } } } processedInBatch.clear(); }); observer.observe(root, this.#observerOptions); this.#activeObservers.add(observer); this.#observedRoots.add(root); } #processExistingElements(root) { if (!this.#combinedSelector) return; for (const node of queryAll(root, this.#combinedSelector)) { this.#routeElement(node); } } #patchAttachShadow() { const self = this; unsafeWindow.Element.prototype.attachShadow = function (init) { const shadowRoot = self.#originalAttachShadow.call(this, init); self.#observe(shadowRoot); return shadowRoot; }; } #routeElement(element) { for (const [selector, callbacks] of this.#rules.entries()) { if (!element.matches(selector)) { continue; } for (const callback of [...callbacks]) { if (callback(element) !== true) { continue; } this.remove(selector, callback); } } } #observerExistentShadows() { for (const node of queryAll(document, "*")) { if (node.shadowRoot) this.#observe(node.shadowRoot); } } #updateCombinedSelector() { this.#combinedSelector = [...this.#rules.keys()].join(","); } } /** * Registers a callback to be executed when an element matching a selector is added to the DOM. * @param {string} selector The CSS selector of the element to watch for. * @param {function(Element): void} callback The callback to execute with the found element. * @returns {OnElements} The OnElements instance. */ function onElement(selector, callback) { const observer = new OnElements({ deep: true }); observer.add(selector, callback).start(); return observer; } /** * Returns a generator that iterates over all text nodes in a scope, * including those inside Shadow DOMs. * @param {Node} [scope=document.body] The root node from which to start the search. * @returns {Generator<Text>} A generator that yields text nodes. */ function* getTextNodes(scope = document.body) { if (!scope) return; for (const element of queryAll(scope, "*")) { for (const node of element.childNodes) { if (node.nodeType === Node.TEXT_NODE) { yield node; } } } } /** * A generator function that finds and yields all shadow roots within the given scope. * * This function iterates through all elements in the provided scope and checks for the existence of a shadowRoot. * * @param {Scope} scope The Document, DocumentFragment, or HTMLElement to search within. * @yields {ShadowRoot} The found shadow root. */ function* getShadowRoots(scope) { for (const element of scope.querySelectorAll("*")) { if (element.shadowRoot) { yield element.shadowRoot; } } } /** * A recursive query selector that traverses into Shadow DOMs. * @param {Node} scope The root node to start the search from. * @param {string} selector The CSS selector to match. * @returns {Generator<Element>} A generator that yields matching elements. */ function* queryAll(scope, selector) { for (const element of scope.querySelectorAll("*")) { if (element.matches(selector)) { yield element; } if (element.shadowRoot) { yield* queryAll(element.shadowRoot, selector); } } } /** * Finds the first element that matches a selector, including inside Shadow DOMs. * @param {Node} scope The root node to start the search from. * @param {string} selector The CSS selector to match. * @returns {Element|null} The first matching element, or null if not found. */ function query(scope, selector) { const iterator = queryAll(scope, selector); const result = iterator.next(); return result.done ? null : result.value; } /** * Finds the closest ancestor element that matches a selector, traversing up through Shadow DOMs. * @param {Element} element The starting element. * @param {string} selector The CSS selector to match against ancestors. * @returns {Element|null} The closest matching ancestor, or null. */ function closest(element, selector) { let node = element; while (node) { const found = node.closest(selector); if (found) { return found; } const root = node.getRootNode(); if (root instanceof ShadowRoot) { node = root.host; continue; } break; } return null; } /** * Injects a script into the document head for execution. * @param {string} code The JavaScript code to inject. */ function injectScriptInline(code) { const script = document.createElement("script"); if (typeof code === "string") { script.textContent = code; } if (typeof code === "function") { script.textContent = code.toString(); } (document.head || document.documentElement).appendChild(script); script.remove(); return true; } /** * Waits for an element that matches a given CSS selector to appear in the DOM. * @param {string} selector The CSS selector for the element to wait for. * @param {number} [timeout=5000] The maximum time to wait in milliseconds. * @returns {Promise<HTMLElement>} A promise that resolves with the found element, or rejects on timeout. */ function waitElement(selector, timeout = 5000) { return new Promise((resolve, reject) => { const onEl = new OnElements({ deep: true }); onEl.once(selector, resolve).start(); setTimeout(() => { onEl.stop(); reject(); }, timeout); }); } /** * Attaches a delegated event listener to a scope. * @param {string} events The name of the event (e.g., 'click'). * @param {string} selector A CSS selector to filter the event target. * @param {function(Event): void} callback The event handler function. * @param {EventListenerOptions} options Options passed to eventListener. * @param {Node} [scope=document] The parent element to attach the listener to. */ function on(events, selector, callback, options, scope = document) { const handler = (event) => { if (closest(event.target, selector)) callback(event); }; for (const event of events.split(/\s+/)) { scope.addEventListener(event, handler, options); } return () => { for (const event of events) { scope.removeEventListener(event, handler, options); } }; } /** * Provides a simplified, proxied interface for accessing and modifying * Greasemonkey's persistent storage. This function abstracts `GM_getValue` * and `GM_setValue`, allowing for direct object and nested property access * as if they were in a regular JavaScript object. * * @returns {Proxy<object>} A proxy object that automatically synchronizes * a user's configuration or data with the userscript's storage. */ function storage() { const storageRoot = {}; return createDeepProxy(storageRoot, ({ action, path, prop, value }) => { if (action === "set") { GM_setValue(prop, value); return true; } if (action === "get") { const rootKey = path[0]; const result = GM_getValue(rootKey, {}); if (result === undefined) return undefined; if (path.length === 1) return result; return getNestedValue(result, path.slice(1)); } }); } /** * Sets a value in a nested object based on an array path. * @param {object} obj The object to modify. * @param {string[]} path The path to the property. * @param {*} value The value to set. */ function setNestedValue(obj, path, value) { let current = obj; for (let i = 0; i < path.length - 1; i++) { const p = path[i]; if (valType(current[p]) !== "object") { current[p] = {}; } current = current[p]; } current[path[path.length - 1]] = value; } /** * Gets a value from a nested object based on an array path. * @param {object} obj The object to search. * @param {string[]} path The path to the property. * @returns {*} The value of the nested property or undefined. */ function getNestedValue(obj, path) { let current = obj; for (const p of path) { if (valType(current) !== "object") return undefined; if (!Object.hasOwn(current, p)) return undefined; current = current[p]; } return current; } /** * Creates a recursive proxy that intercepts property access (get) and modification (set). * @param {object} target The initial object to be proxied. * @param {function(object): any} callback The callback function to be invoked on interception. * @returns {Proxy} The proxied object. */ function createDeepProxy(target, callback) { const _createProxy = (currentTarget, currentPath) => { return new Proxy(currentTarget, { get(obj, prop) { const newPath = [...currentPath, prop]; const value = Reflect.get(obj, prop); const result = callback({ action: "get", path: newPath, prop, value, valueType: valType(value), target: obj, }); if (result !== undefined) { return result; } if (typeof value === "object" && value !== null) { return _createProxy(value, newPath); } return value; }, set(obj, prop, newValue) { const newPath = [...currentPath, prop]; const result = callback({ action: "set", path: newPath, prop, value: newValue, valueType: valType(newValue), target: obj, }); if (result !== undefined) { return result; } return Reflect.set(obj, prop, newValue); }, }); }; return _createProxy(target, []); } /** * Safely retrieves a nested property from an object using a string path. * Supports special wildcards for arrays ('[]') and objects ('{}' or '*'). * @param {object} obj The source object. * @param {string} chain A dot-separated string for the property path (e.g., 'user.address.street'). * @returns {*} The value of the nested property, or undefined if not found. */ function safeGet(obj, chain) { if (!obj || typeof chain !== "string" || chain === "") { return; } const props = chain.split("."); let current = obj; for (let i = 0; i < props.length; i++) { const prop = props[i]; if (current === undefined || current === null) { break; } if (prop === "[]") { i++; current = handleArray(current, props[i]); continue; } if (prop === "{}" || prop === "*") { i++; current = handleObject(current, props[i]); continue; } if (startsEndsWith(prop, "(", ")")) { current = handleFunction(current, prop); continue; } current = current[prop]; } return current; } /** * Safely handles function calls from the property chain. * It parses arguments as JSON. * @param {function} fn The function to call. * @param {string} prop The string containing arguments, e.g., '({"name": "test"})'. * @returns {*} The result of the function call. */ function handleFunction(fn, prop) { const argString = prop.slice(1, -1).trim().replaceAll("'", '"'); let args; if (argString === "") { return fn(); } try { args = JSON.parse(`[${argString}]`); } catch (err) { console.error(`[UST.safeGet] Failed to execute function in property chain "${prop}":`, err); } return typeof fn === "function" ? fn(...args) : undefined; } function _parseValue(value) { if (value === "true") { return true; } if (value === "false") { return false; } if (value === "null") { return null; } if (value === "undefined") { return undefined; } if (typeof value === "string" && (startsEndsWith(value, "'") || startsEndsWith(value, '"'))) { return value.slice(1, -1); } if (typeof value === "string" && value.trim() !== "") { const num = Number(value); return !Number.isNaN(num) ? num : value; } return value; } /** * Helper for `prop` to handle array wildcards. It maps over an array and extracts a property from each item. * @param {Array<object>} arr The array to process. * @param {string} nextProp The property to extract from each item. * @returns {*} An array of results, or a single result if only one is found. */ function handleArray(arr, nextProp) { const results = []; for (const item of arr) { if (getProp(item, nextProp) !== undefined) { results.push(item); } } return results; } /** * Helper for `prop` to handle object wildcards. It maps over an object's values and extracts a property. * @param {object} obj The object to process. * @param {string} nextProp The property to extract from each value. * @returns {*} An array of results, or a single result if only one is found. */ function handleObject(obj, nextProp) { const keys = Object.keys(obj); const results = []; for (const key of keys) { if (getProp(obj[key], nextProp) !== undefined) { results.push(obj[key]); } } return results; } /** * Safely gets an own property from an object. * @param {object} obj The source object. * @param {string} prop The property name. * @returns {*} The property value or undefined if it doesn't exist. */ function getProp(obj, prop) { if (obj && Object.hasOwn(obj, prop)) { return obj[prop]; } return; } /** * Checks if a value is a plain JavaScript object. * @param {*} val The value to check. * @returns {boolean} True if the value is a plain object, otherwise false. */ function isObject(val) { return Object.prototype.toString.call(val) === "[object Object]"; } /** * Checks if all properties and their values in the targetObject exist and are equal in the referenceObject. * @param {Object} referenceObject The object to compare against. * @param {Object} targetObject The object whose properties and values are checked for equality. * @returns {boolean} Returns true if all properties and values in targetObject are present and equal in referenceObject, otherwise false. */ function checkPropertyEquality(referenceObject, targetObject) { const entries = Object.entries(targetObject); for (const [prop, value] of entries) { if (!Object.hasOwn(referenceObject, prop)) { return false; } if (referenceObject[prop] !== value) { return false; } } return true; } /** * Checks if a value reference exists within a list of values. * @param {*} valueReference The value to check for. * @param {...*} values The list of values. * @returns {boolean} True if the value reference is found, otherwise false. */ function containsValue(valueReference, ...values) { for (const value of values) { if (valueReference === value) return true; } return false; } function startsEndsWith(string, ...searchs) { const [startSearch, endSearch] = searchs; const firstChar = string[0]; const lastChar = string[string.length - 1]; if (endSearch === undefined) { return firstChar === startSearch && lastChar === startSearch; } return firstChar === startSearch && lastChar === endSearch; } /** * Gets a more specific type of a value than `typeof`. * @param {*} val The value whose type is to be determined. * @returns {string} The type of the value (e.g., 'string', 'array', 'object', 'class', 'null'). */ function valType(val) { return Object.prototype.toString.call(val).slice(8, -1).toLowerCase(); } /** * Returns the length or size of the given target based on its type. * * Supported types: * - string: Returns the string's length. * - array: Returns the array's length. * - object: Returns the number of own enumerable properties. * - set: Returns the number of elements in the Set. * - map: Returns the number of elements in the Map. * - null: Returns 0. * * @param {*} target - The value whose length or size is to be determined. * @returns {number} The length or size of the target. * @throws {Error} If the type of target is unsupported. */ function len(target) { const type = valType(target); const types = { string: () => target.length, object: () => Object.keys(target).length, array: () => target.length, set: () => target.size, map: () => target.size, null: () => 0, }; if (types[type]) { return types[type](); } else { throw new Error(`Unsupported type: ${type}`); } } /** * Repeatedly calls a function with a delay until it returns `true`. * Uses `requestAnimationFrame` for scheduling. * @param {function(): (boolean|void)} func The function to run. The loop stops if it returns `true`. * @param {number} [time=250] The delay in milliseconds between executions. */ function update(func, time = 250) { const exec = () => { if (func() === true) { return; } setTimeout(() => { requestAnimationFrame(exec); }, time); }; requestAnimationFrame(exec); } /** * Runs a function on every animation frame until the function returns `true`. * @param {function(): (boolean|void)} func The function to execute. The loop stops if it returns `true`. */ function loop(func) { const exec = () => { if (func() === true) { return; } requestAnimationFrame(exec); }; requestAnimationFrame(exec); } /** * Injects a CSS string into the document by adoptedStyleSheets. * @param {string} css The CSS text to apply. * @returns {HTMLStyleElement} A promise that resolves with the created style element. */ function style(css) { const sheet = new CSSStyleSheet(); sheet.replaceSync(css); document.adoptedStyleSheets.push(sheet); for (const node of queryAll(document, "*")) { if (node.shadowRoot) { node.shadowRoot.adoptedStyleSheets.push(sheet); } } } /** * Creates a manager for a dynamic stylesheet, allowing for easy updates. * @param {string} id A unique identifier for the stylesheet. * @returns {object} An object with methods to manage the stylesheet. */ function createStyleManager(id) { const styleElement = document.createElement("style"); styleElement.id = id; document.head.appendChild(styleElement); let currentStyle = ""; return { /** * Adds or updates the stylesheet content. * @param {string} css The CSS string to apply. */ set(css) { currentStyle = css; styleElement.textContent = currentStyle; }, /** * Toggles the stylesheet on or off. * @param {boolean} enable If true, the stylesheet is enabled. Otherwise, it is disabled. */ toggle(enable) { if (enable) { styleElement.textContent = currentStyle; } else { styleElement.textContent = ""; } }, /** * Removes the stylesheet from the DOM. */ remove() { styleElement.remove(); }, }; } /** * Intercepts calls to an object's method using a Proxy, allowing modification of its behavior. * @param {object} owner The object that owns the method. * @param {string} methodName The name of the method to hook. * @param {ProxyHandler<function>} handler The proxy handler to intercept the method call. * @returns {function(): void} A function that, when called, reverts the method to its original implementation. */ function hook(owner, methodName, handler) { const originalMethod = owner[methodName]; if (typeof originalMethod !== "function") { throw new Error(`[UST.patch] The method “${methodName}” was not found in the object "${owner}".`); } const proxy = new Proxy(originalMethod, handler); owner[methodName] = proxy; return () => { owner[methodName] = originalMethod; }; } /** * An object to execute callbacks based on changes in the page URL, useful for Single Page Applications (SPAs). */ const watchUrl = { _enabled: false, _onUrlRules: [], /** * Adds a URL pattern and a callback to execute when the URL matches. * @param {string|RegExp} pattern The URL pattern to match against. Can be a string or a RegExp. * @param {function(): void} func The callback to execute on match. */ add(pattern, func) { const isRegex = pattern instanceof RegExp; const patternRule = pattern.startsWith("/") ? unsafeWindow.location.origin + pattern : pattern; this._onUrlRules.push({ pattern: patternRule, func, isRegex }); if (this._enabled === false) { this._enabled = true; this.init(); } }, /** * @private * Initializes the URL watching mechanism. */ init() { const exec = (currentUrl) => { const ruleFound = this._onUrlRules.find((rule) => (rule.isRegex ? rule.pattern.test(currentUrl) : rule.pattern === currentUrl)); if (ruleFound) { ruleFound.func(); } }; watchLocation(exec); }, }; /** * Monitors `location.href` for changes and triggers a callback. It handles history API changes (pushState, replaceState) * and popstate events, making it suitable for SPAs. * @param {function(string): void} callback The function to call with the new URL when a change is detected. */ function watchLocation(callback) { let previousUrl = location.href; const observer = new MutationObserver(() => checkForChanges()); observer.observe(unsafeWindow.document, { childList: true, subtree: true }); const checkForChanges = () => { requestAnimationFrame(() => { const currentUrl = location.href; if (currentUrl !== previousUrl) { previousUrl = currentUrl; callback(currentUrl); } }); }; const historyHandler = { apply(target, thisArg, args) { const result = Reflect.apply(target, thisArg, args); checkForChanges(); return result; }, }; hook(history, "pushState", historyHandler); hook(history, "replaceState", historyHandler); unsafeWindow.addEventListener("popstate", checkForChanges); callback(previousUrl); } /** * A promise-based wrapper for the Greasemonkey `GM_xmlhttpRequest` function. * @param {object} options The options for the request, matching the `GM_xmlhttpRequest` specification. * @returns {Promise<object>} A promise that resolves with the response object on success or rejects on error/timeout. */ function request(options) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ onload: resolve, onerror: reject, ontimeout: reject, ...options, }); }); } /** * Extracts data from an element based on an array of property path definitions. * @param {HTMLElement} element The root element to extract properties from. * @param {Array<string>} propsArray Array of property definitions, e.g., ["name:innerText", "link:href"]. * @returns {object} An object containing the extracted data. */ function extractProps(element, propsArray) { const data = {}; for (const propDefinition of propsArray) { const [label, valuePath] = propDefinition.split(":"); if (valuePath) { data[label] = safeGet(element, valuePath); } else { data[label] = safeGet(element, label); } } return data; } /** * @private * Handles a string rule in the scrape schema. * @param {HTMLElement} container The container element. * @param {string} rule The CSS selector for the target element. * @returns {string|null} The text content of the found element, or null. */ function _handleStringRule(container, rule) { const element = container.querySelector(rule); return element ? element.textContent.trim() : null; } /** * @private * Handles an array rule in the scrape schema. * @param {HTMLElement} container The container element. * @param {Array<string>} rule An array where the first item is a sub-selector and the rest are property definitions. * @returns {object} The extracted properties from the sub-element. */ function _handleArrayRule(container, rule) { const [subSelector, ...propsToGet] = rule; if (!subSelector) { throw new Error("[UST.scrape] No subselector provided as the first item in the rule"); } const element = container.querySelector(subSelector); return extractProps(element, propsToGet); } const ruleHandlers = { string: _handleStringRule, array: _handleArrayRule, }; /** * @private * Determines the type of a scrape rule. * @param {*} rule The rule to check. * @returns {string} The type of the rule ('string', 'array', or 'unknown'). */ function _getRuleType(rule) { if (typeof rule === "string") return "string"; if (Array.isArray(rule)) return "array"; return "unknown"; } /** * @private * Processes an object schema for scraping. * @param {HTMLElement} container The container element. * @param {object} schema The schema object. * @returns {object} The scraped data object. */ function _processObjectSchema(container, schema) { const item = {}; for (const key in schema) { const rule = schema[key]; const ruleType = _getRuleType(rule); const handler = ruleHandlers[ruleType]; if (handler) { item[key] = handler(container, rule); continue; } console.warn(`[UST.scrape] Rule for key “${key}” has an unsupported type.`); } return item; } /** * @private * Processes a single container element based on the provided schema. * @param {HTMLElement} container The container element to process. * @param {object|Array<string>} schema The schema to apply. * @returns {object} The scraped data. */ function _processContainer(container, schema) { if (Array.isArray(schema)) { return extractProps(container, schema); } if (isObject(schema)) { return _processObjectSchema(container, schema); } console.warn("[UST.scrape] Invalid schema format."); return {}; } /** * Scrapes structured data from the DOM based on a selector and a schema. * @param {string} selector CSS selector for the container elements to scrape. * @param {object|Array<string>} schema Defines the data to extract from each container. * @param {function(HTMLElement, object): void} func A callback for each scraped item, receiving the container element and the extracted data object. * @param {Node} [scope=document] The scope within which to search for containers. * @returns {Array<object>} An array of the scraped data objects. */ function scrape(selector, schema, func, scope = document) { const containers = scope.querySelectorAll(selector); const results = []; for (const container of containers) { const item = _processContainer(container, schema); func(container, item); results.push(item); } return results; } /** * Iterates over all elements matching a selector and applies a function to each. * @param {string} selector A CSS selector. * @param {function(Node): void} func The function to execute for each matching element. * @returns {NodeListOf<Element>} The list of nodes found. */ function each(selector, func) { const nodes = queryAll(document, selector); for (const node of nodes) { func(node); } return nodes; } /** * Chains multiple iterables together into a single sequence. * @param {...Iterable} iterables One or more iterable objects (e.g., arrays, sets). * @returns {Generator} A generator that yields values from each iterable in order. */ function* chain(...iterables) { for (const it of iterables) { yield* it; } } /** * Creates a debounced version of a function that delays its execution until after a certain time has passed * without it being called. * @param {function} func The function to debounce. * @param {number} delay The debounce delay in milliseconds. * @returns {function} The new debounced function. */ function debounce(func, delay) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), delay); }; } /** * Creates a throttled version of a function that only executes once per specified delay. * @param {Function} func - The function to throttle. * @param {number} delay - The number of milliseconds to wait before allowing the next execution. * @returns {Function} A throttled function that invokes `func` at most once every `delay` milliseconds. */ function throttle(func, delay) { let lastExecutedTime = 0; return function (...args) { const now = Date.now(); if (now - lastExecutedTime >= delay) { func.apply(this, args); lastExecutedTime = now; } }; } /** * Pauses execution for a specified number of milliseconds. * @param {number} ms The number of milliseconds to sleep. * @returns {Promise<void>} A promise that resolves after the specified time. */ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * A simple template engine that extends Map. It replaces `{{placeholder}}` syntax in strings. * @extends Map */ class Templates extends Map { /** * Fills a template with the provided data. * @param {*} key The key of the template stored in the map. * @param {object} [data={}] An object with key-value pairs to replace placeholders. * @returns {string|null} The template string with placeholders filled, or null if the template is not found. */ fill(key, data = {}) { const template = super.get(key); if (!template) { console.warn(`[UST.Templates] Template with key “${key}” not found.`); return null; } return template.replace(/\{\{(\s*\w+\s*)\}\}/g, (match, placeholder) => (Object.hasOwn(data, placeholder) ? data[placeholder] : match)); } /** * Renders a template into a DocumentFragment. * @param {*} key The key of the template stored in the map. * @param {object} [data={}] An object with data to fill the placeholders. * @returns {DocumentFragment|null} A document fragment containing the rendered HTML, or null if the template is not found. */ render(key, data = {}) { const filledHtml = this.fill(key, data); if (filledHtml === null) { return null; } const templateElement = document.createElement("template"); templateElement.innerHTML = filledHtml; return templateElement.content.cloneNode(true); } } /** * Factory function to create a new Templates instance. * @returns {Templates} A new instance of the Templates class. */ function templates() { return new Templates(); } /** * A class for creating lazy, chainable operations (map, filter, take) on iterables. * Operations are only executed when the sequence is consumed. */ class LazySequence extends Array { /** * @param {Iterable<any>} iterable The initial iterable. */ constructor(iterable) { super(); this.iterable = iterable; } /** * Creates a new lazy sequence with a mapping function. * @param {function(*): *} func The mapping function. * @returns {LazySequence} A new LazySequence instance. */ map(func) { const self = this; return new LazySequence({ *[Symbol.iterator]() { for (const value of self.iterable) { yield func(value); } }, }); } /** * Creates a new lazy sequence with a filtering function. * @param {function(*): boolean} func The filtering function. * @returns {LazySequence} A new LazySequence instance. */ filter(func) { const self = this; return new LazySequence({ *[Symbol.iterator]() { for (const value of self.iterable) { if (func(value)) { yield value; } } }, }); } /** * Creates a new lazy sequence that takes only the first n items. * @param {number} n The number of items to take. * @returns {LazySequence} A new LazySequence instance. */ take(n) { const self = this; return new LazySequence({ *[Symbol.iterator]() { let count = 0; for (const value of self.iterable) { if (count >= n) break; yield value; count++; } }, }); } /** * Makes the LazySequence itself iterable. */ *[Symbol.iterator]() { yield* this.iterable; } /** * Executes all lazy operations and returns the results as an array. * @returns {Array<*>} An array containing all values from the processed iterable. */ collect() { return [...this.iterable]; } } /** * Factory function to create a new LazySequence. * @param {Iterable<any>} iterable An iterable to wrap. * @returns {LazySequence} A new LazySequence instance. */ function lazy(iterable) { return new LazySequence(iterable); } /** * Adds an event listener that runs during the capturing phase, allowing you to * intercept and manipulate events before they reach normal bubbling listeners. * * @param {EventTarget} target The element or object (e.g., `window` or `document`) * to listen for the event on. * @param {string} eventType The type of event to intercept (e.g., 'click', 'keydown'). * @param {function(Event): void} callback The callback function to execute. * @returns {function(): void} A cleanup function to remove the event listener. */ function interceptEvent(target, eventType, callback) { target.addEventListener(eventType, callback, true); return () => { target.removeEventListener(eventType, callback, true); }; } /** * Creates a DocumentFragment and populates it using a callback. * This is useful for building a piece of DOM in memory before attaching it to the live DOM. * @param {function(DocumentFragment): void} builderCallback A function that receives a document fragment and can append nodes to it. * @returns {DocumentFragment} The populated document fragment. */ function createFromFragment(builderCallback) { const fragment = document.createDocumentFragment(); builderCallback(fragment); return fragment; } /** * Detaches an element from the DOM, runs a callback to perform modifications, and then re-attaches it. * This can improve performance by preventing multiple browser reflows and repaints during manipulation. * @param {HTMLElement|string} elementOrSelector The element or its CSS selector. * @param {function(HTMLElement): void} callback The function to execute with the detached element. */ function withDetached(elementOrSelector, callback) { const element = typeof elementOrSelector === "string" ? document.querySelector(elementOrSelector) : elementOrSelector; if (!element || !element.parentElement) return; const parent = element.parentElement; const nextSibling = element.nextElementSibling; parent.removeChild(element); try { callback(element); } finally { parent.insertBefore(element, nextSibling); } } /** * Gets the currently focused element, traversing into Shadow DOMs. * @param {Document} [doc=document] The document to start the search from. * @returns {Element|null} The active element or null. */ function getDeepActiveElement(doc = document) { let activeElement = doc.activeElement; while (activeElement?.shadowRoot?.activeElement) { activeElement = activeElement.shadowRoot.activeElement; } return activeElement; } /** * Gets the element at a specific coordinate, traversing into Shadow DOMs. * @param {Document} [doc=document] The document to start the search from. * @param {number} x The x-coordinate. * @param {number} y The y-coordinate. * @returns {Element|null} The element at the coordinates or null. */ function getDeepElementFromPoint(doc = document, x, y) { let elementInPoint = doc.elementFromPoint(x, y); while (elementInPoint?.shadowRoot?.elementFromPoint) { elementInPoint = elementInPoint.shadowRoot.elementFromPoint(x, y); } return elementInPoint; } /** * Sets the value of an input element and dispatches a native input event. * @param {HTMLInputElement} input The input element. * @param {*} value The value to set. */ function setInputNativeValue(input, value) { const prototype = Object.getPrototypeOf(input); const valueSetter = Object.getOwnPropertyDescriptor(prototype, "value").set; valueSetter.call(input, value); const event = new Event("input", { bubbles: true }); input.dispatchEvent(event); } /** * Simulates a user typing a string into an input element. * @param {HTMLInputElement} inputElement The input element to type into. * @param {string} text The string to type. */ function simulateTyping(inputElement, text) { let i = 0; let currentText = ""; inputElement.focus(); while (i < text.length) { const char = text[i]; i++; currentText += char; const keyDownEvent = new KeyboardEvent("keydown", { key: char, code: `Key${char.toUpperCase()}`, char: char, keyCode: char.charCodeAt(0), bubbles: true, }); inputElement.dispatchEvent(keyDownEvent); const inputEvent = new InputEvent("input", { bubbles: true, data: char, inputType: "insertText", isComposing: false }); setInputNativeValue(inputElement, currentText); inputElement.dispatchEvent(inputEvent); const keyUpEvent = new KeyboardEvent("keyup", { key: char, code: `Key${char.toUpperCase()}`, char: char, keyCode: char.charCodeAt(0), bubbles: true, }); inputElement.dispatchEvent(keyUpEvent); } } /** * Registers a global keyboard shortcut (hotkey). * @param {string} keys A string representing the key combination (e.g., 'ctrl+shift+s'). * @param {function(KeyboardEvent): void} callback The function to execute when the hotkey is pressed. * @returns {function(): void} A function to unregister the hotkey. */ function createHotkeys(keys, callback) { const keyParts = new Set(keys.toLowerCase().split("+")); const handler = (event) => { const activeElement = getDeepActiveElement(); if (activeElement && (["INPUT", "TEXTAREA"].includes(activeElement.tagName) || activeElement.isContentEditable === true)) { return; } const pressedKeys = new Set(); if (event.ctrlKey) pressedKeys.add("ctrl"); if (event.altKey) pressedKeys.add("alt"); if (event.shiftKey) pressedKeys.add("shift"); pressedKeys.add(event.key.toLowerCase()); const isMatch = [...keyParts].every((key) => pressedKeys.has(key)); if (isMatch) { event.preventDefault(); callback(event); } }; return interceptEvent(window, "keydown", handler); } window.UST = window.UST || {}; Object.assign(window.UST, { observe, OnElements, onElement, getTextNodes, getShadowRoots, queryAll, query, closest, injectScriptInline, waitElement, on, storage, setNestedValue, getNestedValue, createDeepProxy, safeGet, handleArray, handleObject, checkPropertyEquality, getProp, isObject, containsValue, valType, len, update, loop, style, createStyleManager, hook, watchUrl, watchLocation, request, extractProps, scrape, each, chain, debounce, throttle, sleep, templates, lazy, interceptEvent, createFromFragment, withDetached, getDeepActiveElement, getDeepElementFromPoint, setInputNativeValue, simulateTyping, createHotkeys, }); })();