您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Common patterns for working with the WEB API.
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/478440/1304193/NH_web.js
// ==UserScript== // ==UserLibrary== // @name NH_web // @description Common patterns for working with the WEB API. // @version 6 // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0-standalone.html // @homepageURL https://github.com/nexushoratio/userscripts // @supportURL https://github.com/nexushoratio/userscripts/issues // @match https://www.example.com/* // ==/UserLibrary== // ==/UserScript== window.NexusHoratio ??= {}; window.NexusHoratio.web = (function web() { 'use strict'; /** @type {number} - Bumped per release. */ const version = 6; const NH = window.NexusHoratio.base.ensure( [{name: 'base', minVersion: 36}] ); /** * Run querySelector to get an element, then click it. * @param {Element} base - Where to start looking. * @param {string[]} selectorArray - CSS selectors to use to find an * element. * @param {boolean} [matchSelf=false] - If a CSS selector would match base, * then use it. * @returns {boolean} - Whether an element could be found. */ function clickElement(base, selectorArray, matchSelf = false) { if (base) { for (const selector of selectorArray) { let el = null; if (matchSelf && base.matches(selector)) { el = base; } else { el = base.querySelector(selector); } if (el) { el.click(); return true; } } } return false; } /** * Bring the Browser's focus onto element. * @param {Element} element - HTML Element to focus on. */ function focusOnElement(element) { if (element) { const magicTabIndex = -1; const tabIndex = element.getAttribute('tabindex'); element.setAttribute('tabindex', magicTabIndex); element.focus(); if (tabIndex) { element.setAttribute('tabindex', tabIndex); } else { element.removeAttribute('tabindex'); } } } /** * Post a bunch of information about an HTML element to issues. * @param {Element} element - Element to get information about. * @param {string} name - What area this information came from. */ function postInfoAboutElement(element, name) { const msg = `An unsupported element from "${name}" discovered:`; NH.base.issues.post(msg, element.outerHTML); } /** * Determines if the element accepts keyboard input. * @param {Element} element - HTML Element to examine. * @returns {boolean} - Indicating whether the element accepts keyboard * input. */ function isInput(element) { let tagName = ''; if ('tagName' in element) { tagName = element.tagName.toLowerCase(); } // eslint-disable-next-line no-extra-parens return (element.isContentEditable || ['input', 'textarea'].includes(tagName)); } /** * @typedef {object} Continuation * @property {boolean} done - Indicate whether the monitor is done * processing. * @property {object} [results] - Optional results object. */ /** * @callback Monitor * @param {MutationRecord[]} records - Standard mutation records. * @returns {Continuation} - Indicate whether done monitoring. */ /** * Simple function that takes no parameters and returns nothing. * @callback SimpleFunction */ /** * @typedef {object} OtmotWhat * @property {string} name - The name for this observer. * @property {Element} base - Element to observe. */ /** * @typedef {object} OtmotHow * @property {object} observeOptions - MutationObserver().observe() options. * @property {SimpleFunction} [trigger] - Function to call that triggers * observable results. * @property {Monitor} monitor - Callback used to process MutationObserver * records. * @property {number} [timeout] - Time to wait for completion in * milliseconds, default of 0 disables. */ /** * MutationObserver callback for otmot. * * @param {MutationRecord[]} records - Standard mutation records. * @param {MutationObserver} observer - The invoking observer, enhanced with * extra properties by *otmot()*. * @returns {boolean} - The *done* value of the monitor function. */ const otmotMoCallback = (records, observer) => { const {done, results} = observer.monitor(records); observer.logger.log('monitor:', done, results); if (done) { observer.disconnect(); clearTimeout(observer.timeoutID); observer.logger.log('resolving'); observer.resolve(results); } return done; }; /** * One time mutation observer with timeout. * @param {OtmotWhat} what - What to observe. * @param {OtmotHow} how - How to observe. * @returns {Promise<Continuation.results>} - Will resolve with the results * from monitor when done is true. */ function otmot(what, how) { const prom = new Promise((resolve, reject) => { const observer = new MutationObserver(otmotMoCallback); const { name, base, } = what; const { observeOptions, trigger = () => {}, // eslint-disable-line no-empty-function timeout = 0, } = how; observer.monitor = how.monitor; observer.resolve = resolve; observer.logger = new NH.base.Logger(`otmot ${name}`); observer.timeoutID = null; /** Standard setTimeout callback. */ const toCallback = () => { observer.disconnect(); observer.logger.log('one last try'); if (!otmotMoCallback([], observer)) { observer.logger.log('rejecting after timeout'); reject(new Error(`otmot ${name} timed out`)); } }; if (timeout) { observer.timeoutID = setTimeout(toCallback, timeout); } observer.observe(base, observeOptions); trigger(); observer.logger.log('running'); // Call once at start in case we missed the change. otmotMoCallback([], observer); }); return prom; } /** * @typedef {object} OtrotWhat * @property {string} name - The name for this observer. * @property {Element} base - Element to observe. */ /** * @typedef {object} OtrotHow * @property {SimpleFunction} [trigger] - Function to call that triggers * observable events. * @property {number} timeout - Time to wait for completion in milliseconds. */ /** * ResizeObserver callback for otrot. * * @param {ResizeObserverEntry[]} entries - Standard resize records. * @param {ResizeObserver} observer - The invoking observer, enhanced with * extra properties by *otrot()*. * @returns {boolean} - Whether a resize was observed. */ const otrotRoCallback = (entries, observer) => { const {initialHeight, initialWidth} = observer; const {clientHeight, clientWidth} = observer.base; observer.logger.log('observed dimensions:', clientWidth, clientHeight); const resized = clientHeight !== initialHeight || clientWidth !== initialWidth; if (resized) { observer.disconnect(); clearTimeout(observer.timeoutID); observer.logger.log('resolving'); observer.resolve(observer.what); } return resized; }; /** * One time resize observer with timeout. * * Will resolve automatically upon first resize change. * @param {OtrotWhat} what - What to observe. * @param {OtrotHow} how - How to observe. * @returns {Promise<OtrotWhat>} - Will resolve with the what parameter. */ function otrot(what, how) { const prom = new Promise((resolve, reject) => { const observer = new ResizeObserver(otrotRoCallback); const { name, base, } = what; const { trigger = () => {}, // eslint-disable-line no-empty-function timeout, } = how; observer.base = base; observer.initialHeight = base.clientHeight; observer.initialWidth = base.clientWidth; observer.what = what; observer.resolve = resolve; observer.logger = new NH.base.Logger(`otrot ${name}`); observer.logger.log( 'initial dimensions:', observer.initialWidth, observer.initialHeight ); /** Standard setTimeout callback. */ const toCallback = () => { observer.disconnect(); observer.logger.log('one last try'); if (!otrotRoCallback([], observer)) { observer.logger.log('rejecting after timeout'); reject(new Error(`otrot ${name} timed out`)); } }; observer.timeoutID = setTimeout(toCallback, timeout); observer.observe(base); trigger(); observer.logger.log('running'); // Call once at start in case we missed the change. otrotRoCallback([], observer); }); return prom; } /** * @callback ResizeAction * @param {ResizeObserverEntry[]} entries - Standard resize entries. */ /** * @typedef {object} Otrot2How * @property {SimpleFunction} [trigger] - Function to call that triggers * observable events. * @property {ResizeAction} action - Function to call upon each event * observed and also at the end of duration. * @property {number} duration - Time to run in milliseconds. */ /** * ResizeObserver callback for otrot2. * * @param {ResizeObserverEntry[]} entries - Standard resize records. * @param {ResizeObserver} observer - The invoking observer, enhanced with * extra properties by *otrot()*. */ const otrot2RoCallback = (entries, observer) => { observer.logger.log('calling action'); observer.action(entries); }; /** * One time resize observer with action callback and duration. * * Will resolve upon duration expiration. Uses the same what parameter as * {@link otrot}. * @param {OtrotWhat} what - What to observe. * @param {Otrow2How} how - How to observe. * @returns {Promise<string>} - Will resolve after duration expires. */ function otrot2(what, how) { const prom = new Promise((resolve) => { const observer = new ResizeObserver(otrot2RoCallback); const { name, base, } = what; const { trigger = () => {}, // eslint-disable-line no-empty-function duration, } = how; observer.logger = new NH.base.Logger(`otrot2 ${name}`); observer.action = how.action; /** Standard setTimeout callback. */ const toCallback = () => { observer.disconnect(); observer.logger.log('one last call'); otrot2RoCallback([], observer); observer.logger.log('resolving'); resolve(`otrot2 ${name} finished`); }; setTimeout(toCallback, duration); observer.observe(base); trigger(); observer.logger.log('running'); // Call once at start in case we missed the change. otrot2RoCallback([], observer); }); return prom; } /** * Wait for selector to match using querySelector. * @param {string} selector - CSS selector. * @param {number} timeout - Time to wait in milliseconds, 0 disables. * @returns {Promise<Element>} - Matched element. */ function waitForSelector(selector, timeout) { const me = 'waitForSelector'; const logger = new NH.base.Logger(me); logger.entered(me, selector, timeout); /** * @implements {Monitor} * @returns {Continuation} - Indicate whether done monitoring. */ const monitor = () => { const element = document.querySelector(selector); if (element) { logger.log(`match for ${selector}`, element); return {done: true, results: element}; } logger.log('Still waiting for', selector); return {done: false}; }; const what = { name: me, base: document, }; const how = { observeOptions: {childList: true, subtree: true}, monitor: monitor, timeout: timeout, }; logger.leaving(me); return otmot(what, how); } return { version: version, clickElement: clickElement, focusOnElement: focusOnElement, postInfoAboutElement: postInfoAboutElement, isInput: isInput, otmot: otmot, otrot: otrot, otrot2: otrot2, waitForSelector: waitForSelector, }; }());