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 */
8 ChromeUtils.defineESModuleGetters(lazy, {
9 DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
10 ScreenshotsOverlay: "resource:///modules/ScreenshotsOverlayChild.sys.mjs",
13 export class ScreenshotsComponentChild extends JSWindowActorChild {
18 static OVERLAY_EVENTS = [
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();
56 if (!event.isTrusted) {
67 if (!this.overlay?.initialized) {
70 this.overlay.handleEvent(event);
73 this.requestCancelScreenshot("navigation");
76 if (!this.#resizeTask && this.overlay?.initialized) {
77 this.#resizeTask = new lazy.DeferredTask(() => {
78 this.overlay.updateScreenshotsOverlayDimensions("resize");
81 this.#resizeTask.arm();
84 if (!this.#scrollTask && this.overlay?.initialized) {
85 this.#scrollTask = new lazy.DeferredTask(() => {
86 this.overlay.updateScreenshotsOverlayDimensions("scroll");
89 this.#scrollTask.arm();
91 case "Screenshots:Close":
92 this.requestCancelScreenshot(event.detail.reason);
94 case "Screenshots:Copy":
95 this.requestCopyScreenshot(event.detail.region);
97 case "Screenshots:Download":
98 this.requestDownloadScreenshot(event.detail.region);
100 case "Screenshots:OverlaySelection": {
101 let { hasSelection } = event.detail;
102 this.sendOverlaySelection({ hasSelection });
105 case "Screenshots:RecordEvent": {
106 let { eventName, reason, args } = event.detail;
107 this.recordTelemetryEvent(eventName, reason, args);
110 case "Screenshots:ShowPanel":
113 case "Screenshots:HidePanel":
120 * Send a request to cancel the screenshot to the parent process
122 requestCancelScreenshot(reason) {
123 this.sendAsyncMessage("Screenshots:CancelScreenshot", {
127 this.endScreenshotsOverlay();
131 * Send a request to copy the screenshots
132 * @param {Object} region The region dimensions of the screenshot to be copied
134 requestCopyScreenshot(region) {
135 region.devicePixelRatio = this.contentWindow.devicePixelRatio;
136 this.sendAsyncMessage("Screenshots:CopyScreenshot", { region });
137 this.endScreenshotsOverlay({ doNotResetMethods: true });
141 * Send a request to download the screenshots
142 * @param {Object} region The region dimensions of the screenshot to be downloaded
144 requestDownloadScreenshot(region) {
145 region.devicePixelRatio = this.contentWindow.devicePixelRatio;
146 this.sendAsyncMessage("Screenshots:DownloadScreenshot", {
147 title: this.getDocumentTitle(),
150 this.endScreenshotsOverlay({ doNotResetMethods: true });
154 this.sendAsyncMessage("Screenshots:ShowPanel");
158 this.sendAsyncMessage("Screenshots:HidePanel");
162 return this.document.title;
165 sendOverlaySelection(data) {
166 this.sendAsyncMessage("Screenshots:OverlaySelection", data);
170 let methodsUsed = this.#overlay.methodsUsed;
171 this.#overlay.resetMethodsUsed();
176 * Resolves when the document is ready to have an overlay injected into it.
179 * @resolves {Boolean} true when document is ready or rejects
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() {
187 document.readyState !== "uninitialized" && document.documentElement
192 return Promise.resolve();
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);
206 document.addEventListener("readystatechange", onChange);
207 this.contentWindow.addEventListener("pagehide", onChange, { once: true });
211 addEventListeners() {
212 this.contentWindow.addEventListener("beforeunload", this);
213 this.contentWindow.addEventListener("resize", this);
214 this.contentWindow.addEventListener("scroll", this);
215 this.addOverlayEventListeners();
218 addOverlayEventListeners() {
219 let chromeEventHandler = this.docShell.chromeEventHandler;
220 for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
221 chromeEventHandler.addEventListener(event, this, true);
226 * Wait until the document is ready and then show the screenshots overlay
228 * @returns {Boolean} true when document is ready and the overlay is shown
231 async startScreenshotsOverlay() {
233 await this.documentIsReady();
235 console.warn(`ScreenshotsComponentChild: ${ex.message}`);
238 await this.documentIsReady();
241 (this.#overlay = new lazy.ScreenshotsOverlay(this.document));
242 this.addEventListeners();
244 overlay.initialize();
248 removeEventListeners() {
249 this.contentWindow.removeEventListener("beforeunload", this);
250 this.contentWindow.removeEventListener("resize", this);
251 this.contentWindow.removeEventListener("scroll", this);
252 this.removeOverlayEventListeners();
255 removeOverlayEventListeners() {
256 let chromeEventHandler = this.docShell.chromeEventHandler;
257 for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
258 chromeEventHandler.removeEventListener(event, this, true);
263 * Removes event listeners and the screenshots overlay.
265 endScreenshotsOverlay(options = {}) {
266 this.removeEventListeners();
268 this.overlay?.tearDown(options);
269 this.#resizeTask?.disarm();
270 this.#scrollTask?.disarm();
274 this.#resizeTask?.disarm();
275 this.#scrollTask?.disarm();
279 * Gets the full page bounds for a full page screenshot.
281 * @returns { object }
282 * The device pixel ratio and a DOMRect of the scrollable content bounds.
284 * devicePixelRatio (float):
285 * The device pixel ratio of the screen
289 * The scroll top position for the content window.
292 * The scroll left position for the content window.
295 * The scroll width of the content window.
298 * The scroll height of the content window.
300 getFullPageBounds() {
307 } = this.#overlay.windowDimensions.dimensions;
312 bottom: scrollHeight,
314 height: scrollHeight,
321 * Gets the visible page bounds for a visible screenshot.
323 * @returns { object }
324 * The device pixel ratio and a DOMRect of the current visible
327 * devicePixelRatio (float):
328 * The device pixel ratio of the screen
332 * The top position for the content window.
335 * The left position for the content window.
338 * The width of the content window.
341 * The height of the content window.
344 let { scrollX, scrollY, clientWidth, clientHeight, devicePixelRatio } =
345 this.#overlay.windowDimensions.dimensions;
349 right: scrollX + clientWidth,
350 bottom: scrollY + clientHeight,
352 height: clientHeight,
358 recordTelemetryEvent(type, object, args = {}) {
359 Services.telemetry.recordEvent("screenshots", type, object, null, args);