event_utils.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. /* Copyright 2012 Mozilla Foundation
  2. *
  3. * Licensed under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License.
  5. * You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software
  10. * distributed under the License is distributed on an "AS IS" BASIS,
  11. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. * See the License for the specific language governing permissions and
  13. * limitations under the License.
  14. */
  15. const WaitOnType = {
  16. EVENT: "event",
  17. TIMEOUT: "timeout",
  18. };
  19. /**
  20. * @typedef {Object} WaitOnEventOrTimeoutParameters
  21. * @property {Object} target - The event target, can for example be:
  22. * `window`, `document`, a DOM element, or an {EventBus} instance.
  23. * @property {string} name - The name of the event.
  24. * @property {number} delay - The delay, in milliseconds, after which the
  25. * timeout occurs (if the event wasn't already dispatched).
  26. */
  27. /**
  28. * Allows waiting for an event or a timeout, whichever occurs first.
  29. * Can be used to ensure that an action always occurs, even when an event
  30. * arrives late or not at all.
  31. *
  32. * @param {WaitOnEventOrTimeoutParameters}
  33. * @returns {Promise} A promise that is resolved with a {WaitOnType} value.
  34. */
  35. function waitOnEventOrTimeout({ target, name, delay = 0 }) {
  36. return new Promise(function (resolve, reject) {
  37. if (
  38. typeof target !== "object" ||
  39. !(name && typeof name === "string") ||
  40. !(Number.isInteger(delay) && delay >= 0)
  41. ) {
  42. throw new Error("waitOnEventOrTimeout - invalid parameters.");
  43. }
  44. function handler(type) {
  45. if (target instanceof EventBus) {
  46. target._off(name, eventHandler);
  47. } else {
  48. target.removeEventListener(name, eventHandler);
  49. }
  50. if (timeout) {
  51. clearTimeout(timeout);
  52. }
  53. resolve(type);
  54. }
  55. const eventHandler = handler.bind(null, WaitOnType.EVENT);
  56. if (target instanceof EventBus) {
  57. target._on(name, eventHandler);
  58. } else {
  59. target.addEventListener(name, eventHandler);
  60. }
  61. const timeoutHandler = handler.bind(null, WaitOnType.TIMEOUT);
  62. const timeout = setTimeout(timeoutHandler, delay);
  63. });
  64. }
  65. /**
  66. * Simple event bus for an application. Listeners are attached using the `on`
  67. * and `off` methods. To raise an event, the `dispatch` method shall be used.
  68. */
  69. class EventBus {
  70. #listeners = Object.create(null);
  71. /**
  72. * @param {string} eventName
  73. * @param {function} listener
  74. * @param {Object} [options]
  75. */
  76. on(eventName, listener, options = null) {
  77. this._on(eventName, listener, {
  78. external: true,
  79. once: options?.once,
  80. });
  81. }
  82. /**
  83. * @param {string} eventName
  84. * @param {function} listener
  85. * @param {Object} [options]
  86. */
  87. off(eventName, listener, options = null) {
  88. this._off(eventName, listener, {
  89. external: true,
  90. once: options?.once,
  91. });
  92. }
  93. /**
  94. * @param {string} eventName
  95. * @param {Object} data
  96. */
  97. dispatch(eventName, data) {
  98. const eventListeners = this.#listeners[eventName];
  99. if (!eventListeners || eventListeners.length === 0) {
  100. return;
  101. }
  102. let externalListeners;
  103. // Making copy of the listeners array in case if it will be modified
  104. // during dispatch.
  105. for (const { listener, external, once } of eventListeners.slice(0)) {
  106. if (once) {
  107. this._off(eventName, listener);
  108. }
  109. if (external) {
  110. (externalListeners ||= []).push(listener);
  111. continue;
  112. }
  113. listener(data);
  114. }
  115. // Dispatch any "external" listeners *after* the internal ones, to give the
  116. // viewer components time to handle events and update their state first.
  117. if (externalListeners) {
  118. for (const listener of externalListeners) {
  119. listener(data);
  120. }
  121. externalListeners = null;
  122. }
  123. }
  124. /**
  125. * @ignore
  126. */
  127. _on(eventName, listener, options = null) {
  128. const eventListeners = (this.#listeners[eventName] ||= []);
  129. eventListeners.push({
  130. listener,
  131. external: options?.external === true,
  132. once: options?.once === true,
  133. });
  134. }
  135. /**
  136. * @ignore
  137. */
  138. _off(eventName, listener, options = null) {
  139. const eventListeners = this.#listeners[eventName];
  140. if (!eventListeners) {
  141. return;
  142. }
  143. for (let i = 0, ii = eventListeners.length; i < ii; i++) {
  144. if (eventListeners[i].listener === listener) {
  145. eventListeners.splice(i, 1);
  146. return;
  147. }
  148. }
  149. }
  150. }
  151. /**
  152. * NOTE: Only used to support various PDF viewer tests in `mozilla-central`.
  153. */
  154. class AutomationEventBus extends EventBus {
  155. dispatch(eventName, data) {
  156. if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("MOZCENTRAL")) {
  157. throw new Error("Not implemented: AutomationEventBus.dispatch");
  158. }
  159. super.dispatch(eventName, data);
  160. const details = Object.create(null);
  161. if (data) {
  162. for (const key in data) {
  163. const value = data[key];
  164. if (key === "source") {
  165. if (value === window || value === document) {
  166. return; // No need to re-dispatch (already) global events.
  167. }
  168. continue; // Ignore the `source` property.
  169. }
  170. details[key] = value;
  171. }
  172. }
  173. const event = document.createEvent("CustomEvent");
  174. event.initCustomEvent(eventName, true, true, details);
  175. document.dispatchEvent(event);
  176. }
  177. }
  178. export { AutomationEventBus, EventBus, waitOnEventOrTimeout, WaitOnType };