Bug 1883706: part 3) Implement `createHTML`, `createScript` and `createScriptURL...
[gecko.git] / browser / actors / ScreenshotsComponentChild.sys.mjs
blob06d720480394b95e50f01afa497b3d1c4308bfac
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 /* eslint-env mozilla/browser-window */
6 const lazy = {};
8 ChromeUtils.defineESModuleGetters(lazy, {
9   DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
10   ScreenshotsOverlay: "resource:///modules/ScreenshotsOverlayChild.sys.mjs",
11 });
13 export class ScreenshotsComponentChild extends JSWindowActorChild {
14   #resizeTask;
15   #scrollTask;
16   #overlay;
18   static OVERLAY_EVENTS = [
19     "click",
20     "pointerdown",
21     "pointermove",
22     "pointerup",
23     "keyup",
24     "keydown",
25   ];
27   get overlay() {
28     return this.#overlay;
29   }
31   receiveMessage(message) {
32     switch (message.name) {
33       case "Screenshots:ShowOverlay":
34         return this.startScreenshotsOverlay();
35       case "Screenshots:HideOverlay":
36         return this.endScreenshotsOverlay(message.data);
37       case "Screenshots:isOverlayShowing":
38         return this.overlay?.initialized;
39       case "Screenshots:getFullPageBounds":
40         return this.getFullPageBounds();
41       case "Screenshots:getVisibleBounds":
42         return this.getVisibleBounds();
43       case "Screenshots:getDocumentTitle":
44         return this.getDocumentTitle();
45       case "Screenshots:GetMethodsUsed":
46         return this.getMethodsUsed();
47       case "Screenshots:RemoveEventListeners":
48         return this.removeEventListeners();
49       case "Screenshots:AddEventListeners":
50         return this.addEventListeners();
51     }
52     return null;
53   }
55   handleEvent(event) {
56     if (!event.isTrusted) {
57       return;
58     }
60     switch (event.type) {
61       case "click":
62       case "pointerdown":
63       case "pointermove":
64       case "pointerup":
65       case "keyup":
66       case "keydown":
67         if (!this.overlay?.initialized) {
68           return;
69         }
70         this.overlay.handleEvent(event);
71         break;
72       case "beforeunload":
73         this.requestCancelScreenshot("navigation");
74         break;
75       case "resize":
76         if (!this.#resizeTask && this.overlay?.initialized) {
77           this.#resizeTask = new lazy.DeferredTask(() => {
78             this.overlay.updateScreenshotsOverlayDimensions("resize");
79           }, 16);
80         }
81         this.#resizeTask.arm();
82         break;
83       case "scroll":
84         if (!this.#scrollTask && this.overlay?.initialized) {
85           this.#scrollTask = new lazy.DeferredTask(() => {
86             this.overlay.updateScreenshotsOverlayDimensions("scroll");
87           }, 16);
88         }
89         this.#scrollTask.arm();
90         break;
91       case "Screenshots:Close":
92         this.requestCancelScreenshot(event.detail.reason);
93         break;
94       case "Screenshots:Copy":
95         this.requestCopyScreenshot(event.detail.region);
96         break;
97       case "Screenshots:Download":
98         this.requestDownloadScreenshot(event.detail.region);
99         break;
100       case "Screenshots:OverlaySelection": {
101         let { hasSelection } = event.detail;
102         this.sendOverlaySelection({ hasSelection });
103         break;
104       }
105       case "Screenshots:RecordEvent": {
106         let { eventName, reason, args } = event.detail;
107         this.recordTelemetryEvent(eventName, reason, args);
108         break;
109       }
110       case "Screenshots:ShowPanel":
111         this.showPanel();
112         break;
113       case "Screenshots:HidePanel":
114         this.hidePanel();
115         break;
116     }
117   }
119   /**
120    * Send a request to cancel the screenshot to the parent process
121    */
122   requestCancelScreenshot(reason) {
123     this.sendAsyncMessage("Screenshots:CancelScreenshot", {
124       closeOverlay: false,
125       reason,
126     });
127     this.endScreenshotsOverlay();
128   }
130   /**
131    * Send a request to copy the screenshots
132    * @param {Object} region The region dimensions of the screenshot to be copied
133    */
134   requestCopyScreenshot(region) {
135     region.devicePixelRatio = this.contentWindow.devicePixelRatio;
136     this.sendAsyncMessage("Screenshots:CopyScreenshot", { region });
137     this.endScreenshotsOverlay({ doNotResetMethods: true });
138   }
140   /**
141    * Send a request to download the screenshots
142    * @param {Object} region The region dimensions of the screenshot to be downloaded
143    */
144   requestDownloadScreenshot(region) {
145     region.devicePixelRatio = this.contentWindow.devicePixelRatio;
146     this.sendAsyncMessage("Screenshots:DownloadScreenshot", {
147       title: this.getDocumentTitle(),
148       region,
149     });
150     this.endScreenshotsOverlay({ doNotResetMethods: true });
151   }
153   showPanel() {
154     this.sendAsyncMessage("Screenshots:ShowPanel");
155   }
157   hidePanel() {
158     this.sendAsyncMessage("Screenshots:HidePanel");
159   }
161   getDocumentTitle() {
162     return this.document.title;
163   }
165   sendOverlaySelection(data) {
166     this.sendAsyncMessage("Screenshots:OverlaySelection", data);
167   }
169   getMethodsUsed() {
170     let methodsUsed = this.#overlay.methodsUsed;
171     this.#overlay.resetMethodsUsed();
172     return methodsUsed;
173   }
175   /**
176    * Resolves when the document is ready to have an overlay injected into it.
177    *
178    * @returns {Promise}
179    * @resolves {Boolean} true when document is ready or rejects
180    */
181   documentIsReady() {
182     const document = this.document;
183     // Some pages take ages to finish loading - if at all.
184     // We want to respond to enable the screenshots UI as soon that is possible
185     function readyEnough() {
186       return (
187         document.readyState !== "uninitialized" && document.documentElement
188       );
189     }
191     if (readyEnough()) {
192       return Promise.resolve();
193     }
194     return new Promise((resolve, reject) => {
195       function onChange(event) {
196         if (event.type === "pagehide") {
197           document.removeEventListener("readystatechange", onChange);
198           this.contentWindow.removeEventListener("pagehide", onChange);
199           reject(new Error("document unloaded before it was ready"));
200         } else if (readyEnough()) {
201           document.removeEventListener("readystatechange", onChange);
202           this.contentWindow.removeEventListener("pagehide", onChange);
203           resolve();
204         }
205       }
206       document.addEventListener("readystatechange", onChange);
207       this.contentWindow.addEventListener("pagehide", onChange, { once: true });
208     });
209   }
211   addEventListeners() {
212     this.contentWindow.addEventListener("beforeunload", this);
213     this.contentWindow.addEventListener("resize", this);
214     this.contentWindow.addEventListener("scroll", this);
215     this.addOverlayEventListeners();
216   }
218   addOverlayEventListeners() {
219     let chromeEventHandler = this.docShell.chromeEventHandler;
220     for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
221       chromeEventHandler.addEventListener(event, this, true);
222     }
223   }
225   /**
226    * Wait until the document is ready and then show the screenshots overlay
227    *
228    * @returns {Boolean} true when document is ready and the overlay is shown
229    * otherwise false
230    */
231   async startScreenshotsOverlay() {
232     try {
233       await this.documentIsReady();
234     } catch (ex) {
235       console.warn(`ScreenshotsComponentChild: ${ex.message}`);
236       return false;
237     }
238     await this.documentIsReady();
239     let overlay =
240       this.overlay ||
241       (this.#overlay = new lazy.ScreenshotsOverlay(this.document));
242     this.addEventListeners();
244     overlay.initialize();
245     return true;
246   }
248   removeEventListeners() {
249     this.contentWindow.removeEventListener("beforeunload", this);
250     this.contentWindow.removeEventListener("resize", this);
251     this.contentWindow.removeEventListener("scroll", this);
252     this.removeOverlayEventListeners();
253   }
255   removeOverlayEventListeners() {
256     let chromeEventHandler = this.docShell.chromeEventHandler;
257     for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
258       chromeEventHandler.removeEventListener(event, this, true);
259     }
260   }
262   /**
263    * Removes event listeners and the screenshots overlay.
264    */
265   endScreenshotsOverlay(options = {}) {
266     this.removeEventListeners();
268     this.overlay?.tearDown(options);
269     this.#resizeTask?.disarm();
270     this.#scrollTask?.disarm();
271   }
273   didDestroy() {
274     this.#resizeTask?.disarm();
275     this.#scrollTask?.disarm();
276   }
278   /**
279    * Gets the full page bounds for a full page screenshot.
280    *
281    * @returns { object }
282    *   The device pixel ratio and a DOMRect of the scrollable content bounds.
283    *
284    *   devicePixelRatio (float):
285    *      The device pixel ratio of the screen
286    *
287    *   rect (object):
288    *      top (int):
289    *        The scroll top position for the content window.
290    *
291    *      left (int):
292    *        The scroll left position for the content window.
293    *
294    *      width (int):
295    *        The scroll width of the content window.
296    *
297    *      height (int):
298    *        The scroll height of the content window.
299    */
300   getFullPageBounds() {
301     let {
302       scrollMinX,
303       scrollMinY,
304       scrollWidth,
305       scrollHeight,
306       devicePixelRatio,
307     } = this.#overlay.windowDimensions.dimensions;
308     let rect = {
309       left: scrollMinX,
310       top: scrollMinY,
311       right: scrollWidth,
312       bottom: scrollHeight,
313       width: scrollWidth,
314       height: scrollHeight,
315       devicePixelRatio,
316     };
317     return rect;
318   }
320   /**
321    * Gets the visible page bounds for a visible screenshot.
322    *
323    * @returns { object }
324    *   The device pixel ratio and a DOMRect of the current visible
325    *   content bounds.
326    *
327    *   devicePixelRatio (float):
328    *      The device pixel ratio of the screen
329    *
330    *   rect (object):
331    *      top (int):
332    *        The top position for the content window.
333    *
334    *      left (int):
335    *        The left position for the content window.
336    *
337    *      width (int):
338    *        The width of the content window.
339    *
340    *      height (int):
341    *        The height of the content window.
342    */
343   getVisibleBounds() {
344     let { scrollX, scrollY, clientWidth, clientHeight, devicePixelRatio } =
345       this.#overlay.windowDimensions.dimensions;
346     let rect = {
347       left: scrollX,
348       top: scrollY,
349       right: scrollX + clientWidth,
350       bottom: scrollY + clientHeight,
351       width: clientWidth,
352       height: clientHeight,
353       devicePixelRatio,
354     };
355     return rect;
356   }
358   recordTelemetryEvent(type, object, args = {}) {
359     Services.telemetry.recordEvent("screenshots", type, object, null, args);
360   }