Greasy Fork is available in English.

NH_web

Common patterns for working with the WEB API.

Ce script ne devrait pas être installé directement. C'est une librairie créée pour d'autres scripts. Elle doit être inclus avec la commande // @require https://update.greasyfork.org/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,
  };

}());