tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ScreenshotsOverlayChild.sys.mjs (61771B)


      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 
      5 /**
      6 * The Screenshots overlay is inserted into the document's
      7 * anonymous content container (see dom/webidl/Document.webidl).
      8 *
      9 * This container gets cleared automatically when the document navigates.
     10 *
     11 * To retrieve the AnonymousContent instance, use the `content` getter.
     12 */
     13 
     14 /*
     15 * Below are the states of the screenshots overlay
     16 * States:
     17 *  "crosshairs":
     18 *    Nothing has happened, and the crosshairs will follow the movement of the mouse
     19 *  "draggingReady":
     20 *    The user has pressed the mouse button, but hasn't moved enough to create a selection
     21 *  "dragging":
     22 *    The user has pressed down a mouse button, and is dragging out an area far enough to show a selection
     23 *  "selected":
     24 *    The user has selected an area
     25 *  "resizing":
     26 *    The user is resizing the selection
     27 */
     28 
     29 import {
     30  setMaxDetectHeight,
     31  setMaxDetectWidth,
     32  getBestRectForElement,
     33  getElementFromPoint,
     34  Region,
     35  WindowDimensions,
     36 } from "chrome://browser/content/screenshots/overlayHelpers.mjs";
     37 
     38 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
     39 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     40 import { ShortcutUtils } from "resource://gre/modules/ShortcutUtils.sys.mjs";
     41 
     42 const STATES = {
     43  CROSSHAIRS: "crosshairs",
     44  DRAGGING_READY: "draggingReady",
     45  DRAGGING: "dragging",
     46  SELECTED: "selected",
     47  RESIZING: "resizing",
     48 };
     49 
     50 const lazy = {};
     51 
     52 ChromeUtils.defineLazyGetter(lazy, "overlayLocalization", () => {
     53  return new Localization(["browser/screenshots.ftl"], true);
     54 });
     55 
     56 const SCREENSHOTS_LAST_SAVED_METHOD_PREF =
     57  "screenshots.browser.component.last-saved-method";
     58 
     59 XPCOMUtils.defineLazyPreferenceGetter(
     60  lazy,
     61  "SCREENSHOTS_LAST_SAVED_METHOD",
     62  SCREENSHOTS_LAST_SAVED_METHOD_PREF,
     63  "download"
     64 );
     65 
     66 const REGION_CHANGE_THRESHOLD = 5;
     67 const SCROLL_BY_EDGE = 20;
     68 
     69 export class ScreenshotsOverlay {
     70  #content;
     71  #initialized = false;
     72  #state = "";
     73  #moverId;
     74  #cachedEle;
     75  #lastPageX;
     76  #lastPageY;
     77  #lastClientX;
     78  #lastClientY;
     79  #previousDimensions;
     80  #methodsUsed;
     81 
     82  get markup() {
     83    let accelString = ShortcutUtils.getModifierString("accel");
     84    let copyShorcut = accelString + this.copyKey;
     85    let downloadShortcut = accelString + this.downloadKey;
     86 
     87    let [
     88      cancelLabel,
     89      cancelAttributes,
     90      instructions,
     91      downloadAttributes,
     92      copyAttributes,
     93      previewFaceAriaLabel,
     94    ] = lazy.overlayLocalization.formatMessagesSync([
     95      { id: "screenshots-cancel-button" },
     96      { id: "screenshots-component-cancel-button" },
     97      { id: "screenshots-instructions" },
     98      {
     99        id: "screenshots-component-download-button-2",
    100        args: { shortcut: downloadShortcut },
    101      },
    102      {
    103        id: "screenshots-component-copy-button-2",
    104        args: { shortcut: copyShorcut },
    105      },
    106      { id: "screenshots-overlay-preview-face-label" },
    107    ]);
    108 
    109    return `
    110      <template>
    111        <link rel="stylesheet" href="chrome://browser/content/screenshots/overlay/overlay.css" />
    112        <div id="screenshots-component">
    113          <div id="preview-container" hidden>
    114            <div id="face-container" tabindex="0" role="button" aria-label="${previewFaceAriaLabel.attributes[0].value}">
    115              <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
    116                <g>
    117                  <path d="M11.4.9v2.9h-6c-.9 0-1.5.8-1.5 1.5v6H.8V3.8C.8 2.1 2.2.7 3.9.7h7.6v.2z" class="face-line-color"/>
    118                  <path d="M63.2 11.4h-3.1v-6c0-.8-.6-1.5-1.5-1.5h-6v-3h7.6c1.7 0 3.1 1.4 3.1 3.1z" class="face-line-color"/>
    119                  <path d="M52.6 63.2v-3.1h6c.9 0 1.5-.6 1.5-1.5v-6h3.1v7.6c0 1.7-1.4 3.1-3.1 3.1z" class="face-line-color"/>
    120                  <path d="M.8 52.7h3.1v6c0 .9.6 1.5 1.5 1.5h6v3.1H3.8c-1.7 0-3.1-1.4-3.1-3.1z" class="face-line-color"/>
    121                  <path d="M33.3 49.2H33c-4.6-.1-7.8-3.6-7.9-3.8-.6-.8-.6-2 .1-2.7.8-.8 1.9-.6 2.6.1 0 0 2.3 2.6 5.2 2.6 1.8 0 3.6-.9 5.2-2.6.8-.8 1.9-.8 2.7 0 .8.8.8 1.9 0 2.7-2.2 2.4-4.9 3.7-7.6 3.7z" class="face-line-color" style="display:inline"/>
    122                  <ellipse id="leftEye" cx="23" cy="26" class="face-line-color" rx="5" ry="7"/>
    123                  <ellipse id="rightEye" cx="43" cy="26" class="face-line-color" rx="5" ry="7"/>
    124                  <ellipse id="leftPupil" cx="25" cy="30" class="face-pupil-color" rx="3" ry="3"/>
    125                  <ellipse id="rightPupil" cx="45" cy="30" class="face-pupil-color" rx="3" ry="3"/>
    126                </g>
    127              </svg>
    128 
    129            </div>
    130            <div class="preview-instructions">${instructions.value}</div>
    131            <button class="screenshots-button ghost-button" id="screenshots-cancel-button" title="${cancelAttributes.attributes[0].value}" aria-label="${cancelAttributes.attributes[1].value}">${cancelLabel.value}</button>
    132          </div>
    133          <div id="hover-highlight" hidden></div>
    134          <div id="selection-container" hidden>
    135            <div id="top-background" class="bghighlight"></div>
    136            <div id="bottom-background" class="bghighlight"></div>
    137            <div id="left-background" class="bghighlight"></div>
    138            <div id="right-background" class="bghighlight"></div>
    139            <div id="highlight" class="highlight" tabindex="0">
    140              <div id="mover-topLeft" class="mover-target direction-topLeft" tabindex="0">
    141                <div class="mover"></div>
    142              </div>
    143              <div id="mover-top" class="mover-target direction-top">
    144                <div class="mover"></div>
    145              </div>
    146              <div id="mover-topRight" class="mover-target direction-topRight" tabindex="0">
    147                <div class="mover"></div>
    148              </div>
    149              <div id="mover-right" class="mover-target direction-right">
    150                <div class="mover"></div>
    151              </div>
    152              <div id="mover-bottomRight" class="mover-target direction-bottomRight" tabindex="0">
    153                <div class="mover"></div>
    154              </div>
    155              <div id="mover-bottom" class="mover-target direction-bottom">
    156                <div class="mover"></div>
    157              </div>
    158              <div id="mover-bottomLeft" class="mover-target direction-bottomLeft" tabindex="0">
    159                <div class="mover"></div>
    160              </div>
    161              <div id="mover-left" class="mover-target direction-left">
    162                <div class="mover"></div>
    163              </div>
    164              <div id="selection-size-container">
    165                <span id="selection-size" dir="ltr"></span>
    166              </div>
    167            </div>
    168          </div>
    169          <div id="buttons-container" hidden>
    170            <div class="buttons-wrapper">
    171              <button id="cancel" class="screenshots-button" title="${cancelAttributes.attributes[0].value}" aria-label="${cancelAttributes.attributes[1].value}"><img/></button>
    172              <button id="copy" class="screenshots-button" title="${copyAttributes.attributes[0].value}" aria-label="${copyAttributes.attributes[1].value}"><img/><label>${copyAttributes.value}</label></button>
    173              <button id="download" class="screenshots-button primary" title="${downloadAttributes.attributes[0].value}" aria-label="${downloadAttributes.attributes[1].value}"><img/><label>${downloadAttributes.value}</label></button>
    174            </div>
    175          </div>
    176        </div>
    177      </template>`;
    178  }
    179 
    180  get fragment() {
    181    if (!this.overlayTemplate) {
    182      let parser = new DOMParser();
    183      let doc = parser.parseFromString(this.markup, "text/html");
    184      this.overlayTemplate = this.document.importNode(
    185        doc.querySelector("template"),
    186        true
    187      );
    188    }
    189    let fragment = this.overlayTemplate.content.cloneNode(true);
    190    return fragment;
    191  }
    192 
    193  get initialized() {
    194    return this.#initialized;
    195  }
    196 
    197  get state() {
    198    return this.#state;
    199  }
    200 
    201  get methodsUsed() {
    202    return this.#methodsUsed;
    203  }
    204 
    205  constructor(contentDocument) {
    206    this.document = contentDocument;
    207    this.window = contentDocument.ownerGlobal;
    208 
    209    this.windowDimensions = new WindowDimensions();
    210    this.selectionRegion = new Region(this.windowDimensions);
    211    this.hoverElementRegion = new Region(this.windowDimensions);
    212    this.resetMethodsUsed();
    213 
    214    let [downloadKey, copyKey] = lazy.overlayLocalization.formatMessagesSync([
    215      { id: "screenshots-component-download-key" },
    216      { id: "screenshots-component-copy-key" },
    217    ]);
    218 
    219    this.downloadKey = downloadKey.value;
    220    this.copyKey = copyKey.value;
    221  }
    222 
    223  get content() {
    224    if (!this.#content || Cu.isDeadWrapper(this.#content)) {
    225      return null;
    226    }
    227    return this.#content;
    228  }
    229 
    230  getElementById(id) {
    231    return this.content.root.getElementById(id);
    232  }
    233 
    234  async initialize() {
    235    if (this.initialized) {
    236      return;
    237    }
    238 
    239    this.windowDimensions.reset();
    240 
    241    this.#content = this.document.insertAnonymousContent();
    242    this.#content.root.appendChild(this.fragment);
    243 
    244    this.initializeElements();
    245    this.screenshotsContainer.dir = Services.locale.isAppLocaleRTL
    246      ? "rtl"
    247      : "ltr";
    248    await this.updateWindowDimensions();
    249 
    250    this.#setState(STATES.CROSSHAIRS);
    251 
    252    this.selection = this.window.getSelection();
    253    this.ranges = [];
    254    for (let i = 0; i < this.selection.rangeCount; i++) {
    255      this.ranges.push(this.selection.getRangeAt(i));
    256    }
    257 
    258    this.#initialized = true;
    259  }
    260 
    261  /**
    262   * Get all the elements that will be used.
    263   */
    264  initializeElements() {
    265    this.previewCancelButton = this.getElementById("screenshots-cancel-button");
    266    this.cancelButton = this.getElementById("cancel");
    267    this.copyButton = this.getElementById("copy");
    268    this.downloadButton = this.getElementById("download");
    269 
    270    this.previewContainer = this.getElementById("preview-container");
    271    this.previewFace = this.getElementById("face-container");
    272    this.hoverElementContainer = this.getElementById("hover-highlight");
    273    this.selectionContainer = this.getElementById("selection-container");
    274    this.buttonsContainer = this.getElementById("buttons-container");
    275    this.screenshotsContainer = this.getElementById("screenshots-component");
    276 
    277    this.leftEye = this.getElementById("leftPupil");
    278    this.rightEye = this.getElementById("rightPupil");
    279 
    280    this.leftBackgroundEl = this.getElementById("left-background");
    281    this.topBackgroundEl = this.getElementById("top-background");
    282    this.rightBackgroundEl = this.getElementById("right-background");
    283    this.bottomBackgroundEl = this.getElementById("bottom-background");
    284    this.highlightEl = this.getElementById("highlight");
    285 
    286    this.topLeftMover = this.getElementById("mover-topLeft");
    287    this.topRightMover = this.getElementById("mover-topRight");
    288    this.bottomLeftMover = this.getElementById("mover-bottomLeft");
    289    this.bottomRightMover = this.getElementById("mover-bottomRight");
    290 
    291    this.selectionSize = this.getElementById("selection-size");
    292  }
    293 
    294  /**
    295   * Removes all event listeners and removes the overlay from the Anonymous Content
    296   */
    297  tearDown(options = {}) {
    298    if (this.#content) {
    299      if (!(options.doNotResetMethods === true)) {
    300        this.resetMethodsUsed();
    301      }
    302      try {
    303        this.document.removeAnonymousContent(this.#content);
    304      } catch (e) {
    305        // If the current window isn't the one the content was inserted into, this
    306        // will fail, but that's fine.
    307      }
    308    }
    309    this.#initialized = false;
    310    this.#setState("");
    311  }
    312 
    313  resetMethodsUsed() {
    314    this.#methodsUsed = {
    315      element: 0,
    316      region: 0,
    317      move: 0,
    318      resize: 0,
    319    };
    320  }
    321 
    322  focus(direction) {
    323    if (direction === "backward") {
    324      this.previewCancelButton.focus({ focusVisible: true });
    325    } else {
    326      this.previewFace.focus({ focusVisible: true });
    327    }
    328  }
    329 
    330  /**
    331   * Returns the x and y coordinates of the event relative to both the
    332   * viewport and the page.
    333   *
    334   * @param {Event} event The event
    335   * @returns
    336   *  {
    337   *    clientX: The x position relative to the viewport
    338   *    clientY: The y position relative to the viewport
    339   *    pageX: The x position relative to the entire page
    340   *    pageY: The y position relative to the entire page
    341   *  }
    342   */
    343  getCoordinatesFromEvent(event) {
    344    let { clientX, clientY, pageX, pageY } = event;
    345    pageX -= this.windowDimensions.scrollMinX;
    346    pageY -= this.windowDimensions.scrollMinY;
    347 
    348    return { clientX, clientY, pageX, pageY };
    349  }
    350 
    351  handleEvent(event) {
    352    switch (event.type) {
    353      case "click":
    354        this.handleClick(event);
    355        break;
    356      case "pointerdown":
    357        this.handlePointerDown(event);
    358        break;
    359      case "pointermove":
    360        this.handlePointerMove(event);
    361        break;
    362      case "pointerup":
    363        this.handlePointerUp(event);
    364        break;
    365      case "keydown":
    366        this.handleKeyDown(event);
    367        break;
    368      case "keyup":
    369        this.handleKeyUp(event);
    370        break;
    371      case "selectionchange":
    372        this.handleSelectionChange();
    373        break;
    374    }
    375  }
    376 
    377  /**
    378   * If the event came from the primary button, return false as we should not
    379   * early return in the event handler function.
    380   * If the event had another button, set to the crosshairs or selected state
    381   * and return true to early return from the event handler function.
    382   *
    383   * @param {PointerEvent} event
    384   * @returns true if the event button(s) was the non primary button
    385   *          false otherwise
    386   */
    387  preEventHandler(event) {
    388    if (event.button > 0 || event.buttons > 1) {
    389      switch (this.#state) {
    390        case STATES.DRAGGING_READY:
    391          this.#setState(STATES.CROSSHAIRS);
    392          break;
    393        case STATES.DRAGGING:
    394        case STATES.RESIZING:
    395          this.#setState(STATES.SELECTED);
    396          break;
    397      }
    398      return true;
    399    }
    400    return false;
    401  }
    402 
    403  handleClick(event) {
    404    if (this.preEventHandler(event)) {
    405      return;
    406    }
    407 
    408    switch (event.originalTarget.id) {
    409      case "screenshots-cancel-button":
    410      case "cancel":
    411        this.maybeCancelScreenshots();
    412        break;
    413      case "copy":
    414        this.copySelectedRegion();
    415        break;
    416      case "download":
    417        this.downloadSelectedRegion();
    418        break;
    419    }
    420  }
    421 
    422  maybeCancelScreenshots() {
    423    if (this.#state === STATES.CROSSHAIRS) {
    424      this.#dispatchEvent("Screenshots:Close", {
    425        reason: "OverlayCancel",
    426      });
    427    } else {
    428      this.#setState(STATES.CROSSHAIRS);
    429    }
    430  }
    431 
    432  /**
    433   * Handles the pointerdown event depending on the state.
    434   * Early return when a pointer down happens on a button.
    435   *
    436   * @param {Event} event The pointerown event
    437   */
    438  handlePointerDown(event) {
    439    // Early return if the event target is not within the screenshots component
    440    // element.
    441    if (!event.originalTarget.closest("#screenshots-component")) {
    442      return;
    443    }
    444 
    445    if (this.preEventHandler(event)) {
    446      return;
    447    }
    448 
    449    if (
    450      event.originalTarget.id === "screenshots-cancel-button" ||
    451      event.originalTarget.closest("#buttons-container") ===
    452        this.buttonsContainer
    453    ) {
    454      event.stopPropagation();
    455      return;
    456    }
    457 
    458    const { pageX, pageY } = this.getCoordinatesFromEvent(event);
    459 
    460    switch (this.#state) {
    461      case STATES.CROSSHAIRS: {
    462        this.crosshairsDragStart(pageX, pageY);
    463        break;
    464      }
    465      case STATES.SELECTED: {
    466        this.selectedDragStart(pageX, pageY, event.originalTarget.id);
    467        break;
    468      }
    469    }
    470  }
    471 
    472  /**
    473   * Handles the pointermove event depending on the state
    474   *
    475   * @param {Event} event The pointermove event
    476   */
    477  handlePointerMove(event) {
    478    if (this.preEventHandler(event)) {
    479      return;
    480    }
    481 
    482    const { pageX, pageY, clientX, clientY } =
    483      this.getCoordinatesFromEvent(event);
    484 
    485    switch (this.#state) {
    486      case STATES.CROSSHAIRS: {
    487        this.crosshairsMove(clientX, clientY);
    488        break;
    489      }
    490      case STATES.DRAGGING_READY: {
    491        this.draggingReadyDrag(pageX, pageY);
    492        break;
    493      }
    494      case STATES.DRAGGING: {
    495        this.draggingDrag(pageX, pageY);
    496        break;
    497      }
    498      case STATES.RESIZING: {
    499        this.resizingDrag(pageX, pageY);
    500        break;
    501      }
    502    }
    503  }
    504 
    505  /**
    506   * Handles the pointerup event depending on the state
    507   *
    508   * @param {Event} event The pointerup event
    509   */
    510  handlePointerUp(event) {
    511    const { pageX, pageY, clientX, clientY } =
    512      this.getCoordinatesFromEvent(event);
    513 
    514    switch (this.#state) {
    515      case STATES.DRAGGING_READY: {
    516        this.draggingReadyDragEnd(pageX - clientX, pageY - clientY);
    517        break;
    518      }
    519      case STATES.DRAGGING: {
    520        this.draggingDragEnd(pageX, pageY, event.originalTarget.id);
    521        break;
    522      }
    523      case STATES.RESIZING: {
    524        this.resizingDragEnd(pageX, pageY);
    525        break;
    526      }
    527    }
    528  }
    529 
    530  /**
    531   * Handles when a keydown occurs in the screenshots component.
    532   *
    533   * @param {Event} event The keydown event
    534   */
    535  handleKeyDown(event) {
    536    if (event.key === "Escape") {
    537      this.maybeCancelScreenshots();
    538      return;
    539    }
    540 
    541    switch (this.#state) {
    542      case STATES.CROSSHAIRS:
    543        this.crosshairsKeyDown(event);
    544        break;
    545      case STATES.DRAGGING:
    546        this.draggingKeyDown(event);
    547        break;
    548      case STATES.RESIZING:
    549        this.resizingKeyDown(event);
    550        break;
    551      case STATES.SELECTED:
    552        this.selectedKeyDown(event);
    553        break;
    554    }
    555  }
    556 
    557  /**
    558   * Handles when a keyup occurs in the screenshots component.
    559   * All we need to do on keyup is set the state to selected.
    560   *
    561   * @param {Event} event The keydown event
    562   */
    563  handleKeyUp(event) {
    564    switch (this.#state) {
    565      case STATES.RESIZING:
    566        switch (event.key) {
    567          case "ArrowLeft":
    568          case "ArrowUp":
    569          case "ArrowRight":
    570          case "ArrowDown":
    571            switch (event.originalTarget.id) {
    572              case "highlight":
    573              case "mover-bottomLeft":
    574              case "mover-bottomRight":
    575              case "mover-topLeft":
    576              case "mover-topRight":
    577                this.#setState(STATES.SELECTED, { doNotMoveFocus: true });
    578                break;
    579            }
    580            break;
    581        }
    582        break;
    583    }
    584  }
    585 
    586  /**
    587   * Gets the accel key depending on the platform.
    588   * metaKey for macOS. ctrlKey for Windows and Linux.
    589   *
    590   * @param {Event} event The keydown event
    591   * @returns {boolean} True if the accel key is pressed, false otherwise.
    592   */
    593  getAccelKey(event) {
    594    if (AppConstants.platform === "macosx") {
    595      return event.metaKey;
    596    }
    597    return event.ctrlKey;
    598  }
    599 
    600  crosshairsKeyDown(event) {
    601    switch (event.key) {
    602      case "ArrowLeft":
    603      case "ArrowUp":
    604      case "ArrowRight":
    605      case "ArrowDown":
    606        // Do nothing so we can prevent default below
    607        break;
    608      case "Tab":
    609        this.maybeLockFocus(event);
    610        return;
    611      case "Enter":
    612        if (this.handleKeyDownOnButton(event)) {
    613          return;
    614        }
    615 
    616        if (this.hoverElementRegion.isRegionValid) {
    617          this.draggingReadyStart();
    618          this.draggingReadyDragEnd();
    619          return;
    620        }
    621      // eslint-disable-next-line no-fallthrough
    622      case " ": {
    623        if (this.handleKeyDownOnButton(event)) {
    624          return;
    625        }
    626 
    627        if (Services.appinfo.isWayland) {
    628          return;
    629        }
    630 
    631        // If the preview face is focused, create a region from the preview
    632        // face and move focus to the bottom right mover for adjustments
    633        if (Services.focus.focusedElement === this.previewFace) {
    634          let rect = this.previewFace.getBoundingClientRect();
    635          this.hoverElementRegion.dimensions = rect;
    636          this.draggingReadyStart();
    637          this.draggingReadyDragEnd({ doNotMoveFocus: true });
    638          this.bottomRightMover.focus({ focusVisible: true });
    639          return;
    640        }
    641 
    642        if (Services.appinfo.isWayland) {
    643          return;
    644        }
    645 
    646        // The left and top coordinates from cursorRegion are relative to
    647        // the client window so we need to add the scroll offset of the page to
    648        // get the correct coordinates.
    649        let x = {};
    650        let y = {};
    651        this.window.windowUtils.getLastOverWindowPointerLocationInCSSPixels(
    652          x,
    653          y
    654        );
    655        this.crosshairsDragStart(
    656          x.value + this.windowDimensions.scrollX,
    657          y.value + this.windowDimensions.scrollY
    658        );
    659        this.#setState(STATES.DRAGGING);
    660        break;
    661      }
    662    }
    663  }
    664 
    665  /**
    666   * Handles a keydown event for the dragging state.
    667   *
    668   * @param {Event} event The keydown event
    669   */
    670  draggingKeyDown(event) {
    671    switch (event.key) {
    672      case "ArrowLeft":
    673        this.handleArrowLeftKeyDown(event);
    674        break;
    675      case "ArrowUp":
    676        this.handleArrowUpKeyDown(event);
    677        break;
    678      case "ArrowRight":
    679        this.handleArrowRightKeyDown(event);
    680        break;
    681      case "ArrowDown":
    682        this.handleArrowDownKeyDown(event);
    683        break;
    684      case "Enter":
    685      case " ":
    686        this.#setState(STATES.SELECTED);
    687        return;
    688      default:
    689        return;
    690    }
    691 
    692    this.drawSelectionContainer();
    693  }
    694 
    695  /**
    696   * Handles a keydown event for the resizing state.
    697   *
    698   * @param {Event} event The keydown event
    699   */
    700  resizingKeyDown(event) {
    701    switch (event.key) {
    702      case "ArrowLeft":
    703        this.resizingArrowLeftKeyDown(event);
    704        break;
    705      case "ArrowUp":
    706        this.resizingArrowUpKeyDown(event);
    707        break;
    708      case "ArrowRight":
    709        this.resizingArrowRightKeyDown(event);
    710        break;
    711      case "ArrowDown":
    712        this.resizingArrowDownKeyDown(event);
    713        break;
    714    }
    715  }
    716 
    717  selectedKeyDown(event) {
    718    let isSelectionElement = event.originalTarget.closest(
    719      "#selection-container"
    720    );
    721    switch (event.key) {
    722      case "ArrowLeft":
    723        if (isSelectionElement) {
    724          this.resizingArrowLeftKeyDown(event);
    725        }
    726        break;
    727      case "ArrowUp":
    728        if (isSelectionElement) {
    729          this.resizingArrowUpKeyDown(event);
    730        }
    731        break;
    732      case "ArrowRight":
    733        if (isSelectionElement) {
    734          this.resizingArrowRightKeyDown(event);
    735        }
    736        break;
    737      case "ArrowDown":
    738        if (isSelectionElement) {
    739          this.resizingArrowDownKeyDown(event);
    740        }
    741        break;
    742      case "Tab":
    743        this.maybeLockFocus(event);
    744        break;
    745      case "Enter":
    746      case " ":
    747        this.handleKeyDownOnButton(event);
    748        break;
    749      case this.copyKey.toLowerCase():
    750        if (this.state === "selected" && this.getAccelKey(event)) {
    751          this.copySelectedRegion();
    752        }
    753        break;
    754      case this.downloadKey.toLowerCase():
    755        if (this.state === "selected" && this.getAccelKey(event)) {
    756          this.downloadSelectedRegion();
    757        }
    758        break;
    759    }
    760  }
    761 
    762  /**
    763   * Move the region or its left or right side to the left.
    764   * Just the arrow key will move the region by 1px.
    765   * Arrow key + shift will move the region by 10px.
    766   * Arrow key + control/meta will move to the edge of the window.
    767   *
    768   * @param {Event} event The keydown event
    769   */
    770  resizingArrowLeftKeyDown(event) {
    771    this.handleArrowLeftKeyDown(event);
    772 
    773    if (this.#state !== STATES.RESIZING) {
    774      this.#setState(STATES.RESIZING);
    775    }
    776 
    777    this.drawSelectionContainer();
    778  }
    779 
    780  /**
    781   * Move the region or its left or right side to the left.
    782   * Just the arrow key will move the region by 1px.
    783   * Arrow key + shift will move the region by 10px.
    784   * Arrow key + control/meta will move to the edge of the window.
    785   *
    786   * @param {Event} event The keydown event
    787   */
    788  handleArrowLeftKeyDown(event) {
    789    let exponent = event.shiftKey ? 1 : 0;
    790    switch (event.originalTarget.id) {
    791      case "highlight":
    792        if (this.getAccelKey(event)) {
    793          let width = this.selectionRegion.width;
    794          this.selectionRegion.left = this.windowDimensions.scrollX;
    795          this.selectionRegion.right = this.windowDimensions.scrollX + width;
    796          break;
    797        }
    798 
    799        this.selectionRegion.right -= 10 ** exponent;
    800      // eslint-disable-next-line no-fallthrough
    801      case "mover-topLeft":
    802      case "mover-bottomLeft":
    803        if (this.getAccelKey(event)) {
    804          this.selectionRegion.left = this.windowDimensions.scrollX;
    805          break;
    806        }
    807 
    808        this.selectionRegion.left -= 10 ** exponent;
    809        this.scrollIfByEdge(
    810          this.selectionRegion.left,
    811          this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2
    812        );
    813        break;
    814      case "mover-topRight":
    815      case "mover-bottomRight":
    816        if (this.getAccelKey(event)) {
    817          let left = this.selectionRegion.left;
    818          this.selectionRegion.left = this.windowDimensions.scrollX;
    819          this.selectionRegion.right = left;
    820          if (event.originalTarget.id === "mover-topRight") {
    821            this.topLeftMover.focus({ focusVisible: true });
    822          } else if (event.originalTarget.id === "mover-bottomRight") {
    823            this.bottomLeftMover.focus({ focusVisible: true });
    824          }
    825          break;
    826        }
    827 
    828        this.selectionRegion.right -= 10 ** exponent;
    829        if (this.selectionRegion.x1 >= this.selectionRegion.x2) {
    830          this.selectionRegion.sortCoords();
    831          if (event.originalTarget.id === "mover-topRight") {
    832            this.topLeftMover.focus({ focusVisible: true });
    833          } else if (event.originalTarget.id === "mover-bottomRight") {
    834            this.bottomLeftMover.focus({ focusVisible: true });
    835          }
    836        }
    837        break;
    838    }
    839  }
    840 
    841  /**
    842   * Move the region or its top or bottom side upward.
    843   * Just the arrow key will move the region by 1px.
    844   * Arrow key + shift will move the region by 10px.
    845   * Arrow key + control/meta will move to the edge of the window.
    846   *
    847   * @param {Event} event The keydown event
    848   */
    849  resizingArrowUpKeyDown(event) {
    850    this.handleArrowUpKeyDown(event);
    851 
    852    if (this.#state !== STATES.RESIZING) {
    853      this.#setState(STATES.RESIZING);
    854    }
    855 
    856    this.drawSelectionContainer();
    857  }
    858 
    859  /**
    860   * Move the region or its top or bottom side upward.
    861   * Just the arrow key will move the region by 1px.
    862   * Arrow key + shift will move the region by 10px.
    863   * Arrow key + control/meta will move to the edge of the window.
    864   *
    865   * @param {Event} event The keydown event
    866   */
    867  handleArrowUpKeyDown(event) {
    868    let exponent = event.shiftKey ? 1 : 0;
    869    switch (event.originalTarget.id) {
    870      case "highlight":
    871        if (this.getAccelKey(event)) {
    872          let height = this.selectionRegion.height;
    873          this.selectionRegion.top = this.windowDimensions.scrollY;
    874          this.selectionRegion.bottom = this.windowDimensions.scrollY + height;
    875          break;
    876        }
    877 
    878        this.selectionRegion.bottom -= 10 ** exponent;
    879      // eslint-disable-next-line no-fallthrough
    880      case "mover-topLeft":
    881      case "mover-topRight":
    882        if (this.getAccelKey(event)) {
    883          this.selectionRegion.top = this.windowDimensions.scrollY;
    884          break;
    885        }
    886 
    887        this.selectionRegion.top -= 10 ** exponent;
    888        this.scrollIfByEdge(
    889          this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2,
    890          this.selectionRegion.top
    891        );
    892        break;
    893      case "mover-bottomLeft":
    894      case "mover-bottomRight":
    895        if (this.getAccelKey(event)) {
    896          let top = this.selectionRegion.top;
    897          this.selectionRegion.top = this.windowDimensions.scrollY;
    898          this.selectionRegion.bottom = top;
    899          if (event.originalTarget.id === "mover-bottomLeft") {
    900            this.topLeftMover.focus({ focusVisible: true });
    901          } else if (event.originalTarget.id === "mover-bottomRight") {
    902            this.topRightMover.focus({ focusVisible: true });
    903          }
    904          break;
    905        }
    906 
    907        this.selectionRegion.bottom -= 10 ** exponent;
    908        if (this.selectionRegion.y1 >= this.selectionRegion.y2) {
    909          this.selectionRegion.sortCoords();
    910          if (event.originalTarget.id === "mover-bottomLeft") {
    911            this.topLeftMover.focus({ focusVisible: true });
    912          } else if (event.originalTarget.id === "mover-bottomRight") {
    913            this.topRightMover.focus({ focusVisible: true });
    914          }
    915        }
    916        break;
    917    }
    918  }
    919 
    920  /**
    921   * Move the region or its left or right side to the right.
    922   * Just the arrow key will move the region by 1px.
    923   * Arrow key + shift will move the region by 10px.
    924   * Arrow key + control/meta will move to the edge of the window.
    925   *
    926   * @param {Event} event The keydown event
    927   */
    928  resizingArrowRightKeyDown(event) {
    929    this.handleArrowRightKeyDown(event);
    930 
    931    if (this.#state !== STATES.RESIZING) {
    932      this.#setState(STATES.RESIZING);
    933    }
    934 
    935    this.drawSelectionContainer();
    936  }
    937 
    938  /**
    939   * Move the region or its left or right side to the right.
    940   * Just the arrow key will move the region by 1px.
    941   * Arrow key + shift will move the region by 10px.
    942   * Arrow key + control/meta will move to the edge of the window.
    943   *
    944   * @param {Event} event The keydown event
    945   */
    946  handleArrowRightKeyDown(event) {
    947    let exponent = event.shiftKey ? 1 : 0;
    948    switch (event.originalTarget.id) {
    949      case "highlight":
    950        if (this.getAccelKey(event)) {
    951          let width = this.selectionRegion.width;
    952          let { scrollX, clientWidth } = this.windowDimensions.dimensions;
    953          this.selectionRegion.right = scrollX + clientWidth;
    954          this.selectionRegion.left = this.selectionRegion.right - width;
    955          break;
    956        }
    957 
    958        this.selectionRegion.left += 10 ** exponent;
    959      // eslint-disable-next-line no-fallthrough
    960      case "mover-topRight":
    961      case "mover-bottomRight":
    962        if (this.getAccelKey(event)) {
    963          this.selectionRegion.right =
    964            this.windowDimensions.scrollX + this.windowDimensions.clientWidth;
    965          break;
    966        }
    967 
    968        this.selectionRegion.right += 10 ** exponent;
    969        this.scrollIfByEdge(
    970          this.selectionRegion.right,
    971          this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2
    972        );
    973        break;
    974      case "mover-topLeft":
    975      case "mover-bottomLeft":
    976        if (this.getAccelKey(event)) {
    977          let right = this.selectionRegion.right;
    978          this.selectionRegion.right =
    979            this.windowDimensions.scrollX + this.windowDimensions.clientWidth;
    980          this.selectionRegion.left = right;
    981          if (event.originalTarget.id === "mover-topLeft") {
    982            this.topRightMover.focus({ focusVisible: true });
    983          } else if (event.originalTarget.id === "mover-bottomLeft") {
    984            this.bottomRightMover.focus({ focusVisible: true });
    985          }
    986          break;
    987        }
    988 
    989        this.selectionRegion.left += 10 ** exponent;
    990        if (this.selectionRegion.x1 >= this.selectionRegion.x2) {
    991          this.selectionRegion.sortCoords();
    992          if (event.originalTarget.id === "mover-topLeft") {
    993            this.topRightMover.focus({ focusVisible: true });
    994          } else if (event.originalTarget.id === "mover-bottomLeft") {
    995            this.bottomRightMover.focus({ focusVisible: true });
    996          }
    997        }
    998        break;
    999    }
   1000  }
   1001 
   1002  /**
   1003   * Move the region or its top or bottom side downward.
   1004   * Just the arrow key will move the region by 1px.
   1005   * Arrow key + shift will move the region by 10px.
   1006   * Arrow key + control/meta will move to the edge of the window.
   1007   *
   1008   * @param {Event} event The keydown event
   1009   */
   1010  resizingArrowDownKeyDown(event) {
   1011    this.handleArrowDownKeyDown(event);
   1012 
   1013    if (this.#state !== STATES.RESIZING) {
   1014      this.#setState(STATES.RESIZING);
   1015    }
   1016 
   1017    this.drawSelectionContainer();
   1018  }
   1019 
   1020  handleArrowDownKeyDown(event) {
   1021    let exponent = event.shiftKey ? 1 : 0;
   1022    switch (event.originalTarget.id) {
   1023      case "highlight":
   1024        if (this.getAccelKey(event)) {
   1025          let height = this.selectionRegion.height;
   1026          let { scrollY, clientHeight } = this.windowDimensions.dimensions;
   1027          this.selectionRegion.bottom = scrollY + clientHeight;
   1028          this.selectionRegion.top = this.selectionRegion.bottom - height;
   1029          break;
   1030        }
   1031 
   1032        this.selectionRegion.top += 10 ** exponent;
   1033      // eslint-disable-next-line no-fallthrough
   1034      case "mover-bottomLeft":
   1035      case "mover-bottomRight":
   1036        if (this.getAccelKey(event)) {
   1037          this.selectionRegion.bottom =
   1038            this.windowDimensions.scrollY + this.windowDimensions.clientHeight;
   1039          break;
   1040        }
   1041 
   1042        this.selectionRegion.bottom += 10 ** exponent;
   1043        this.scrollIfByEdge(
   1044          this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2,
   1045          this.selectionRegion.bottom
   1046        );
   1047        break;
   1048      case "mover-topLeft":
   1049      case "mover-topRight":
   1050        if (this.getAccelKey(event)) {
   1051          let bottom = this.selectionRegion.bottom;
   1052          this.selectionRegion.bottom =
   1053            this.windowDimensions.scrollY + this.windowDimensions.clientHeight;
   1054          this.selectionRegion.top = bottom;
   1055          if (event.originalTarget.id === "mover-topLeft") {
   1056            this.bottomLeftMover.focus({ focusVisible: true });
   1057          } else if (event.originalTarget.id === "mover-topRight") {
   1058            this.bottomRightMover.focus({ focusVisible: true });
   1059          }
   1060          break;
   1061        }
   1062 
   1063        this.selectionRegion.top += 10 ** exponent;
   1064        if (this.selectionRegion.y1 >= this.selectionRegion.y2) {
   1065          this.selectionRegion.sortCoords();
   1066          if (event.originalTarget.id === "mover-topLeft") {
   1067            this.bottomLeftMover.focus({ focusVisible: true });
   1068          } else if (event.originalTarget.id === "mover-topRight") {
   1069            this.bottomRightMover.focus({ focusVisible: true });
   1070          }
   1071        }
   1072        break;
   1073    }
   1074  }
   1075 
   1076  /**
   1077   * We lock focus to the overlay when a region is selected.
   1078   * Can still escape with shift + F6.
   1079   *
   1080   * @param {Event} event The keydown event
   1081   */
   1082  maybeLockFocus(event) {
   1083    switch (this.#state) {
   1084      case STATES.CROSSHAIRS:
   1085        if (event.originalTarget === this.previewCancelButton) {
   1086          if (event.shiftKey) {
   1087            this.previewFace.focus({ focusVisible: true });
   1088          } else {
   1089            this.#dispatchEvent("Screenshots:FocusPanel", {
   1090              direction: "forward",
   1091            });
   1092          }
   1093        } else if (event.originalTarget === this.previewFace) {
   1094          if (event.shiftKey) {
   1095            this.#dispatchEvent("Screenshots:FocusPanel", {
   1096              direction: "backward",
   1097            });
   1098          } else {
   1099            this.previewCancelButton.focus({ focusVisible: true });
   1100          }
   1101        }
   1102        break;
   1103      case STATES.SELECTED:
   1104        if (event.originalTarget.id === "highlight" && event.shiftKey) {
   1105          this.downloadButton.focus({ focusVisible: true });
   1106        } else if (event.originalTarget.id === "download" && !event.shiftKey) {
   1107          this.highlightEl.focus({ focusVisible: true });
   1108        } else {
   1109          // The content document can listen for keydown events and prevent moving
   1110          // focus so we manually move focus to the next element here.
   1111          let direction = event.shiftKey
   1112            ? Services.focus.MOVEFOCUS_BACKWARD
   1113            : Services.focus.MOVEFOCUS_FORWARD;
   1114          Services.focus.moveFocus(
   1115            this.window,
   1116            null,
   1117            direction,
   1118            Services.focus.FLAG_BYKEY
   1119          );
   1120        }
   1121        break;
   1122    }
   1123  }
   1124 
   1125  /**
   1126   * Set the focus to the most recent saved method.
   1127   * This will default to the download button.
   1128   */
   1129  setFocusToActionButton() {
   1130    if (lazy.SCREENSHOTS_LAST_SAVED_METHOD === "copy") {
   1131      this.copyButton.focus({ focusVisible: true, preventScroll: true });
   1132    } else {
   1133      this.downloadButton.focus({ focusVisible: true, preventScroll: true });
   1134    }
   1135  }
   1136 
   1137  /**
   1138   * We prevent all events in ScreenshotsComponentChild so we need to
   1139   * explicitly handle keydown events on buttons here.
   1140   *
   1141   * @param {KeyEvent} event The keydown event
   1142   *
   1143   * @returns {boolean} True if the event was handled here, otherwise false.
   1144   */
   1145  handleKeyDownOnButton(event) {
   1146    switch (event.originalTarget) {
   1147      case this.cancelButton:
   1148      case this.previewCancelButton:
   1149        this.maybeCancelScreenshots();
   1150        break;
   1151      case this.copyButton:
   1152        this.copySelectedRegion();
   1153        break;
   1154      case this.downloadButton:
   1155        this.downloadSelectedRegion();
   1156        break;
   1157      default:
   1158        return false;
   1159    }
   1160    return true;
   1161  }
   1162 
   1163  /**
   1164   * All of the selection ranges were recorded at initialization. The ranges
   1165   * are removed when focus is set to the buttons so we add the selection
   1166   * ranges back so a selected region can be captured.
   1167   */
   1168  handleSelectionChange() {
   1169    if (this.ranges.length) {
   1170      for (let range of this.ranges) {
   1171        this.selection.addRange(range);
   1172      }
   1173    }
   1174  }
   1175 
   1176  /**
   1177   * Dispatch a custom event to the ScreenshotsComponentChild actor
   1178   *
   1179   * @param {string} eventType The name of the event
   1180   * @param {object} detail Extra details to send to the child actor
   1181   */
   1182  #dispatchEvent(eventType, detail) {
   1183    this.window.windowUtils.dispatchEventToChromeOnly(
   1184      this.window,
   1185      new CustomEvent(eventType, {
   1186        bubbles: true,
   1187        detail,
   1188      })
   1189    );
   1190  }
   1191 
   1192  /**
   1193   * Set a new state for the overlay
   1194   *
   1195   * @param {string} newState
   1196   * @param {object} options (optional) Options for calling start of state method
   1197   */
   1198  #setState(newState, options = {}) {
   1199    if (this.#state === STATES.SELECTED && newState === STATES.CROSSHAIRS) {
   1200      this.#dispatchEvent("Screenshots:RecordEvent", {
   1201        eventName: "startedOverlayRetry",
   1202      });
   1203    }
   1204    if (newState !== this.#state) {
   1205      this.#dispatchEvent("Screenshots:OverlaySelection", {
   1206        hasSelection: [
   1207          STATES.DRAGGING_READY,
   1208          STATES.DRAGGING,
   1209          STATES.RESIZING,
   1210          STATES.SELECTED,
   1211        ].includes(newState),
   1212        overlayState: newState,
   1213      });
   1214    }
   1215    this.#state = newState;
   1216 
   1217    switch (this.#state) {
   1218      case STATES.CROSSHAIRS: {
   1219        this.crosshairsStart();
   1220        break;
   1221      }
   1222      case STATES.DRAGGING_READY: {
   1223        this.draggingReadyStart();
   1224        break;
   1225      }
   1226      case STATES.DRAGGING: {
   1227        this.draggingStart();
   1228        break;
   1229      }
   1230      case STATES.SELECTED: {
   1231        this.selectedStart(options);
   1232        break;
   1233      }
   1234      case STATES.RESIZING: {
   1235        this.resizingStart();
   1236        break;
   1237      }
   1238    }
   1239  }
   1240 
   1241  copySelectedRegion() {
   1242    this.#dispatchEvent("Screenshots:Copy", {
   1243      region: this.selectionRegion.dimensions,
   1244    });
   1245  }
   1246 
   1247  downloadSelectedRegion() {
   1248    this.#dispatchEvent("Screenshots:Download", {
   1249      region: this.selectionRegion.dimensions,
   1250    });
   1251  }
   1252 
   1253  /**
   1254   * Hide hover element, selection and buttons containers.
   1255   * Show the preview container and the panel.
   1256   * This is the initial state of the overlay.
   1257   */
   1258  crosshairsStart() {
   1259    this.hideHoverElementContainer();
   1260    this.hideSelectionContainer();
   1261    this.hideButtonsContainer();
   1262    this.showPreviewContainer();
   1263    this.#dispatchEvent("Screenshots:ShowPanel");
   1264    this.#previousDimensions = null;
   1265    this.#cachedEle = null;
   1266    this.hoverElementRegion.resetDimensions();
   1267  }
   1268 
   1269  /**
   1270   * Hide the panel because we have started dragging.
   1271   */
   1272  draggingReadyStart() {
   1273    this.#dispatchEvent("Screenshots:HidePanel");
   1274  }
   1275 
   1276  /**
   1277   * Hide the preview, hover element and buttons containers.
   1278   * Show the selection container.
   1279   */
   1280  draggingStart() {
   1281    this.hidePreviewContainer();
   1282    this.hideButtonsContainer();
   1283    this.hideHoverElementContainer();
   1284    this.drawSelectionContainer();
   1285  }
   1286 
   1287  /**
   1288   * Hide the preview and hover element containers.
   1289   * Draw the selection and buttons containers.
   1290   *
   1291   * @param {object} [options={}] (optional)
   1292   * @param {boolean} doNotMoveFocus True if focus should not be moved to an action button
   1293   */
   1294  selectedStart(options = {}) {
   1295    this.selectionRegion.sortCoords();
   1296    this.hidePreviewContainer();
   1297    this.hideHoverElementContainer();
   1298    this.drawSelectionContainer();
   1299    this.drawButtonsContainer();
   1300 
   1301    if (!options.doNotMoveFocus) {
   1302      this.setFocusToActionButton();
   1303    }
   1304  }
   1305 
   1306  /**
   1307   * Hide the buttons container.
   1308   * Store the width and height of the current selected region.
   1309   * The dimensions will be used when moving the region along the edge of the
   1310   * page and for recording telemetry.
   1311   */
   1312  resizingStart() {
   1313    this.hideButtonsContainer();
   1314    let { width, height } = this.selectionRegion.dimensions;
   1315    this.#previousDimensions = { width, height };
   1316  }
   1317 
   1318  /**
   1319   * Dragging has started so we set the initial selection region and set the
   1320   * state to draggingReady.
   1321   *
   1322   * @param {number} pageX The x position relative to the page
   1323   * @param {number} pageY The y position relative to the page
   1324   */
   1325  crosshairsDragStart(pageX, pageY) {
   1326    this.selectionRegion.dimensions = {
   1327      left: pageX,
   1328      top: pageY,
   1329      right: pageX,
   1330      bottom: pageY,
   1331    };
   1332 
   1333    this.#setState(STATES.DRAGGING_READY);
   1334  }
   1335 
   1336  /**
   1337   * If the background is clicked we set the state to crosshairs
   1338   * otherwise set the state to resizing
   1339   *
   1340   * @param {number} pageX The x position relative to the page
   1341   * @param {number} pageY The y position relative to the page
   1342   * @param {string} targetId The id of the event target
   1343   */
   1344  selectedDragStart(pageX, pageY, targetId) {
   1345    if (targetId === this.screenshotsContainer.id) {
   1346      this.#setState(STATES.CROSSHAIRS);
   1347      return;
   1348    }
   1349    this.#moverId = targetId;
   1350    this.#lastPageX = pageX;
   1351    this.#lastPageY = pageY;
   1352 
   1353    this.#setState(STATES.RESIZING);
   1354  }
   1355 
   1356  /**
   1357   * Draw the eyes in the preview container and find the element currently
   1358   * being hovered.
   1359   *
   1360   * @param {number} clientX The x position relative to the viewport
   1361   * @param {number} clientY The y position relative to the viewport
   1362   */
   1363  crosshairsMove(clientX, clientY) {
   1364    this.drawPreviewEyes(clientX, clientY);
   1365 
   1366    this.handleElementHover(clientX, clientY);
   1367  }
   1368 
   1369  /**
   1370   * Set the selection region dimensions and if the region is at least 40
   1371   * pixels diagnally in distance, set the state to dragging.
   1372   *
   1373   * @param {number} pageX The x position relative to the page
   1374   * @param {number} pageY The y position relative to the page
   1375   */
   1376  draggingReadyDrag(pageX, pageY) {
   1377    this.selectionRegion.dimensions = {
   1378      right: pageX,
   1379      bottom: pageY,
   1380    };
   1381 
   1382    if (this.selectionRegion.distance > 40) {
   1383      this.#setState(STATES.DRAGGING);
   1384    }
   1385  }
   1386 
   1387  /**
   1388   * Scroll if along the edge of the viewport, update the selection region
   1389   * dimensions and draw the selection container.
   1390   *
   1391   * @param {number} pageX The x position relative to the page
   1392   * @param {number} pageY The y position relative to the page
   1393   */
   1394  draggingDrag(pageX, pageY) {
   1395    this.scrollIfByEdge(pageX, pageY);
   1396    this.selectionRegion.dimensions = {
   1397      right: pageX,
   1398      bottom: pageY,
   1399    };
   1400 
   1401    this.drawSelectionContainer();
   1402  }
   1403 
   1404  /**
   1405   * Resize the selection region depending on the mover that started the resize.
   1406   *
   1407   * @param {number} pageX The x position relative to the page
   1408   * @param {number} pageY The y position relative to the page
   1409   */
   1410  resizingDrag(pageX, pageY) {
   1411    this.scrollIfByEdge(pageX, pageY);
   1412    switch (this.#moverId) {
   1413      case "mover-topLeft": {
   1414        this.selectionRegion.dimensions = {
   1415          left: pageX,
   1416          top: pageY,
   1417        };
   1418        break;
   1419      }
   1420      case "mover-top": {
   1421        this.selectionRegion.dimensions = { top: pageY };
   1422        break;
   1423      }
   1424      case "mover-topRight": {
   1425        this.selectionRegion.dimensions = {
   1426          top: pageY,
   1427          right: pageX,
   1428        };
   1429        break;
   1430      }
   1431      case "mover-right": {
   1432        this.selectionRegion.dimensions = {
   1433          right: pageX,
   1434        };
   1435        break;
   1436      }
   1437      case "mover-bottomRight": {
   1438        this.selectionRegion.dimensions = {
   1439          right: pageX,
   1440          bottom: pageY,
   1441        };
   1442        break;
   1443      }
   1444      case "mover-bottom": {
   1445        this.selectionRegion.dimensions = {
   1446          bottom: pageY,
   1447        };
   1448        break;
   1449      }
   1450      case "mover-bottomLeft": {
   1451        this.selectionRegion.dimensions = {
   1452          left: pageX,
   1453          bottom: pageY,
   1454        };
   1455        break;
   1456      }
   1457      case "mover-left": {
   1458        this.selectionRegion.dimensions = { left: pageX };
   1459        break;
   1460      }
   1461      case "highlight": {
   1462        let diffX = this.#lastPageX - pageX;
   1463        let diffY = this.#lastPageY - pageY;
   1464 
   1465        let newLeft;
   1466        let newRight;
   1467        let newTop;
   1468        let newBottom;
   1469 
   1470        // Unpack dimensions to use here
   1471        let {
   1472          left: boxLeft,
   1473          top: boxTop,
   1474          right: boxRight,
   1475          bottom: boxBottom,
   1476          width: boxWidth,
   1477          height: boxHeight,
   1478        } = this.selectionRegion.dimensions;
   1479        let { scrollWidth, scrollHeight } = this.windowDimensions.dimensions;
   1480 
   1481        // wait until all 4 if elses have completed before setting box dimensions
   1482        if (boxWidth <= this.#previousDimensions.width && boxLeft === 0) {
   1483          newLeft = boxRight - this.#previousDimensions.width;
   1484        } else {
   1485          newLeft = boxLeft;
   1486        }
   1487 
   1488        if (
   1489          boxWidth <= this.#previousDimensions.width &&
   1490          boxRight === scrollWidth
   1491        ) {
   1492          newRight = boxLeft + this.#previousDimensions.width;
   1493        } else {
   1494          newRight = boxRight;
   1495        }
   1496 
   1497        if (boxHeight <= this.#previousDimensions.height && boxTop === 0) {
   1498          newTop = boxBottom - this.#previousDimensions.height;
   1499        } else {
   1500          newTop = boxTop;
   1501        }
   1502 
   1503        if (
   1504          boxHeight <= this.#previousDimensions.height &&
   1505          boxBottom === scrollHeight
   1506        ) {
   1507          newBottom = boxTop + this.#previousDimensions.height;
   1508        } else {
   1509          newBottom = boxBottom;
   1510        }
   1511 
   1512        this.selectionRegion.dimensions = {
   1513          left: newLeft - diffX,
   1514          top: newTop - diffY,
   1515          right: newRight - diffX,
   1516          bottom: newBottom - diffY,
   1517        };
   1518 
   1519        this.#lastPageX = pageX;
   1520        this.#lastPageY = pageY;
   1521        break;
   1522      }
   1523    }
   1524    this.drawSelectionContainer();
   1525  }
   1526 
   1527  /**
   1528   * If there is a valid element region, update and draw the selection
   1529   * container and set the state to selected.
   1530   * Otherwise set the state to crosshairs.
   1531   *
   1532   * @param {object} options (optional) Options for passing to setState method
   1533   */
   1534  draggingReadyDragEnd(options = {}) {
   1535    if (this.hoverElementRegion.isRegionValid) {
   1536      this.selectionRegion.dimensions = this.hoverElementRegion.dimensions;
   1537      this.#setState(STATES.SELECTED, options);
   1538      this.#dispatchEvent("Screenshots:RecordEvent", {
   1539        eventName: "selectedElement",
   1540      });
   1541      this.#methodsUsed.element += 1;
   1542    } else {
   1543      this.#setState(STATES.CROSSHAIRS);
   1544    }
   1545  }
   1546 
   1547  /**
   1548   * Update the selection region dimensions and set the state to selected.
   1549   *
   1550   * @param {number} pageX The x position relative to the page
   1551   * @param {number} pageY The y position relative to the page
   1552   */
   1553  draggingDragEnd(pageX, pageY) {
   1554    this.selectionRegion.dimensions = {
   1555      right: pageX,
   1556      bottom: pageY,
   1557    };
   1558    this.#setState(STATES.SELECTED);
   1559    this.maybeRecordRegionSelected();
   1560    this.#methodsUsed.region += 1;
   1561  }
   1562 
   1563  /**
   1564   * Update the selection region dimensions by calling `resizingDrag` and set
   1565   * the state to selected.
   1566   *
   1567   * @param {number} pageX The x position relative to the page
   1568   * @param {number} pageY The y position relative to the page
   1569   */
   1570  resizingDragEnd(pageX, pageY) {
   1571    this.resizingDrag(pageX, pageY);
   1572    this.#setState(STATES.SELECTED);
   1573    this.maybeRecordRegionSelected();
   1574    if (this.#moverId === "highlight") {
   1575      this.#methodsUsed.move += 1;
   1576    } else {
   1577      this.#methodsUsed.resize += 1;
   1578    }
   1579  }
   1580 
   1581  maybeRecordRegionSelected() {
   1582    let { width, height } = this.selectionRegion.dimensions;
   1583 
   1584    if (
   1585      !this.#previousDimensions ||
   1586      (Math.abs(this.#previousDimensions.width - width) >
   1587        REGION_CHANGE_THRESHOLD &&
   1588        Math.abs(this.#previousDimensions.height - height) >
   1589          REGION_CHANGE_THRESHOLD)
   1590    ) {
   1591      this.#dispatchEvent("Screenshots:RecordEvent", {
   1592        eventName: "selectedRegionSelection",
   1593      });
   1594    }
   1595    this.#previousDimensions = { width, height };
   1596  }
   1597 
   1598  /**
   1599   * Draw the preview eyes pointer towards the mouse.
   1600   *
   1601   * @param {number} clientX The x position relative to the viewport
   1602   * @param {number} clientY The y position relative to the viewport
   1603   */
   1604  drawPreviewEyes(clientX, clientY) {
   1605    let { clientWidth, clientHeight } = this.windowDimensions.dimensions;
   1606    const xpos = Math.floor((10 * (clientX - clientWidth / 2)) / clientWidth);
   1607    const ypos = Math.floor((10 * (clientY - clientHeight / 2)) / clientHeight);
   1608    const move = `transform:translate(${xpos}px, ${ypos}px);`;
   1609    this.leftEye.style = move;
   1610    this.rightEye.style = move;
   1611  }
   1612 
   1613  showPreviewContainer() {
   1614    this.previewContainer.hidden = false;
   1615  }
   1616 
   1617  hidePreviewContainer() {
   1618    this.previewContainer.hidden = true;
   1619  }
   1620 
   1621  updatePreviewContainer() {
   1622    let { clientWidth, clientHeight } = this.windowDimensions.dimensions;
   1623    this.previewContainer.style.width = `${clientWidth}px`;
   1624    this.previewContainer.style.height = `${clientHeight}px`;
   1625  }
   1626 
   1627  /**
   1628   * Update the screenshots overlay container based on the window dimensions.
   1629   */
   1630  updateScreenshotsOverlayContainer() {
   1631    let { scrollWidth, scrollHeight, scrollMinX } =
   1632      this.windowDimensions.dimensions;
   1633    this.screenshotsContainer.style = `left:${scrollMinX};width:${scrollWidth}px;height:${scrollHeight}px;`;
   1634  }
   1635 
   1636  showScreenshotsOverlayContainer() {
   1637    this.screenshotsContainer.hidden = false;
   1638  }
   1639 
   1640  hideScreenshotsOverlayContainer() {
   1641    this.screenshotsContainer.hidden = true;
   1642  }
   1643 
   1644  /**
   1645   * Draw the hover element container based on the hover element region.
   1646   */
   1647  drawHoverElementRegion() {
   1648    this.showHoverElementContainer();
   1649 
   1650    let { top, left, width, height } = this.hoverElementRegion.dimensions;
   1651 
   1652    this.hoverElementContainer.style = `top:${top}px;left:${left}px;width:${width}px;height:${height}px;`;
   1653  }
   1654 
   1655  showHoverElementContainer() {
   1656    this.hoverElementContainer.hidden = false;
   1657  }
   1658 
   1659  hideHoverElementContainer() {
   1660    this.hoverElementContainer.hidden = true;
   1661  }
   1662 
   1663  /**
   1664   * Draw each background element and the highlight element base on the
   1665   * selection region.
   1666   */
   1667  drawSelectionContainer() {
   1668    this.showSelectionContainer();
   1669 
   1670    let { top, left, right, bottom, width, height } =
   1671      this.selectionRegion.dimensions;
   1672 
   1673    this.highlightEl.style = `top:${top}px;left:${left}px;width:${width}px;height:${height}px;`;
   1674 
   1675    this.leftBackgroundEl.style = `top:${top}px;width:${left}px;height:${height}px;`;
   1676    this.topBackgroundEl.style.height = `${top}px`;
   1677    this.rightBackgroundEl.style = `top:${top}px;left:${right}px;width:calc(100% - ${right}px);height:${height}px;`;
   1678    this.bottomBackgroundEl.style = `top:${bottom}px;height:calc(100% - ${bottom}px);`;
   1679 
   1680    this.updateSelectionSizeText();
   1681  }
   1682 
   1683  /**
   1684   * Update the size of the selected region. Use the zoom to correctly display
   1685   * the region dimensions.
   1686   */
   1687  updateSelectionSizeText() {
   1688    let { width, height } = this.selectionRegion.dimensions;
   1689    let zoom = Math.round(this.window.browsingContext.fullZoom * 100) / 100;
   1690 
   1691    let [selectionSizeTranslation] =
   1692      lazy.overlayLocalization.formatMessagesSync([
   1693        {
   1694          id: "screenshots-overlay-selection-region-size-3",
   1695          args: {
   1696            width: Math.floor(width * zoom),
   1697            height: Math.floor(height * zoom),
   1698          },
   1699        },
   1700      ]);
   1701    this.selectionSize.textContent = selectionSizeTranslation.value;
   1702  }
   1703 
   1704  showSelectionContainer() {
   1705    this.selectionContainer.hidden = false;
   1706  }
   1707 
   1708  hideSelectionContainer() {
   1709    this.selectionContainer.hidden = true;
   1710  }
   1711 
   1712  /**
   1713   * Draw the buttons container in the bottom right corner of the selection
   1714   * container if possible.
   1715   * The buttons will be visible in the viewport if the selection container
   1716   * is within the viewport, otherwise skip drawing the buttons.
   1717   */
   1718  drawButtonsContainer() {
   1719    this.showButtonsContainer();
   1720 
   1721    let {
   1722      left: boxLeft,
   1723      top: boxTop,
   1724      right: boxRight,
   1725      bottom: boxBottom,
   1726    } = this.selectionRegion.dimensions;
   1727 
   1728    let { clientWidth, clientHeight, scrollX, scrollY, scrollWidth } =
   1729      this.windowDimensions.dimensions;
   1730 
   1731    if (!this.windowDimensions.isInViewport(this.selectionRegion.dimensions)) {
   1732      // The box is entirely offscreen so need to draw the buttons
   1733 
   1734      return;
   1735    }
   1736 
   1737    let top = boxBottom;
   1738 
   1739    if (scrollY + clientHeight - boxBottom < 70) {
   1740      if (boxBottom < scrollY + clientHeight) {
   1741        top = boxBottom - 60;
   1742      } else if (scrollY + clientHeight - boxTop < 70) {
   1743        top = boxTop - 60;
   1744      } else {
   1745        top = scrollY + clientHeight - 60;
   1746      }
   1747    }
   1748 
   1749    if (!this.buttonsContainerRect) {
   1750      this.buttonsContainerRect = this.buttonsContainer.getBoundingClientRect();
   1751    }
   1752 
   1753    let viewportLeft = scrollX;
   1754    let viewportRight = scrollX + clientWidth;
   1755 
   1756    let left, right;
   1757    let isLTR = !Services.locale.isAppLocaleRTL;
   1758    if (isLTR) {
   1759      left = Math.max(
   1760        Math.min(viewportRight, boxRight),
   1761        viewportLeft + Math.ceil(this.buttonsContainerRect.width)
   1762      );
   1763      right = scrollWidth - left;
   1764 
   1765      this.buttonsContainer.style.right = `${right}px`;
   1766      this.buttonsContainer.style.left = "";
   1767    } else {
   1768      left = Math.min(
   1769        Math.max(viewportLeft, boxLeft),
   1770        viewportRight - Math.ceil(this.buttonsContainerRect.width)
   1771      );
   1772 
   1773      this.buttonsContainer.style.left = `${left}px`;
   1774      this.buttonsContainer.style.right = "";
   1775    }
   1776 
   1777    this.buttonsContainer.style.top = `${top}px`;
   1778  }
   1779 
   1780  showButtonsContainer() {
   1781    this.buttonsContainer.hidden = false;
   1782  }
   1783 
   1784  hideButtonsContainer() {
   1785    this.buttonsContainer.hidden = true;
   1786  }
   1787 
   1788  updateCursorRegion(left, top) {
   1789    this.cursorRegion = { left, top, right: left, bottom: top };
   1790  }
   1791 
   1792  /**
   1793   * Set the pointer events to none on the screenshots elements so
   1794   * elementFromPoint can find the real element at the given point.
   1795   */
   1796  setPointerEventsNone() {
   1797    this.screenshotsContainer.style.pointerEvents = "none";
   1798  }
   1799 
   1800  resetPointerEvents() {
   1801    this.screenshotsContainer.style.pointerEvents = "";
   1802  }
   1803 
   1804  /**
   1805   * Try to find a reasonable element for a given point.
   1806   * If a reasonable element is found, draw the hover element container for
   1807   * that element region.
   1808   *
   1809   * @param {number} clientX The x position relative to the viewport
   1810   * @param {number} clientY The y position relative to the viewport
   1811   */
   1812  async handleElementHover(clientX, clientY) {
   1813    this.setPointerEventsNone();
   1814    let promise = getElementFromPoint(clientX, clientY, this.document);
   1815    this.resetPointerEvents();
   1816    let { ele, rect } = await promise;
   1817 
   1818    if (
   1819      this.#cachedEle &&
   1820      !this.window.HTMLIFrameElement.isInstance(this.#cachedEle) &&
   1821      this.#cachedEle === ele
   1822    ) {
   1823      // Still hovering over the same element
   1824      return;
   1825    }
   1826    this.#cachedEle = ele;
   1827 
   1828    if (!rect) {
   1829      // this means we found an element that wasn't an iframe
   1830      rect = getBestRectForElement(ele, this.document);
   1831    }
   1832 
   1833    if (rect) {
   1834      let { scrollX, scrollY } = this.windowDimensions.dimensions;
   1835      let { left, top, right, bottom } = rect;
   1836      let newRect = {
   1837        left: left + scrollX,
   1838        top: top + scrollY,
   1839        right: right + scrollX,
   1840        bottom: bottom + scrollY,
   1841      };
   1842      this.hoverElementRegion.dimensions = newRect;
   1843      this.drawHoverElementRegion();
   1844    } else {
   1845      this.hoverElementRegion.resetDimensions();
   1846      this.hideHoverElementContainer();
   1847    }
   1848  }
   1849 
   1850  /**
   1851   * Scroll the viewport if near one or both of the edges.
   1852   *
   1853   * @param {number} pageX The x position relative to the page
   1854   * @param {number} pageY The y position relative to the page
   1855   */
   1856  scrollIfByEdge(pageX, pageY) {
   1857    let { scrollX, scrollY, clientWidth, clientHeight } =
   1858      this.windowDimensions.dimensions;
   1859 
   1860    if (pageY - scrollY < SCROLL_BY_EDGE) {
   1861      // Scroll up
   1862      this.scrollWindow(0, -(SCROLL_BY_EDGE - (pageY - scrollY)));
   1863    } else if (scrollY + clientHeight - pageY < SCROLL_BY_EDGE) {
   1864      // Scroll down
   1865      this.scrollWindow(0, SCROLL_BY_EDGE - (scrollY + clientHeight - pageY));
   1866    }
   1867 
   1868    if (pageX - scrollX <= SCROLL_BY_EDGE) {
   1869      // Scroll left
   1870      this.scrollWindow(-(SCROLL_BY_EDGE - (pageX - scrollX)), 0);
   1871    } else if (scrollX + clientWidth - pageX <= SCROLL_BY_EDGE) {
   1872      // Scroll right
   1873      this.scrollWindow(SCROLL_BY_EDGE - (scrollX + clientWidth - pageX), 0);
   1874    }
   1875  }
   1876 
   1877  /**
   1878   * Scroll the window by the given amount.
   1879   *
   1880   * @param {number} x The x amount to scroll
   1881   * @param {number} y The y amount to scroll
   1882   */
   1883  scrollWindow(x, y) {
   1884    this.window.scrollBy(x, y);
   1885    this.updateScreenshotsOverlayDimensions("scroll");
   1886  }
   1887 
   1888  /**
   1889   * The page was resized or scrolled. We need to update the screenshots
   1890   * container size so we don't draw outside the page bounds.
   1891   *
   1892   * @param {string} eventType will be "scroll" or "resize"
   1893   */
   1894  async updateScreenshotsOverlayDimensions(eventType) {
   1895    let updateWindowDimensionsPromise = this.updateWindowDimensions();
   1896 
   1897    if (this.#state === STATES.CROSSHAIRS) {
   1898      if (eventType === "resize") {
   1899        this.hideHoverElementContainer();
   1900        this.#cachedEle = null;
   1901      } else if (eventType === "scroll") {
   1902        if (this.#lastClientX && this.#lastClientY) {
   1903          this.#cachedEle = null;
   1904          this.handleElementHover(this.#lastClientX, this.#lastClientY);
   1905        }
   1906      }
   1907    } else if (this.#state === STATES.SELECTED) {
   1908      await updateWindowDimensionsPromise;
   1909      this.selectionRegion.shift();
   1910      this.drawSelectionContainer();
   1911      this.drawButtonsContainer();
   1912      this.updateSelectionSizeText();
   1913    }
   1914  }
   1915 
   1916  /**
   1917   * Returns the window's dimensions for the current window.
   1918   *
   1919   * @return {object} An object containing window dimensions
   1920   *   {
   1921   *     clientWidth: The width of the viewport
   1922   *     clientHeight: The height of the viewport
   1923   *     scrollWidth: The width of the enitre page
   1924   *     scrollHeight: The height of the entire page
   1925   *     scrollX: The X scroll offset of the viewport
   1926   *     scrollY: The Y scroll offest of the viewport
   1927   *     scrollMinX: The X minimum the viewport can scroll to
   1928   *     scrollMinY: The Y minimum the viewport can scroll to
   1929   *     scrollMaxX: The X maximum the viewport can scroll to
   1930   *     scrollMaxY: The Y maximum the viewport can scroll to
   1931   *   }
   1932   */
   1933  getDimensionsFromWindow() {
   1934    let {
   1935      innerHeight,
   1936      innerWidth,
   1937      scrollMaxY,
   1938      scrollMaxX,
   1939      scrollMinY,
   1940      scrollMinX,
   1941      scrollY,
   1942      scrollX,
   1943    } = this.window;
   1944 
   1945    let scrollWidth = innerWidth + scrollMaxX - scrollMinX;
   1946    let scrollHeight = innerHeight + scrollMaxY - scrollMinY;
   1947    let clientHeight = innerHeight;
   1948    let clientWidth = innerWidth;
   1949 
   1950    const scrollbarHeight = {};
   1951    const scrollbarWidth = {};
   1952    this.window.windowUtils.getScrollbarSize(
   1953      false,
   1954      scrollbarWidth,
   1955      scrollbarHeight
   1956    );
   1957    scrollWidth -= scrollbarWidth.value;
   1958    scrollHeight -= scrollbarHeight.value;
   1959    clientWidth -= scrollbarWidth.value;
   1960    clientHeight -= scrollbarHeight.value;
   1961 
   1962    return {
   1963      clientWidth,
   1964      clientHeight,
   1965      scrollWidth,
   1966      scrollHeight,
   1967      scrollX,
   1968      scrollY,
   1969      scrollMinX,
   1970      scrollMinY,
   1971      scrollMaxX,
   1972      scrollMaxY,
   1973    };
   1974  }
   1975 
   1976  /**
   1977   * We have to be careful not to draw the overlay larger than the document
   1978   * because the overlay is absolutely position and within the document so we
   1979   * can cause the document to overflow when it shouldn't. To mitigate this,
   1980   * we will temporarily position the overlay to position fixed with width and
   1981   * height 100% so the overlay is within the document bounds. Then we will get
   1982   * the dimensions of the document to correctly draw the overlay.
   1983   */
   1984  async updateWindowDimensions() {
   1985    // Setting the screenshots container attribute "resizing" will make the
   1986    // overlay fixed position with width and height of 100% percent so it
   1987    // does not draw outside the actual document.
   1988    this.screenshotsContainer.toggleAttribute("resizing", true);
   1989 
   1990    await new Promise(r => this.window.requestAnimationFrame(r));
   1991 
   1992    let {
   1993      clientWidth,
   1994      clientHeight,
   1995      scrollWidth,
   1996      scrollHeight,
   1997      scrollX,
   1998      scrollY,
   1999      scrollMinX,
   2000      scrollMinY,
   2001      scrollMaxX,
   2002      scrollMaxY,
   2003    } = this.getDimensionsFromWindow();
   2004    this.screenshotsContainer.toggleAttribute("resizing", false);
   2005 
   2006    this.windowDimensions.dimensions = {
   2007      clientWidth,
   2008      clientHeight,
   2009      scrollWidth,
   2010      scrollHeight,
   2011      scrollX,
   2012      scrollY,
   2013      scrollMinX,
   2014      scrollMinY,
   2015      scrollMaxX,
   2016      scrollMaxY,
   2017      devicePixelRatio: this.window.devicePixelRatio,
   2018    };
   2019 
   2020    this.updatePreviewContainer();
   2021    this.updateScreenshotsOverlayContainer();
   2022 
   2023    setMaxDetectHeight(Math.max(clientHeight + 100, 700));
   2024    setMaxDetectWidth(Math.max(clientWidth + 100, 1000));
   2025  }
   2026 }