event_bus.js

/* eslint-disable prettier/prettier */
/**
 * Simple event bus for an application. Listeners are attached using the `on`
 * and `off` methods. To raise an event, the `dispatch` method shall be used.
 * @ignore
 */
class EventBus {
  constructor(options) {
    this._listeners = Object.create(null);

    if (typeof PDFJSDev === "undefined" || PDFJSDev.test("MOZCENTRAL")) {
      this._isInAutomation =
        options !== null && options.isInAutomation === true;
    }
  }

  /**
   * @param {string} eventName
   * @param {function} listener
   * @param {Object} [options]
   */
  on(eventName, listener, options = null) {
    this._on(eventName, listener, {
      external: true,
      once: options && options.once,
    });
  }

  /**
   * @param {string} eventName
   * @param {function} listener
   * @param {Object} [options]
   */
  off(eventName, listener, options = null) {
    this._off(eventName, listener, {
      external: true,
      once: options && options.once,
    });
  }

  dispatch(eventName) {
    const eventListeners = this._listeners[eventName];
    if (!eventListeners || eventListeners.length === 0) {
      if (
        (typeof PDFJSDev === "undefined" || PDFJSDev.test("MOZCENTRAL")) &&
        this._isInAutomation
      ) {
        const args = Array.prototype.slice.call(arguments, 1);
        dispatchDOMEvent(eventName, args);
      }
      return;
    }
    // Passing all arguments after the eventName to the listeners.
    const args = Array.prototype.slice.call(arguments, 1);
    let externalListeners;
    // Making copy of the listeners array in case if it will be modified
    // during dispatch.
    eventListeners.slice(0).forEach(({ listener, external, once }) => {
      if (once) {
        this._off(eventName, listener);
      }
      if (external) {
        (externalListeners || (externalListeners = [])).push(listener);
        return;
      }
      listener.apply(null, args);
    });
    // Dispatch any "external" listeners *after* the internal ones, to give the
    // viewer components time to handle events and update their state first.
    if (externalListeners) {
      externalListeners.forEach(listener => {
        listener.apply(null, args);
      });
      externalListeners = null;
    }
    if (
      (typeof PDFJSDev === "undefined" || PDFJSDev.test("MOZCENTRAL")) &&
      this._isInAutomation
    ) {
      dispatchDOMEvent(eventName, args);
    }
  }

  /**
   * @ignore
   */
  _on(eventName, listener, options = null) {
    const eventListeners =
      this._listeners[eventName] || (this._listeners[eventName] = []);
    eventListeners.push({
      listener,
      external: options && options.external === true,
      once: options && options.once === true,
    });
  }

  /**
   * @ignore
   */
  _off(eventName, listener, options = null) {
    const eventListeners = this._listeners[eventName];
    if (!eventListeners) {
      return;
    }
    for (let i = 0, ii = eventListeners.length; i < ii; i++) {
      if (eventListeners[i].listener === listener) {
        eventListeners.splice(i, 1);
        return;
      }
    }
  }
}

/**
 * NOTE: Only used to support various PDF viewer tests in `mozilla-central`.
 */
function dispatchDOMEvent(eventName, args = null) {
  if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("MOZCENTRAL")) {
    throw new Error("Not implemented: dispatchDOMEvent");
  }
  const details = Object.create(null);
  if (args && args.length > 0) {
    const obj = args[0];
    for (const key in obj) {
      const value = obj[key];
      if (key === "source") {
        if (value === window || value === document) {
          return; // No need to re-dispatch (already) global events.
        }
        continue; // Ignore the `source` property.
      }
      details[key] = value;
    }
  }
  const event = document.createEvent("CustomEvent");
  event.initCustomEvent(eventName, true, true, details);
  document.dispatchEvent(event);
}

const WaitOnType = {
  EVENT: "event",
  TIMEOUT: "timeout",
};

/**
 * @typedef {Object} WaitOnEventOrTimeoutParameters
 * @property {Object} target - The event target, can for example be:
 *   `window`, `document`, a DOM element, or an {EventBus} instance.
 * @property {string} name - The name of the event.
 * @property {number} delay - The delay, in milliseconds, after which the
 *   timeout occurs (if the event wasn't already dispatched).
 */

/**
 * Allows waiting for an event or a timeout, whichever occurs first.
 * Can be used to ensure that an action always occurs, even when an event
 * arrives late or not at all.
 *
 * @param {WaitOnEventOrTimeoutParameters}
 * @returns {Promise} A promise that is resolved with a {WaitOnType} value.
 */
function waitOnEventOrTimeout({ target, name, delay = 0 }) {
  return new Promise(function (resolve, reject) {
    if (
      typeof target !== "object" ||
      !(name && typeof name === "string") ||
      !(Number.isInteger(delay) && delay >= 0)
    ) {
      throw new Error("waitOnEventOrTimeout - invalid parameters.");
    }

    function handler(type) {
      if (target instanceof EventBus) {
        target._off(name, eventHandler);
      } else {
        target.removeEventListener(name, eventHandler);
      }

      if (timeout) {
        clearTimeout(timeout);
      }
      resolve(type);
    }

    const eventHandler = handler.bind(null, WaitOnType.EVENT);
    if (target instanceof EventBus) {
      target._on(name, eventHandler);
    } else {
      target.addEventListener(name, eventHandler);
    }

    const timeoutHandler = handler.bind(null, WaitOnType.TIMEOUT);
    const timeout = setTimeout(timeoutHandler, delay);
  });
}

export { EventBus, waitOnEventOrTimeout, WaitOnType };