mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 02:09:05 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			431 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			431 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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",
 | 
						|
});
 | 
						|
 | 
						|
const SCREENSHOTS_PREVENT_CONTENT_EVENTS_PREF =
 | 
						|
  "screenshots.browser.component.preventContentEvents";
 | 
						|
 | 
						|
export class ScreenshotsComponentChild extends JSWindowActorChild {
 | 
						|
  #resizeTask;
 | 
						|
  #scrollTask;
 | 
						|
  #overlay;
 | 
						|
  #preventableEventsAdded = false;
 | 
						|
 | 
						|
  static OVERLAY_EVENTS = [
 | 
						|
    "click",
 | 
						|
    "pointerdown",
 | 
						|
    "pointermove",
 | 
						|
    "pointerup",
 | 
						|
    "keyup",
 | 
						|
    "keydown",
 | 
						|
  ];
 | 
						|
 | 
						|
  // The following events are only listened to so we can prevent them from
 | 
						|
  // reaching the content page. The events in OVERLAY_EVENTS are also prevented.
 | 
						|
  static PREVENTABLE_EVENTS = [
 | 
						|
    "mousemove",
 | 
						|
    "mousedown",
 | 
						|
    "mouseup",
 | 
						|
    "mouseenter",
 | 
						|
    "mouseover",
 | 
						|
    "mouseout",
 | 
						|
    "mouseleave",
 | 
						|
    "touchstart",
 | 
						|
    "touchmove",
 | 
						|
    "touchend",
 | 
						|
    "dblclick",
 | 
						|
    "auxclick",
 | 
						|
    "keypress",
 | 
						|
    "contextmenu",
 | 
						|
    "pointerenter",
 | 
						|
    "pointerover",
 | 
						|
    "pointerout",
 | 
						|
    "pointerleave",
 | 
						|
  ];
 | 
						|
 | 
						|
  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();
 | 
						|
      case "Screenshots:RemoveEventListeners":
 | 
						|
        return this.removeEventListeners();
 | 
						|
      case "Screenshots:AddEventListeners":
 | 
						|
        return this.addEventListeners();
 | 
						|
      case "Screenshots:MoveFocusToContent":
 | 
						|
        return this.focusOverlay();
 | 
						|
      case "Screenshots:ClearFocus":
 | 
						|
        Services.focus.clearFocus(this.contentWindow);
 | 
						|
        return null;
 | 
						|
    }
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  handleEvent(event) {
 | 
						|
    if (!event.isTrusted) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Handle overlay events here
 | 
						|
    if (
 | 
						|
      [
 | 
						|
        ...ScreenshotsComponentChild.OVERLAY_EVENTS,
 | 
						|
        ...ScreenshotsComponentChild.PREVENTABLE_EVENTS,
 | 
						|
        "selectionchange",
 | 
						|
      ].includes(event.type)
 | 
						|
    ) {
 | 
						|
      if (!this.overlay?.initialized) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      // Preventing a pointerdown event throws an error in debug builds.
 | 
						|
      // See https://searchfox.org/mozilla-central/rev/b41bb321fe4bd7d03926083698ac498ebec0accf/widget/WidgetEventImpl.cpp#566-572
 | 
						|
      // Don't prevent the default context menu.
 | 
						|
      if (!["contextmenu", "pointerdown"].includes(event.type)) {
 | 
						|
        event.preventDefault();
 | 
						|
      }
 | 
						|
 | 
						|
      event.stopImmediatePropagation();
 | 
						|
      this.overlay.handleEvent(event);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    switch (event.type) {
 | 
						|
      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 "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, overlayState } = event.detail;
 | 
						|
        this.sendOverlaySelection({ hasSelection, overlayState });
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      case "Screenshots:RecordEvent": {
 | 
						|
        let { eventName, reason, args } = event.detail;
 | 
						|
        this.recordTelemetryEvent(eventName, reason, args);
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      case "Screenshots:ShowPanel":
 | 
						|
        this.sendAsyncMessage("Screenshots:ShowPanel");
 | 
						|
        break;
 | 
						|
      case "Screenshots:HidePanel":
 | 
						|
        this.sendAsyncMessage("Screenshots:HidePanel");
 | 
						|
        break;
 | 
						|
      case "Screenshots:FocusPanel":
 | 
						|
        this.sendAsyncMessage("Screenshots:MoveFocusToParent", event.detail);
 | 
						|
        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 });
 | 
						|
  }
 | 
						|
 | 
						|
  getDocumentTitle() {
 | 
						|
    return this.document.title;
 | 
						|
  }
 | 
						|
 | 
						|
  sendOverlaySelection(data) {
 | 
						|
    this.sendAsyncMessage("Screenshots:OverlaySelection", data);
 | 
						|
  }
 | 
						|
 | 
						|
  getMethodsUsed() {
 | 
						|
    let methodsUsed = this.#overlay.methodsUsed;
 | 
						|
    this.#overlay.resetMethodsUsed();
 | 
						|
    return methodsUsed;
 | 
						|
  }
 | 
						|
 | 
						|
  focusOverlay() {
 | 
						|
    this.contentWindow.focus();
 | 
						|
    this.#overlay.focus();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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 });
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  addEventListeners() {
 | 
						|
    this.contentWindow.addEventListener("beforeunload", this);
 | 
						|
    this.contentWindow.addEventListener("resize", this);
 | 
						|
    this.contentWindow.addEventListener("scroll", this);
 | 
						|
    this.addOverlayEventListeners();
 | 
						|
  }
 | 
						|
 | 
						|
  addOverlayEventListeners() {
 | 
						|
    let chromeEventHandler = this.docShell.chromeEventHandler;
 | 
						|
    for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
 | 
						|
      chromeEventHandler.addEventListener(event, this, true);
 | 
						|
    }
 | 
						|
 | 
						|
    this.document.addEventListener("selectionchange", this);
 | 
						|
 | 
						|
    if (Services.prefs.getBoolPref(SCREENSHOTS_PREVENT_CONTENT_EVENTS_PREF)) {
 | 
						|
      for (let event of ScreenshotsComponentChild.PREVENTABLE_EVENTS) {
 | 
						|
        chromeEventHandler.addEventListener(event, this, true);
 | 
						|
      }
 | 
						|
 | 
						|
      this.#preventableEventsAdded = 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.addEventListeners();
 | 
						|
 | 
						|
    overlay.initialize();
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  removeEventListeners() {
 | 
						|
    this.contentWindow.removeEventListener("beforeunload", this);
 | 
						|
    this.contentWindow.removeEventListener("resize", this);
 | 
						|
    this.contentWindow.removeEventListener("scroll", this);
 | 
						|
    this.removeOverlayEventListeners();
 | 
						|
  }
 | 
						|
 | 
						|
  removeOverlayEventListeners() {
 | 
						|
    let chromeEventHandler = this.docShell.chromeEventHandler;
 | 
						|
    for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
 | 
						|
      chromeEventHandler.removeEventListener(event, this, true);
 | 
						|
    }
 | 
						|
 | 
						|
    this.document.removeEventListener("selectionchange", this);
 | 
						|
 | 
						|
    if (this.#preventableEventsAdded) {
 | 
						|
      for (let event of ScreenshotsComponentChild.PREVENTABLE_EVENTS) {
 | 
						|
        chromeEventHandler.removeEventListener(event, this, true);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    this.#preventableEventsAdded = false;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Removes event listeners and the screenshots overlay.
 | 
						|
   */
 | 
						|
  endScreenshotsOverlay(options = {}) {
 | 
						|
    this.removeEventListeners();
 | 
						|
 | 
						|
    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: scrollMinX + scrollWidth,
 | 
						|
      bottom: scrollMinY + 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 {
 | 
						|
      pageScrollX,
 | 
						|
      pageScrollY,
 | 
						|
      clientWidth,
 | 
						|
      clientHeight,
 | 
						|
      devicePixelRatio,
 | 
						|
    } = this.#overlay.windowDimensions.dimensions;
 | 
						|
    let rect = {
 | 
						|
      left: pageScrollX,
 | 
						|
      top: pageScrollY,
 | 
						|
      right: pageScrollX + clientWidth,
 | 
						|
      bottom: pageScrollY + clientHeight,
 | 
						|
      width: clientWidth,
 | 
						|
      height: clientHeight,
 | 
						|
      devicePixelRatio,
 | 
						|
    };
 | 
						|
    return rect;
 | 
						|
  }
 | 
						|
 | 
						|
  recordTelemetryEvent(type, object, args = {}) {
 | 
						|
    Services.telemetry.recordEvent("screenshots", type, object, null, args);
 | 
						|
  }
 | 
						|
}
 |