From 0b1f41c5b3885a0b916a3fa29ecf7f5fb835a55a Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Tue, 28 Aug 2018 10:26:18 +0200 Subject: [PATCH] Add general support for re-dispatching events, on `EventBus` instances, to the DOM This patch is the first step to be able to eventually get rid of the `attachDOMEventsToEventBus` function, by allowing `EventBus` instances to simply re-dispatch most[1] events to the DOM. Note that the re-dispatching is purposely implemented to occur *after* all registered `EventBus` listeners have been serviced, to prevent the ordering issues that necessitated the duplicated page/scale-change events. The DOM events are currently necessary for the `mozilla-central` tests, see https://hg.mozilla.org/mozilla-central/file/tip/browser/extensions/pdfjs/test, and perhaps also for custom deployments of the PDF.js default viewer. Once this have landed, and been successfully uplifted to `mozilla-central`, I intent to submit a patch to update the test-code to utilize the new preference. This will thus, eventually, make it possible to remove the `attachDOMEventsToEventBus` functionality. *Please note:* I've successfully ran all `mozilla-central` tests locally, with these patches applied. --- [1] The exception being events that originated on the `window` or `document`, since those are already globally available anyway. --- extensions/chromium/preferences_schema.json | 4 +++ test/unit/ui_utils_spec.js | 39 +++++++++++++++++++++ web/app.js | 3 +- web/app_options.js | 5 +++ web/default_preferences.json | 1 + web/dom_events.js | 11 +++--- web/ui_utils.js | 35 +++++++++++++++++- 7 files changed, 91 insertions(+), 7 deletions(-) diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json index 1833f61a6..696090120 100644 --- a/extensions/chromium/preferences_schema.json +++ b/extensions/chromium/preferences_schema.json @@ -47,6 +47,10 @@ "type": "boolean", "default": false }, + "eventBusDispatchToDOM": { + "type": "boolean", + "default": false + }, "pdfBugEnabled": { "title": "Enable debugging tools", "description": "Whether to enable debugging tools.", diff --git a/test/unit/ui_utils_spec.js b/test/unit/ui_utils_spec.js index 0e93bd8ac..4ba2a22e0 100644 --- a/test/unit/ui_utils_spec.js +++ b/test/unit/ui_utils_spec.js @@ -262,6 +262,45 @@ describe('ui_utils', function() { eventBus.dispatch('test'); expect(count).toEqual(2); }); + + it('should not, by default, re-dispatch to DOM', function(done) { + if (isNodeJS()) { + pending('Document in not supported in Node.js.'); + } + const eventBus = new EventBus(); + let count = 0; + eventBus.on('test', function() { + count++; + }); + document.addEventListener('test', function() { + count++; + }); + eventBus.dispatch('test'); + + Promise.resolve().then(() => { + expect(count).toEqual(1); + done(); + }); + }); + it('should re-dispatch to DOM', function(done) { + if (isNodeJS()) { + pending('Document in not supported in Node.js.'); + } + const eventBus = new EventBus({ dispatchToDOM: true, }); + let count = 0; + eventBus.on('test', function() { + count++; + }); + document.addEventListener('test', function() { + count++; + }); + eventBus.dispatch('test'); + + Promise.resolve().then(() => { + expect(count).toEqual(2); + done(); + }); + }); }); describe('isValidRotation', function() { diff --git a/web/app.js b/web/app.js index c5bd4ff3a..4304c4cb9 100644 --- a/web/app.js +++ b/web/app.js @@ -288,7 +288,8 @@ let PDFViewerApplication = { return new Promise((resolve, reject) => { this.overlayManager = new OverlayManager(); - let eventBus = appConfig.eventBus || getGlobalEventBus(); + const dispatchToDOM = AppOptions.get('eventBusDispatchToDOM'); + let eventBus = appConfig.eventBus || getGlobalEventBus(dispatchToDOM); this.eventBus = eventBus; let pdfRenderingQueue = new PDFRenderingQueue(); diff --git a/web/app_options.js b/web/app_options.js index 83b7d3d47..08fb28848 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -68,6 +68,11 @@ const defaultOptions = { value: false, kind: OptionKind.VIEWER, }, + eventBusDispatchToDOM: { + /** @type {boolean} */ + value: false, + kind: OptionKind.VIEWER, + }, externalLinkRel: { /** @type {string} */ value: 'noopener noreferrer nofollow', diff --git a/web/default_preferences.json b/web/default_preferences.json index 78a998495..3389a5cf8 100644 --- a/web/default_preferences.json +++ b/web/default_preferences.json @@ -4,6 +4,7 @@ "sidebarViewOnLoad": 0, "cursorToolOnLoad": 0, "enableWebGL": false, + "eventBusDispatchToDOM": false, "pdfBugEnabled": false, "disableRange": false, "disableStream": false, diff --git a/web/dom_events.js b/web/dom_events.js index 1d7250f00..51a6999d5 100644 --- a/web/dom_events.js +++ b/web/dom_events.js @@ -129,12 +129,13 @@ function attachDOMEventsToEventBus(eventBus) { } let globalEventBus = null; -function getGlobalEventBus() { - if (globalEventBus) { - return globalEventBus; +function getGlobalEventBus(dispatchToDOM = false) { + if (!globalEventBus) { + globalEventBus = new EventBus({ dispatchToDOM, }); + if (!dispatchToDOM) { + attachDOMEventsToEventBus(globalEventBus); + } } - globalEventBus = new EventBus(); - attachDOMEventsToEventBus(globalEventBus); return globalEventBus; } diff --git a/web/ui_utils.js b/web/ui_utils.js index 7c615f2f8..d87b78c15 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -683,8 +683,9 @@ let animationStarted = new Promise(function (resolve) { * used. */ class EventBus { - constructor() { + constructor({ dispatchToDOM = false, } = {}) { this._listeners = Object.create(null); + this._dispatchToDOM = dispatchToDOM === true; } on(eventName, listener) { @@ -708,6 +709,9 @@ class EventBus { dispatch(eventName) { let eventListeners = this._listeners[eventName]; if (!eventListeners || eventListeners.length === 0) { + if (this._dispatchToDOM) { + this._dispatchDOMEvent(eventName); + } return; } // Passing all arguments after the eventName to the listeners. @@ -717,6 +721,35 @@ class EventBus { eventListeners.slice(0).forEach(function (listener) { listener.apply(null, args); }); + if (this._dispatchToDOM) { + this._dispatchDOMEvent(eventName, args); + } + } + + /** + * @private + */ + _dispatchDOMEvent(eventName, args = null) { + if (!this._dispatchToDOM) { + return; + } + const details = Object.create(null); + if (args && args.length > 0) { + const obj = args[0]; + for (let 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); } }