summaryrefslogtreecommitdiffstats
path: root/remote/shared/listeners/LoadListener.sys.mjs
blob: cccfca7a900fda27c3a46e04ba2a5bab63aa7b2f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
});

/**
 * The LoadListener can be used to listen for load events.
 *
 * Example:
 * ```
 * const listener = new LoadListener();
 * listener.on("DOMContentLoaded", onDOMContentLoaded);
 * listener.startListening();
 *
 * const onDOMContentLoaded = (eventName, data = {}) => {
 *   const { target } = data;
 *   ...
 * };
 * ```
 *
 * @fires message
 *    The LoadListener emits "DOMContentLoaded" and "load" events,
 *    with the following object as payload:
 *      - {Document} target
 *            The target document.
 */
export class LoadListener {
  #abortController;
  #window;

  /**
   * Create a new LoadListener instance.
   */
  constructor(win) {
    lazy.EventEmitter.decorate(this);

    // Use an abort controller instead of removeEventListener because destroy
    // might be called close to the window global destruction.
    this.#abortController = null;

    this.#window = win;
  }

  destroy() {
    this.stopListening();
  }

  startListening() {
    if (this.#abortController) {
      return;
    }

    this.#abortController = new AbortController();

    // Events are attached to the windowRoot instead of the regular window to
    // avoid issues with document.open (Bug 1822772).
    this.#window.windowRoot.addEventListener(
      "DOMContentLoaded",
      this.#onDOMContentLoaded,
      {
        capture: true,
        mozSystemGroup: true,
        signal: this.#abortController.signal,
      }
    );

    this.#window.windowRoot.addEventListener("load", this.#onLoad, {
      capture: true,
      mozSystemGroup: true,
      signal: this.#abortController.signal,
    });
  }

  stopListening() {
    if (!this.#abortController) {
      return;
    }

    this.#abortController.abort();
    this.#abortController = null;
  }

  #onDOMContentLoaded = event => {
    // Check that this event was emitted for the relevant window, because events
    // from inner frames can bubble to the windowRoot.
    if (event.target.defaultView === this.#window) {
      this.emit("DOMContentLoaded", { target: event.target });
    }
  };

  #onLoad = event => {
    // Check that this event was emitted for the relevant window, because events
    // from inner frames can bubble to the windowRoot.
    if (event.target.defaultView === this.#window) {
      this.emit("load", { target: event.target });
    }
  };
}