xsanda / DOM Waiter

// ==UserScript==
// @exclude *
//
// ==UserLibrary==
// @name DOM Waiter
// @description DOM helper to wait HTML elements
// @copyright 2020, Sergey Zaborovsky (seryiza.xyz)
// @license MIT
// @version 0.1.5
// ==/UserLibrary==
//
// ==OpenUserJs==
// @author Seryiza
// ==/OpenUserJs==
// ==/UserScript==
/* jshint esversion: 6 */

/**
 * Print any errors thrown by a function to log, and rethrow them. Useful for
 * asynchronous callbacks.
 */
function logCallback(cb) {
  return function (...args) {
    try {
      return cb(...args);
    }
    catch (e) {
      console.error(e);
      throw e;
    }
  };
}

/**
 * Default finder of HTML elements.
 *
 * @param {string} selector
 * @returns {HTMLElement[]}
 */
function findElementsFromDocument(selector, source = undefined) {
  if (source) return [...source].filter(el => el.matches(selector));
  return Array.from(document.querySelectorAll(selector));
}

/**
 * Check element by comparation of text content.
 *
 * @param {string} expectingText
 * @param {HTMLElement} element
 */
function checkElementByTextContent(expectingText, element) {
  return element.textContent === expectingText;
}

/**
 * Get checker function from multiple types.
 *
 * @param {function|string|undefined} checker
 */
function getCheckerFunction(checker) {
  if (checker instanceof Function) {
    return checker;
  }

  if (typeof checker === 'string') {
    return checkElementByTextContent.bind(null, checker);
  }

  return function () {
    return true;
  }
}

/**
 * Create wait function.
 *
 * @param {function} elementsFinder
 */
function Waiter(findElementsFn = findElementsFromDocument) {
  return function (selector, checker) {
    const checkerFn = getCheckerFunction(checker);
    const select = (source = undefined) => {
      const elements = findElementsFn(selector);
      if (elements.length === 0) {
        return;
      }

      const checkedElements = elements.filter(checkerFn);
      if (checkedElements.length === 0) {
        return;
      }

      return (checkedElements.length > 1) ? checkedElements : checkedElements[0];
    };

    const selected = select();
    if (selected) {
      return Promise.resolve(selected);
    }

    return new Promise(logCallback((resolve) => {
      const observer = new MutationObserver(logCallback((mods) => {
        for (const mod of mods) {
          const selected = select(mod.addedNodes);
          if (selected) {
            observer.disconnect();
            resolve(selected);
            return;
          }
        }
      }));

      // Document because document.body may not yet be instantiated
      observer.observe(document, {
        childList: true,
        subtree: true,
      });
    }));
  }
}

window.Waiter = Waiter;