diff --git a/test/unit/clitests.json b/test/unit/clitests.json index 9d7352d5a..a64126390 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -23,6 +23,7 @@ "document_spec.js", "encodings_spec.js", "evaluator_spec.js", + "event_utils_spec.js", "function_spec.js", "message_handler_spec.js", "metadata_spec.js", diff --git a/test/unit/event_utils_spec.js b/test/unit/event_utils_spec.js new file mode 100644 index 000000000..50c7e8b5b --- /dev/null +++ b/test/unit/event_utils_spec.js @@ -0,0 +1,286 @@ +/* Copyright 2017 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + EventBus, + waitOnEventOrTimeout, + WaitOnType, +} from "../../web/event_utils.js"; +import { isNodeJS } from "../../src/shared/is_node.js"; + +describe("event_utils", function () { + describe("EventBus", function () { + it("dispatch event", function () { + const eventBus = new EventBus(); + let count = 0; + eventBus.on("test", function (evt) { + expect(evt).toEqual(undefined); + count++; + }); + eventBus.dispatch("test"); + expect(count).toEqual(1); + }); + it("dispatch event with arguments", function () { + const eventBus = new EventBus(); + let count = 0; + eventBus.on("test", function (evt) { + expect(evt).toEqual({ abc: 123 }); + count++; + }); + eventBus.dispatch("test", { + abc: 123, + }); + expect(count).toEqual(1); + }); + it("dispatch different event", function () { + const eventBus = new EventBus(); + let count = 0; + eventBus.on("test", function () { + count++; + }); + eventBus.dispatch("nottest"); + expect(count).toEqual(0); + }); + it("dispatch event multiple times", function () { + const eventBus = new EventBus(); + let count = 0; + eventBus.dispatch("test"); + eventBus.on("test", function () { + count++; + }); + eventBus.dispatch("test"); + eventBus.dispatch("test"); + expect(count).toEqual(2); + }); + it("dispatch event to multiple handlers", function () { + const eventBus = new EventBus(); + let count = 0; + eventBus.on("test", function () { + count++; + }); + eventBus.on("test", function () { + count++; + }); + eventBus.dispatch("test"); + expect(count).toEqual(2); + }); + it("dispatch to detached", function () { + const eventBus = new EventBus(); + let count = 0; + const listener = function () { + count++; + }; + eventBus.on("test", listener); + eventBus.dispatch("test"); + eventBus.off("test", listener); + eventBus.dispatch("test"); + expect(count).toEqual(1); + }); + it("dispatch to wrong detached", function () { + const eventBus = new EventBus(); + let count = 0; + eventBus.on("test", function () { + count++; + }); + eventBus.dispatch("test"); + eventBus.off("test", function () { + count++; + }); + eventBus.dispatch("test"); + expect(count).toEqual(2); + }); + it("dispatch to detached during handling", function () { + const eventBus = new EventBus(); + let count = 0; + const listener1 = function () { + eventBus.off("test", listener2); + count++; + }; + const listener2 = function () { + eventBus.off("test", listener1); + count++; + }; + eventBus.on("test", listener1); + eventBus.on("test", listener2); + eventBus.dispatch("test"); + eventBus.dispatch("test"); + expect(count).toEqual(2); + }); + + it("dispatch event to handlers with/without 'once' option", function () { + const eventBus = new EventBus(); + let multipleCount = 0, + onceCount = 0; + + eventBus.on("test", function () { + multipleCount++; + }); + eventBus.on( + "test", + function () { + onceCount++; + }, + { once: true } + ); + + eventBus.dispatch("test"); + eventBus.dispatch("test"); + eventBus.dispatch("test"); + + expect(multipleCount).toEqual(3); + expect(onceCount).toEqual(1); + }); + + it("should not re-dispatch to DOM", async function () { + if (isNodeJS) { + pending("Document is not supported in Node.js."); + } + const eventBus = new EventBus(); + let count = 0; + eventBus.on("test", function (evt) { + expect(evt).toEqual(undefined); + count++; + }); + function domEventListener() { + // Shouldn't get here. + expect(false).toEqual(true); + } + document.addEventListener("test", domEventListener); + + eventBus.dispatch("test"); + + await Promise.resolve(); + expect(count).toEqual(1); + + document.removeEventListener("test", domEventListener); + }); + }); + + describe("waitOnEventOrTimeout", function () { + let eventBus; + + beforeAll(function () { + eventBus = new EventBus(); + }); + + afterAll(function () { + eventBus = null; + }); + + it("should reject invalid parameters", async function () { + const invalidTarget = waitOnEventOrTimeout({ + target: "window", + name: "DOMContentLoaded", + }).then( + function () { + // Shouldn't get here. + expect(false).toEqual(true); + }, + function (reason) { + expect(reason instanceof Error).toEqual(true); + } + ); + + const invalidName = waitOnEventOrTimeout({ + target: eventBus, + name: "", + }).then( + function () { + // Shouldn't get here. + expect(false).toEqual(true); + }, + function (reason) { + expect(reason instanceof Error).toEqual(true); + } + ); + + const invalidDelay = waitOnEventOrTimeout({ + target: eventBus, + name: "pagerendered", + delay: -1000, + }).then( + function () { + // Shouldn't get here. + expect(false).toEqual(true); + }, + function (reason) { + expect(reason instanceof Error).toEqual(true); + } + ); + + await Promise.all([invalidTarget, invalidName, invalidDelay]); + }); + + it("should resolve on event, using the DOM", async function () { + if (isNodeJS) { + pending("Document is not supported in Node.js."); + } + const button = document.createElement("button"); + + const buttonClicked = waitOnEventOrTimeout({ + target: button, + name: "click", + delay: 10000, + }); + // Immediately dispatch the expected event. + button.click(); + + const type = await buttonClicked; + expect(type).toEqual(WaitOnType.EVENT); + }); + + it("should resolve on timeout, using the DOM", async function () { + if (isNodeJS) { + pending("Document is not supported in Node.js."); + } + const button = document.createElement("button"); + + const buttonClicked = waitOnEventOrTimeout({ + target: button, + name: "click", + delay: 10, + }); + // Do *not* dispatch the event, and wait for the timeout. + + const type = await buttonClicked; + expect(type).toEqual(WaitOnType.TIMEOUT); + }); + + it("should resolve on event, using the EventBus", async function () { + const pageRendered = waitOnEventOrTimeout({ + target: eventBus, + name: "pagerendered", + delay: 10000, + }); + // Immediately dispatch the expected event. + eventBus.dispatch("pagerendered"); + + const type = await pageRendered; + expect(type).toEqual(WaitOnType.EVENT); + }); + + it("should resolve on timeout, using the EventBus", async function () { + const pageRendered = waitOnEventOrTimeout({ + target: eventBus, + name: "pagerendered", + delay: 10, + }); + // Do *not* dispatch the event, and wait for the timeout. + + const type = await pageRendered; + expect(type).toEqual(WaitOnType.TIMEOUT); + }); + }); +}); diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index f73287ec2..7c3e5c6d6 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -68,6 +68,7 @@ async function initializePDFJS(callback) { "pdfjs-test/unit/document_spec.js", "pdfjs-test/unit/encodings_spec.js", "pdfjs-test/unit/evaluator_spec.js", + "pdfjs-test/unit/event_utils_spec.js", "pdfjs-test/unit/function_spec.js", "pdfjs-test/unit/fetch_stream_spec.js", "pdfjs-test/unit/message_handler_spec.js", diff --git a/test/unit/pdf_find_controller_spec.js b/test/unit/pdf_find_controller_spec.js index 73bb10cd9..8c6453aff 100644 --- a/test/unit/pdf_find_controller_spec.js +++ b/test/unit/pdf_find_controller_spec.js @@ -14,7 +14,7 @@ */ import { buildGetDocumentParams } from "./test_utils.js"; -import { EventBus } from "../../web/ui_utils.js"; +import { EventBus } from "../../web/event_utils.js"; import { getDocument } from "../../src/display/api.js"; import { PDFFindController } from "../../web/pdf_find_controller.js"; import { SimpleLinkService } from "../../web/pdf_link_service.js"; diff --git a/test/unit/ui_utils_spec.js b/test/unit/ui_utils_spec.js index 4b7bf147d..d262f1079 100644 --- a/test/unit/ui_utils_spec.js +++ b/test/unit/ui_utils_spec.js @@ -16,16 +16,12 @@ import { backtrackBeforeAllVisibleElements, binarySearchFirstItem, - EventBus, getPageSizeInches, getVisibleElements, isPortraitOrientation, isValidRotation, parseQueryString, - waitOnEventOrTimeout, - WaitOnType, } from "../../web/ui_utils.js"; -import { isNodeJS } from "../../src/shared/is_node.js"; describe("ui_utils", function () { describe("binary search", function () { @@ -56,153 +52,6 @@ describe("ui_utils", function () { }); }); - describe("EventBus", function () { - it("dispatch event", function () { - const eventBus = new EventBus(); - let count = 0; - eventBus.on("test", function (evt) { - expect(evt).toEqual(undefined); - count++; - }); - eventBus.dispatch("test"); - expect(count).toEqual(1); - }); - it("dispatch event with arguments", function () { - const eventBus = new EventBus(); - let count = 0; - eventBus.on("test", function (evt) { - expect(evt).toEqual({ abc: 123 }); - count++; - }); - eventBus.dispatch("test", { - abc: 123, - }); - expect(count).toEqual(1); - }); - it("dispatch different event", function () { - const eventBus = new EventBus(); - let count = 0; - eventBus.on("test", function () { - count++; - }); - eventBus.dispatch("nottest"); - expect(count).toEqual(0); - }); - it("dispatch event multiple times", function () { - const eventBus = new EventBus(); - let count = 0; - eventBus.dispatch("test"); - eventBus.on("test", function () { - count++; - }); - eventBus.dispatch("test"); - eventBus.dispatch("test"); - expect(count).toEqual(2); - }); - it("dispatch event to multiple handlers", function () { - const eventBus = new EventBus(); - let count = 0; - eventBus.on("test", function () { - count++; - }); - eventBus.on("test", function () { - count++; - }); - eventBus.dispatch("test"); - expect(count).toEqual(2); - }); - it("dispatch to detached", function () { - const eventBus = new EventBus(); - let count = 0; - const listener = function () { - count++; - }; - eventBus.on("test", listener); - eventBus.dispatch("test"); - eventBus.off("test", listener); - eventBus.dispatch("test"); - expect(count).toEqual(1); - }); - it("dispatch to wrong detached", function () { - const eventBus = new EventBus(); - let count = 0; - eventBus.on("test", function () { - count++; - }); - eventBus.dispatch("test"); - eventBus.off("test", function () { - count++; - }); - eventBus.dispatch("test"); - expect(count).toEqual(2); - }); - it("dispatch to detached during handling", function () { - const eventBus = new EventBus(); - let count = 0; - const listener1 = function () { - eventBus.off("test", listener2); - count++; - }; - const listener2 = function () { - eventBus.off("test", listener1); - count++; - }; - eventBus.on("test", listener1); - eventBus.on("test", listener2); - eventBus.dispatch("test"); - eventBus.dispatch("test"); - expect(count).toEqual(2); - }); - - it("dispatch event to handlers with/without 'once' option", function () { - const eventBus = new EventBus(); - let multipleCount = 0, - onceCount = 0; - - eventBus.on("test", function () { - multipleCount++; - }); - eventBus.on( - "test", - function () { - onceCount++; - }, - { once: true } - ); - - eventBus.dispatch("test"); - eventBus.dispatch("test"); - eventBus.dispatch("test"); - - expect(multipleCount).toEqual(3); - expect(onceCount).toEqual(1); - }); - - it("should not re-dispatch to DOM", async function () { - if (isNodeJS) { - pending("Document is not supported in Node.js."); - } - const eventBus = new EventBus(); - let count = 0; - eventBus.on("test", function (evt) { - expect(evt).toEqual(undefined); - count++; - }); - function domEventListener() { - // Shouldn't get here. - expect(false).toEqual(true); - } - document.addEventListener("test", domEventListener); - - eventBus.dispatch("test"); - - await Promise.resolve(); - expect(count).toEqual(1); - - document.removeEventListener("test", domEventListener); - }); - }); - describe("isValidRotation", function () { it("should reject non-integer angles", function () { expect(isValidRotation()).toEqual(false); @@ -290,122 +139,6 @@ describe("ui_utils", function () { }); }); - describe("waitOnEventOrTimeout", function () { - let eventBus; - - beforeAll(function () { - eventBus = new EventBus(); - }); - - afterAll(function () { - eventBus = null; - }); - - it("should reject invalid parameters", async function () { - const invalidTarget = waitOnEventOrTimeout({ - target: "window", - name: "DOMContentLoaded", - }).then( - function () { - // Shouldn't get here. - expect(false).toEqual(true); - }, - function (reason) { - expect(reason instanceof Error).toEqual(true); - } - ); - - const invalidName = waitOnEventOrTimeout({ - target: eventBus, - name: "", - }).then( - function () { - // Shouldn't get here. - expect(false).toEqual(true); - }, - function (reason) { - expect(reason instanceof Error).toEqual(true); - } - ); - - const invalidDelay = waitOnEventOrTimeout({ - target: eventBus, - name: "pagerendered", - delay: -1000, - }).then( - function () { - // Shouldn't get here. - expect(false).toEqual(true); - }, - function (reason) { - expect(reason instanceof Error).toEqual(true); - } - ); - - await Promise.all([invalidTarget, invalidName, invalidDelay]); - }); - - it("should resolve on event, using the DOM", async function () { - if (isNodeJS) { - pending("Document is not supported in Node.js."); - } - const button = document.createElement("button"); - - const buttonClicked = waitOnEventOrTimeout({ - target: button, - name: "click", - delay: 10000, - }); - // Immediately dispatch the expected event. - button.click(); - - const type = await buttonClicked; - expect(type).toEqual(WaitOnType.EVENT); - }); - - it("should resolve on timeout, using the DOM", async function () { - if (isNodeJS) { - pending("Document is not supported in Node.js."); - } - const button = document.createElement("button"); - - const buttonClicked = waitOnEventOrTimeout({ - target: button, - name: "click", - delay: 10, - }); - // Do *not* dispatch the event, and wait for the timeout. - - const type = await buttonClicked; - expect(type).toEqual(WaitOnType.TIMEOUT); - }); - - it("should resolve on event, using the EventBus", async function () { - const pageRendered = waitOnEventOrTimeout({ - target: eventBus, - name: "pagerendered", - delay: 10000, - }); - // Immediately dispatch the expected event. - eventBus.dispatch("pagerendered"); - - const type = await pageRendered; - expect(type).toEqual(WaitOnType.EVENT); - }); - - it("should resolve on timeout, using the EventBus", async function () { - const pageRendered = waitOnEventOrTimeout({ - target: eventBus, - name: "pagerendered", - delay: 10, - }); - // Do *not* dispatch the event, and wait for the timeout. - - const type = await pageRendered; - expect(type).toEqual(WaitOnType.TIMEOUT); - }); - }); - describe("getPageSizeInches", function () { it("gets page size (in inches)", function () { const page = { diff --git a/web/app.js b/web/app.js index 451151b86..0bd425669 100644 --- a/web/app.js +++ b/web/app.js @@ -18,10 +18,8 @@ import { animationStarted, apiPageLayoutToViewerModes, apiPageModeToSidebarView, - AutomationEventBus, AutoPrintRegExp, DEFAULT_SCALE_VALUE, - EventBus, getActiveOrFocusedElement, isValidRotation, isValidScrollMode, @@ -37,6 +35,7 @@ import { TextLayerMode, } from "./ui_utils.js"; import { AppOptions, compatibilityParams, OptionKind } from "./app_options.js"; +import { AutomationEventBus, EventBus } from "./event_utils.js"; import { build, createPromiseCapability, diff --git a/web/event_utils.js b/web/event_utils.js new file mode 100644 index 000000000..077699598 --- /dev/null +++ b/web/event_utils.js @@ -0,0 +1,196 @@ +/* Copyright 2012 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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); + }); +} + +/** + * 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. + */ +class EventBus { + constructor() { + this._listeners = Object.create(null); + } + + /** + * @param {string} eventName + * @param {function} listener + * @param {Object} [options] + */ + on(eventName, listener, options = null) { + this._on(eventName, listener, { + external: true, + once: options?.once, + }); + } + + /** + * @param {string} eventName + * @param {function} listener + * @param {Object} [options] + */ + off(eventName, listener, options = null) { + this._off(eventName, listener, { + external: true, + once: options?.once, + }); + } + + /** + * @param {string} eventName + * @param {Object} data + */ + dispatch(eventName, data) { + const eventListeners = this._listeners[eventName]; + if (!eventListeners || eventListeners.length === 0) { + return; + } + let externalListeners; + // Making copy of the listeners array in case if it will be modified + // during dispatch. + for (const { listener, external, once } of eventListeners.slice(0)) { + if (once) { + this._off(eventName, listener); + } + if (external) { + (externalListeners ||= []).push(listener); + continue; + } + listener(data); + } + // Dispatch any "external" listeners *after* the internal ones, to give the + // viewer components time to handle events and update their state first. + if (externalListeners) { + for (const listener of externalListeners) { + listener(data); + } + externalListeners = null; + } + } + + /** + * @ignore + */ + _on(eventName, listener, options = null) { + const eventListeners = (this._listeners[eventName] ||= []); + eventListeners.push({ + listener, + external: options?.external === true, + once: 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`. + */ +class AutomationEventBus extends EventBus { + dispatch(eventName, data) { + if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("MOZCENTRAL")) { + throw new Error("Not implemented: AutomationEventBus.dispatch"); + } + super.dispatch(eventName, data); + + const details = Object.create(null); + if (data) { + for (const key in data) { + const value = data[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); + } +} + +export { AutomationEventBus, EventBus, waitOnEventOrTimeout, WaitOnType }; diff --git a/web/pdf_history.js b/web/pdf_history.js index 4a49ee5a7..7fdc1e459 100644 --- a/web/pdf_history.js +++ b/web/pdf_history.js @@ -17,8 +17,8 @@ import { isValidRotation, parseQueryString, PresentationModeState, - waitOnEventOrTimeout, } from "./ui_utils.js"; +import { waitOnEventOrTimeout } from "./event_utils.js"; // Heuristic value used when force-resetting `this._blockHashChange`. const HASH_CHANGE_TIMEOUT = 1000; // milliseconds diff --git a/web/pdf_viewer.component.js b/web/pdf_viewer.component.js index a424c29e7..63be97d16 100644 --- a/web/pdf_viewer.component.js +++ b/web/pdf_viewer.component.js @@ -29,16 +29,17 @@ import { DefaultXfaLayerFactory, XfaLayerBuilder, } from "./xfa_layer_builder.js"; -import { EventBus, ProgressBar } from "./ui_utils.js"; import { PDFLinkService, SimpleLinkService } from "./pdf_link_service.js"; import { PDFSinglePageViewer, PDFViewer } from "./pdf_viewer.js"; import { DownloadManager } from "./download_manager.js"; +import { EventBus } from "./event_utils.js"; import { GenericL10n } from "./genericl10n.js"; import { NullL10n } from "./l10n_utils.js"; import { PDFFindController } from "./pdf_find_controller.js"; import { PDFHistory } from "./pdf_history.js"; import { PDFPageView } from "./pdf_page_view.js"; import { PDFScriptingManager } from "./pdf_scripting_manager.js"; +import { ProgressBar } from "./ui_utils.js"; // eslint-disable-next-line no-unused-vars const pdfjsVersion = PDFJSDev.eval("BUNDLE_VERSION"); diff --git a/web/ui_utils.js b/web/ui_utils.js index 4c9a057cc..c662f84cf 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -632,63 +632,6 @@ function isPortraitOrientation(size) { return size.width <= size.height; } -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); - }); -} - /** * Promise that is resolved when DOM window becomes visible. */ @@ -706,129 +649,6 @@ const animationStarted = new Promise(function (resolve) { window.requestAnimationFrame(resolve); }); -/** - * 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. - */ -class EventBus { - constructor() { - this._listeners = Object.create(null); - } - - /** - * @param {string} eventName - * @param {function} listener - * @param {Object} [options] - */ - on(eventName, listener, options = null) { - this._on(eventName, listener, { - external: true, - once: options?.once, - }); - } - - /** - * @param {string} eventName - * @param {function} listener - * @param {Object} [options] - */ - off(eventName, listener, options = null) { - this._off(eventName, listener, { - external: true, - once: options?.once, - }); - } - - /** - * @param {string} eventName - * @param {Object} data - */ - dispatch(eventName, data) { - const eventListeners = this._listeners[eventName]; - if (!eventListeners || eventListeners.length === 0) { - return; - } - let externalListeners; - // Making copy of the listeners array in case if it will be modified - // during dispatch. - for (const { listener, external, once } of eventListeners.slice(0)) { - if (once) { - this._off(eventName, listener); - } - if (external) { - (externalListeners ||= []).push(listener); - continue; - } - listener(data); - } - // Dispatch any "external" listeners *after* the internal ones, to give the - // viewer components time to handle events and update their state first. - if (externalListeners) { - for (const listener of externalListeners) { - listener(data); - } - externalListeners = null; - } - } - - /** - * @ignore - */ - _on(eventName, listener, options = null) { - const eventListeners = (this._listeners[eventName] ||= []); - eventListeners.push({ - listener, - external: options?.external === true, - once: 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`. - */ -class AutomationEventBus extends EventBus { - dispatch(eventName, data) { - if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("MOZCENTRAL")) { - throw new Error("Not implemented: AutomationEventBus.dispatch"); - } - super.dispatch(eventName, data); - - const details = Object.create(null); - if (data) { - for (const key in data) { - const value = data[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); - } -} - function clamp(v, min, max) { return Math.min(Math.max(v, min), max); } @@ -988,14 +808,12 @@ export { apiPageLayoutToViewerModes, apiPageModeToSidebarView, approximateFraction, - AutomationEventBus, AutoPrintRegExp, backtrackBeforeAllVisibleElements, // only exported for testing binarySearchFirstItem, DEFAULT_SCALE, DEFAULT_SCALE_DELTA, DEFAULT_SCALE_VALUE, - EventBus, getActiveOrFocusedElement, getOutputScale, getPageSizeInches, @@ -1023,7 +841,5 @@ export { TextLayerMode, UNKNOWN_SCALE, VERTICAL_PADDING, - waitOnEventOrTimeout, - WaitOnType, watchScroll, };