diff options
Diffstat (limited to 'browser/actors/ScreenshotsComponentChild.sys.mjs')
-rw-r--r-- | browser/actors/ScreenshotsComponentChild.sys.mjs | 353 |
1 files changed, 353 insertions, 0 deletions
diff --git a/browser/actors/ScreenshotsComponentChild.sys.mjs b/browser/actors/ScreenshotsComponentChild.sys.mjs new file mode 100644 index 0000000000..0a4d6d2539 --- /dev/null +++ b/browser/actors/ScreenshotsComponentChild.sys.mjs @@ -0,0 +1,353 @@ +/* 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/. */ +/* eslint-env mozilla/browser-window */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", + ScreenshotsOverlay: "resource:///modules/ScreenshotsOverlayChild.sys.mjs", +}); + +export class ScreenshotsComponentChild extends JSWindowActorChild { + #resizeTask; + #scrollTask; + #overlay; + + static OVERLAY_EVENTS = [ + "click", + "pointerdown", + "pointermove", + "pointerup", + "keyup", + "keydown", + ]; + + get overlay() { + return this.#overlay; + } + + receiveMessage(message) { + switch (message.name) { + case "Screenshots:ShowOverlay": + return this.startScreenshotsOverlay(); + case "Screenshots:HideOverlay": + return this.endScreenshotsOverlay(message.data); + case "Screenshots:isOverlayShowing": + return this.overlay?.initialized; + case "Screenshots:getFullPageBounds": + return this.getFullPageBounds(); + case "Screenshots:getVisibleBounds": + return this.getVisibleBounds(); + case "Screenshots:getDocumentTitle": + return this.getDocumentTitle(); + case "Screenshots:GetMethodsUsed": + return this.getMethodsUsed(); + } + return null; + } + + handleEvent(event) { + switch (event.type) { + case "click": + case "pointerdown": + case "pointermove": + case "pointerup": + case "keyup": + case "keydown": + if (!this.overlay?.initialized) { + return; + } + this.overlay.handleEvent(event); + break; + case "beforeunload": + this.requestCancelScreenshot("navigation"); + break; + case "resize": + if (!this.#resizeTask && this.overlay?.initialized) { + this.#resizeTask = new lazy.DeferredTask(() => { + this.overlay.updateScreenshotsOverlayDimensions("resize"); + }, 16); + } + this.#resizeTask.arm(); + break; + case "scroll": + if (!this.#scrollTask && this.overlay?.initialized) { + this.#scrollTask = new lazy.DeferredTask(() => { + this.overlay.updateScreenshotsOverlayDimensions("scroll"); + }, 16); + } + this.#scrollTask.arm(); + break; + case "visibilitychange": + if ( + event.target.visibilityState === "hidden" && + this.overlay?.state === "crosshairs" + ) { + this.requestCancelScreenshot("navigation"); + } + break; + case "Screenshots:Close": + this.requestCancelScreenshot(event.detail.reason); + break; + case "Screenshots:Copy": + this.requestCopyScreenshot(event.detail.region); + break; + case "Screenshots:Download": + this.requestDownloadScreenshot(event.detail.region); + break; + case "Screenshots:OverlaySelection": + let { hasSelection } = event.detail; + this.sendOverlaySelection({ hasSelection }); + break; + case "Screenshots:RecordEvent": + let { eventName, reason, args } = event.detail; + this.recordTelemetryEvent(eventName, reason, args); + break; + case "Screenshots:ShowPanel": + this.showPanel(); + break; + case "Screenshots:HidePanel": + this.hidePanel(); + break; + } + } + + /** + * Send a request to cancel the screenshot to the parent process + */ + requestCancelScreenshot(reason) { + this.sendAsyncMessage("Screenshots:CancelScreenshot", { + closeOverlay: false, + reason, + }); + this.endScreenshotsOverlay(); + } + + /** + * Send a request to copy the screenshots + * @param {Object} region The region dimensions of the screenshot to be copied + */ + requestCopyScreenshot(region) { + region.devicePixelRatio = this.contentWindow.devicePixelRatio; + this.sendAsyncMessage("Screenshots:CopyScreenshot", { region }); + this.endScreenshotsOverlay({ doNotResetMethods: true }); + } + + /** + * Send a request to download the screenshots + * @param {Object} region The region dimensions of the screenshot to be downloaded + */ + requestDownloadScreenshot(region) { + region.devicePixelRatio = this.contentWindow.devicePixelRatio; + this.sendAsyncMessage("Screenshots:DownloadScreenshot", { + title: this.getDocumentTitle(), + region, + }); + this.endScreenshotsOverlay({ doNotResetMethods: true }); + } + + showPanel() { + this.sendAsyncMessage("Screenshots:ShowPanel"); + } + + hidePanel() { + this.sendAsyncMessage("Screenshots:HidePanel"); + } + + getDocumentTitle() { + return this.document.title; + } + + sendOverlaySelection(data) { + this.sendAsyncMessage("Screenshots:OverlaySelection", data); + } + + getMethodsUsed() { + let methodsUsed = this.#overlay.methodsUsed; + this.#overlay.resetMethodsUsed(); + return methodsUsed; + } + + /** + * Resolves when the document is ready to have an overlay injected into it. + * + * @returns {Promise} + * @resolves {Boolean} true when document is ready or rejects + */ + documentIsReady() { + const document = this.document; + // Some pages take ages to finish loading - if at all. + // We want to respond to enable the screenshots UI as soon that is possible + function readyEnough() { + return ( + document.readyState !== "uninitialized" && document.documentElement + ); + } + + if (readyEnough()) { + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + function onChange(event) { + if (event.type === "pagehide") { + document.removeEventListener("readystatechange", onChange); + this.contentWindow.removeEventListener("pagehide", onChange); + reject(new Error("document unloaded before it was ready")); + } else if (readyEnough()) { + document.removeEventListener("readystatechange", onChange); + this.contentWindow.removeEventListener("pagehide", onChange); + resolve(); + } + } + document.addEventListener("readystatechange", onChange); + this.contentWindow.addEventListener("pagehide", onChange, { once: true }); + }); + } + + addOverlayEventListeners() { + let chromeEventHandler = this.docShell.chromeEventHandler; + for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) { + chromeEventHandler.addEventListener(event, this, true); + } + } + + /** + * Wait until the document is ready and then show the screenshots overlay + * + * @returns {Boolean} true when document is ready and the overlay is shown + * otherwise false + */ + async startScreenshotsOverlay() { + try { + await this.documentIsReady(); + } catch (ex) { + console.warn(`ScreenshotsComponentChild: ${ex.message}`); + return false; + } + await this.documentIsReady(); + let overlay = + this.overlay || + (this.#overlay = new lazy.ScreenshotsOverlay(this.document)); + this.document.ownerGlobal.addEventListener("beforeunload", this); + this.contentWindow.addEventListener("resize", this); + this.contentWindow.addEventListener("scroll", this); + this.contentWindow.addEventListener("visibilitychange", this); + this.addOverlayEventListeners(); + + overlay.initialize(); + return true; + } + + removeOverlayEventListeners() { + let chromeEventHandler = this.docShell.chromeEventHandler; + for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) { + chromeEventHandler.removeEventListener(event, this, true); + } + } + + /** + * Removes event listeners and the screenshots overlay. + */ + endScreenshotsOverlay(options = {}) { + this.document.ownerGlobal.removeEventListener("beforeunload", this); + this.contentWindow.removeEventListener("resize", this); + this.contentWindow.removeEventListener("scroll", this); + this.contentWindow.removeEventListener("visibilitychange", this); + this.removeOverlayEventListeners(); + + this.overlay?.tearDown(options); + this.#resizeTask?.disarm(); + this.#scrollTask?.disarm(); + } + + didDestroy() { + this.#resizeTask?.disarm(); + this.#scrollTask?.disarm(); + } + + /** + * Gets the full page bounds for a full page screenshot. + * + * @returns { object } + * The device pixel ratio and a DOMRect of the scrollable content bounds. + * + * devicePixelRatio (float): + * The device pixel ratio of the screen + * + * rect (object): + * top (int): + * The scroll top position for the content window. + * + * left (int): + * The scroll left position for the content window. + * + * width (int): + * The scroll width of the content window. + * + * height (int): + * The scroll height of the content window. + */ + getFullPageBounds() { + let { + scrollMinX, + scrollMinY, + scrollWidth, + scrollHeight, + devicePixelRatio, + } = this.#overlay.windowDimensions.dimensions; + let rect = { + left: scrollMinX, + top: scrollMinY, + right: scrollWidth, + bottom: scrollHeight, + width: scrollWidth, + height: scrollHeight, + devicePixelRatio, + }; + return rect; + } + + /** + * Gets the visible page bounds for a visible screenshot. + * + * @returns { object } + * The device pixel ratio and a DOMRect of the current visible + * content bounds. + * + * devicePixelRatio (float): + * The device pixel ratio of the screen + * + * rect (object): + * top (int): + * The top position for the content window. + * + * left (int): + * The left position for the content window. + * + * width (int): + * The width of the content window. + * + * height (int): + * The height of the content window. + */ + getVisibleBounds() { + let { scrollX, scrollY, clientWidth, clientHeight, devicePixelRatio } = + this.#overlay.windowDimensions.dimensions; + let rect = { + left: scrollX, + top: scrollY, + right: scrollX + clientWidth, + bottom: scrollY + clientHeight, + width: clientWidth, + height: clientHeight, + devicePixelRatio, + }; + return rect; + } + + recordTelemetryEvent(type, object, args = {}) { + Services.telemetry.recordEvent("screenshots", type, object, null, args); + } +} |