Home Reference Source

lib/delays.js

import { qs, selectorExists } from './dom';
import { promiseResolveReject } from './general';

/**
 * 1 second in milliseconds
 * @type {number}
 */
export const DelayAmount1Second = 1000;

/**
 * 2the second in milliseconds
 * @type {number}
 */
export const DelayAmount2Seconds = 2000;

/**
 * 3 second in milliseconds
 * @type {number}
 */
export const DelayAmount3Seconds = 3000;

/**
 * 4 second in milliseconds
 * @type {number}
 */
export const DelayAmount4Seconds = 4000;

/**
 * 5 second in milliseconds
 * @type {number}
 */
export const DelayAmount5Seconds = 5000;

/**
 * Converts the supplied n number of seconds into milliseconds
 * which is required for setTimeout and setInterval
 * @param {number} n - The number of seconds to be delayed
 * @return {number} - The delay amount in milliseconds
 */
export function secondsToDelayAmount(n) {
  return n * 1000;
}

/**
 * Returns a Promise that resolves once
 * the behavior has been un-paused
 * @return {Promise<any>}
 */
export function resolveWhenBehaviorUnPaused() {
  return new Promise(resolve => {
    let intervalId = setInterval(() => {
      if (!window.$WBBehaviorPaused) {
        clearInterval(intervalId);
        resolve();
      }
    }, 2000);
  });
}

/**
 * Polls the supplied function at the timeout rate
 * only if the behavior is not paused
 * @return {number} - The id of the interval
 */
export function setIntervalP(callback, timeout) {
  return setInterval(function() {
    if (!window.$WBBehaviorPaused) {
      callback();
    }
  }, timeout);
}

/**
 * Like setTimeout except that the timeout respects the pause state
 * of the behavior
 * @return {number} - The id of the timeout
 */
export function setTimeoutP(callback, timeout) {
  function execIfNotPaused() {
    if (!window.$WBBehaviorPaused) {
      callback();
    } else {
      setTimeout(execIfNotPaused, 500);
    }
  }

  return setTimeout(execIfNotPaused, timeout);
}

/**
 * Returns a Promise that resolves after waiting the amount of time
 * specified by delayTime.
 * @param {number} [delayTime = 3000] - The length of the delay in milliseconds
 * @return {Promise<void>}
 */
export function delay(delayTime) {
  return new Promise(resolve => {
    setTimeout(resolve, delayTime || 3000);
  });
}

/**
 * Returns a promise that resolves once the supplied predicate function
 * returns a truthy value.
 * @param {function(): boolean} predicate - A predicate function that
 * returns a truthy value to indicate the wait condition was satisfied otherwise
 * a falsy value to indicate wait condition not satisfied
 * @param {?WaitForOptions} [options] - Options controlling how the wait
 * will happen
 * @return {Promise<WaitResult>}
 */
export function waitForPredicate(predicate, options) {
  const opts = Object.assign({ pollRate: 1000 }, options);
  const results = { maxExceeded: false, predicate: false };
  let maxTo;
  return new Promise(resolve => {
    if (predicate()) {
      results.predicate = true;
      return resolve(results);
    }
    let int = setInterval(() => {
      if (predicate()) {
        results.predicate = true;
        clearInterval(int);
        if (maxTo) clearTimeout(maxTo);
        resolve(results);
      }
    }, opts.pollRate);
    if (opts.max && opts.max !== -1) {
      maxTo = setTimeout(() => {
        results.predicate = predicate();
        results.maxExceeded = true;
        clearInterval(int);
        resolve(results);
      }, opts.max);
    }
  });
}

/**
 * Calls `fromNode.querySelector` using the supplied selector. If the
 * return value of the first calls is null/undefined then waits for the same call
 * to return a truthy value and then returns the element the selector matches.
 * @param {SomeElement} fromNode - The element the to be selected `selector` from
 * @param {string} selector - The querySelector to use for `fromNode.querySelector`
 * call.
 * @param {?WaitForOptions} [options] - Options controlling the wait
 * @return {Promise<?SomeElement>}
 */
export async function waitForAndSelectElement(fromNode, selector, options) {
  const elem = qs(selector, fromNode);
  if (!elem) {
    const waitRet = await waitForPredicate(
      () => selectorExists(selector, fromNode),
      options
    );
    if (waitRet.predicate) return qs(selector, fromNode);
    return null;
  }
  return elem;
}

/**
 * Returns a promise that resolves once
 * `document.readyState === 'complete'`
 * @return {Promise<void>}
 */
export function domCompletePromise() {
  if (document.readyState !== 'complete') {
    return new Promise(r => {
      let i = setInterval(() => {
        if (document.readyState === 'complete') {
          clearInterval(i);
          r();
        }
      }, 1000);
    });
  }
  return Promise.resolve();
}

/**
 * Waits for the number children of the supplied parentElement
 * to become greater than the supplied parent elements `currentChildCount`.
 *
 * __Configuration__
 *
 * If a `max` is supplied (defaults to 15 seconds) the wait for additional children
 * to be added will not exceed the configured `max` value unless `max = -1`,
 * which in this case, the wait is infinite.
 *
 * When a `guard` function is supplied it is polled at value of `pollRate`
 * (defaults to 1 second), if and when it returns true the wait is considered
 * done regardless if the supplied parent element had additional children
 * added to it.
 *
 * @param {SomeElement} parentElement - The parent element
 * @param {WaitForOptions} [options] - Options controlling the wait
 * @return {Promise<WaitResult>}
 */
export function waitForAdditionalElemChildren(parentElement, options) {
  const opts = Object.assign({ pollRate: 1000, max: 15000 }, options);
  let int;
  let safety;
  const currentChildCount = parentElement.childElementCount;
  const results = { predicate: false, maxExceeded: false };
  return new Promise(resolve => {
    int = setInterval(() => {
      if (!parentElement.isConnected) {
        clearInterval(int);
        if (safety) clearTimeout(safety);
        return resolve(results);
      }
      if (opts.guard && opts.guard()) {
        clearInterval(int);
        if (safety) clearTimeout(safety);
        return resolve(results);
      }
      if (parentElement.childElementCount > currentChildCount) {
        clearInterval(int);
        if (safety) clearTimeout(safety);
        results.predicate = true;
        return resolve(results);
      }
    }, opts.pollRate);
    if (opts.max !== -1) {
      safety = setTimeout(() => {
        clearInterval(int);
        results.predicate = parentElement.childElementCount > currentChildCount;
        results.maxExceeded = true;
        resolve(results);
      }, opts.max);
    }
  });
}

/**
 * Waits for additional child elements to be added to the supplied parentElement
 * using a mutation observer.
 *
 * __Configuration__
 *
 * If a `max` is supplied (defaults to 15 seconds) the wait for additional children
 * to be added will not exceed the configured `max` value unless `max = -1`,
 * which in this case, the wait is infinite.
 *
 * When a `guard` function is supplied it is polled at value of `pollRate`
 * (defaults to 1 second), if and when it returns true the wait is considered
 * done regardless if the supplied parent element had additional children
 * added to it.
 *
 * @param {SomeElement} parentElement - The parent element
 * @param {GuardedWaitForOptions} [options] - Options controlling the wait
 * @return {Promise<WaitResult>}
 */
export function waitForAdditionalElemChildrenMO(parentElement, options) {
  const opts = Object.assign({ pollRate: 1000, max: 15000 }, options);
  let maxTo;
  let pollIn;
  const results = { predicate: false, maxExceeded: false };
  const prr = promiseResolveReject();
  let mutatationObz;
  const observer = () => {
    if (maxTo) clearTimeout(maxTo);
    if (pollIn) clearInterval(pollIn);
    results.predicate = true;
    if (mutatationObz) {
      mutatationObz.disconnect();
    }
    prr.resolve(results);
  };
  if (opts.guard && opts.guard()) {
    results.predicate = true;
    prr.resolve(results);
    return prr.promise;
  }
  mutatationObz = new MutationObserver(observer);
  mutatationObz.observe(parentElement, { childList: true });
  if (opts.guard) {
    pollIn = setInterval(() => {
      if (opts.guard()) {
        observer();
      }
    }, opts.pollRate);
  }
  if (opts.max !== -1) {
    maxTo = setTimeout(() => {
      if (pollIn !== -1) clearInterval(pollIn);
      results.maxExceeded = true;
      mutatationObz.disconnect();
      prr.resolve(results);
    }, opts.max);
  }
  return prr.promise;
}

/**
 * Waits for the supplied element to be removed from the document.
 * The condition, element removed from document, is polled at the configured
 * `pollRate` (defaults to 1 second) for a maximum of `max` time (defaults to 15 seconds)
 * unless `max = -1` which in this case there is no maximum.
 * @param {SomeElement} elem - The element that should be removed
 * @param {WaitForOptions} [options] - Options controlling the wait
 * @return {Promise<WaitResult>}
 */
export function waitUntilElementIsRemovedFromDom(elem, options) {
  const results = { predicate: false, maxExceeded: false };
  const opts = Object.assign({ pollRate: 1000, max: 15000 }, options);
  return new Promise(resolve => {
    if (!elem.isConnected) {
      results.predicate = true;
      return resolve(results);
    }
    let safety;
    const poll = setInterval(() => {
      if (!elem.isConnected) {
        if (safety) clearTimeout(safety);
        clearInterval(poll);
        results.predicate = true;
        return resolve(results);
      }
    }, opts.pollRate);
    if (opts.max !== -1) {
      safety = setTimeout(() => {
        clearInterval(poll);
        results.predicate = !elem.isConnected;
        results.maxExceeded = true;
        resolve(results);
      }, opts.max);
    }
  });
}

/**
 * Waits for the supplied selector to exist
 * @param {WaitForSelectorOptions} config - Options controlling the wait
 * @return {Promise<WaitResult>}
 */
export function waitForSelector({ selector, cntx, options }) {
  return waitForPredicate(() => selectorExists(selector, cntx), options);
}

/**
 * @typedef {Object} WaitForOptions
 * @property {number} [pollRate = 1000] - The rate at which the condition will be checked
 * @property {?number} [max = 15000] - The maximum length of time the polling will happen in milliseconds
 */

/**
 * @typedef {Object} GuardedWaitForOptions
 * @property {function(): boolean} [guard] - Function polled to determine if the wait
 * is considered done by additional conditions
 * @property {number} [pollRate = 1000] - The rate at which the condition will be checked
 * @property {?number} [max] - The maximum length of time the polling will happen in milliseconds
 */

/**
 * @typedef {Object} WaitResult
 * @property {boolean} predicate - T/F indicating if the stopping condition(s) of a wait was met, this includes guards
 * @property {boolean} maxExceeded - T/F indicating if the configured maximum wait time was exceeded
 */

/**
 * @typedef {Object} WaitForSelectorOptions
 * @property {string} selector - The selector to be waited for
 * @param {SomeElement|Document} [cntx] - element to use rather than document for the querySelector call
 * @param {WaitForOptions} [options] - Options controlling the wait
 */