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 });
104 case "Screenshots:RecordEvent":
105 let { eventName, reason, args } = event.detail;
106 this.recordTelemetryEvent(eventName, reason, args);
108 case "Screenshots:ShowPanel":
111 case "Screenshots:HidePanel":
118 * Send a request to cancel the screenshot to the parent process
120 requestCancelScreenshot(reason) {
121 this.sendAsyncMessage("Screenshots:CancelScreenshot", {
125 this.endScreenshotsOverlay();
129 * Send a request to copy the screenshots
130 * @param {Object} region The region dimensions of the screenshot to be copied
132 requestCopyScreenshot(region) {
133 region.devicePixelRatio = this.contentWindow.devicePixelRatio;
134 this.sendAsyncMessage("Screenshots:CopyScreenshot", { region });
135 this.endScreenshotsOverlay({ doNotResetMethods: true });
139 * Send a request to download the screenshots
140 * @param {Object} region The region dimensions of the screenshot to be downloaded
142 requestDownloadScreenshot(region) {
143 region.devicePixelRatio = this.contentWindow.devicePixelRatio;
144 this.sendAsyncMessage("Screenshots:DownloadScreenshot", {
145 title: this.getDocumentTitle(),
148 this.endScreenshotsOverlay({ doNotResetMethods: true });
152 this.sendAsyncMessage("Screenshots:ShowPanel");
156 this.sendAsyncMessage("Screenshots:HidePanel");
160 return this.document.title;
163 sendOverlaySelection(data) {
164 this.sendAsyncMessage("Screenshots:OverlaySelection", data);
168 let methodsUsed = this.#overlay.methodsUsed;
169 this.#overlay.resetMethodsUsed();
174 * Resolves when the document is ready to have an overlay injected into it.
177 * @resolves {Boolean} true when document is ready or rejects
180 const document = this.document;
181 // Some pages take ages to finish loading - if at all.
182 // We want to respond to enable the screenshots UI as soon that is possible
183 function readyEnough() {
185 document.readyState !== "uninitialized" && document.documentElement
190 return Promise.resolve();
192 return new Promise((resolve, reject) => {
193 function onChange(event) {
194 if (event.type === "pagehide") {
195 document.removeEventListener("readystatechange", onChange);
196 this.contentWindow.removeEventListener("pagehide", onChange);
197 reject(new Error("document unloaded before it was ready"));
198 } else if (readyEnough()) {
199 document.removeEventListener("readystatechange", onChange);
200 this.contentWindow.removeEventListener("pagehide", onChange);
204 document.addEventListener("readystatechange", onChange);
205 this.contentWindow.addEventListener("pagehide", onChange, { once: true });
209 addEventListeners() {
210 this.contentWindow.addEventListener("beforeunload", this);
211 this.contentWindow.addEventListener("resize", this);
212 this.contentWindow.addEventListener("scroll", this);
213 this.addOverlayEventListeners();
216 addOverlayEventListeners() {
217 let chromeEventHandler = this.docShell.chromeEventHandler;
218 for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
219 chromeEventHandler.addEventListener(event, this, true);
224 * Wait until the document is ready and then show the screenshots overlay
226 * @returns {Boolean} true when document is ready and the overlay is shown
229 async startScreenshotsOverlay() {
231 await this.documentIsReady();
233 console.warn(`ScreenshotsComponentChild: ${ex.message}`);
236 await this.documentIsReady();
239 (this.#overlay = new lazy.ScreenshotsOverlay(this.document));
240 this.addEventListeners();
242 overlay.initialize();
246 removeEventListeners() {
247 this.contentWindow.removeEventListener("beforeunload", this);
248 this.contentWindow.removeEventListener("resize", this);
249 this.contentWindow.removeEventListener("scroll", this);
250 this.removeOverlayEventListeners();
253 removeOverlayEventListeners() {
254 let chromeEventHandler = this.docShell.chromeEventHandler;
255 for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
256 chromeEventHandler.removeEventListener(event, this, true);
261 * Removes event listeners and the screenshots overlay.
263 endScreenshotsOverlay(options = {}) {
264 this.removeEventListeners();
266 this.overlay?.tearDown(options);
267 this.#resizeTask?.disarm();
268 this.#scrollTask?.disarm();
272 this.#resizeTask?.disarm();
273 this.#scrollTask?.disarm();
277 * Gets the full page bounds for a full page screenshot.
279 * @returns { object }
280 * The device pixel ratio and a DOMRect of the scrollable content bounds.
282 * devicePixelRatio (float):
283 * The device pixel ratio of the screen
287 * The scroll top position for the content window.
290 * The scroll left position for the content window.
293 * The scroll width of the content window.
296 * The scroll height of the content window.
298 getFullPageBounds() {
305 } = this.#overlay.windowDimensions.dimensions;
310 bottom: scrollHeight,
312 height: scrollHeight,
319 * Gets the visible page bounds for a visible screenshot.
321 * @returns { object }
322 * The device pixel ratio and a DOMRect of the current visible
325 * devicePixelRatio (float):
326 * The device pixel ratio of the screen
330 * The top position for the content window.
333 * The left position for the content window.
336 * The width of the content window.
339 * The height of the content window.
342 let { scrollX, scrollY, clientWidth, clientHeight, devicePixelRatio } =
343 this.#overlay.windowDimensions.dimensions;
347 right: scrollX + clientWidth,
348 bottom: scrollY + clientHeight,
350 height: clientHeight,
356 recordTelemetryEvent(type, object, args = {}) {
357 Services.telemetry.recordEvent("screenshots", type, object, null, args);