1982 lines
59 KiB
JavaScript
1982 lines
59 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/. */
|
|
|
|
/**
|
|
* The Screenshots overlay is inserted into the document's
|
|
* anonymous content container (see dom/webidl/Document.webidl).
|
|
*
|
|
* This container gets cleared automatically when the document navigates.
|
|
*
|
|
* To retrieve the AnonymousContent instance, use the `content` getter.
|
|
*/
|
|
|
|
/*
|
|
* Below are the states of the screenshots overlay
|
|
* States:
|
|
* "crosshairs":
|
|
* Nothing has happened, and the crosshairs will follow the movement of the mouse
|
|
* "draggingReady":
|
|
* The user has pressed the mouse button, but hasn't moved enough to create a selection
|
|
* "dragging":
|
|
* The user has pressed down a mouse button, and is dragging out an area far enough to show a selection
|
|
* "selected":
|
|
* The user has selected an area
|
|
* "resizing":
|
|
* The user is resizing the selection
|
|
*/
|
|
|
|
import {
|
|
setMaxDetectHeight,
|
|
setMaxDetectWidth,
|
|
getBestRectForElement,
|
|
getElementFromPoint,
|
|
Region,
|
|
WindowDimensions,
|
|
} from "chrome://browser/content/screenshots/overlayHelpers.mjs";
|
|
|
|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
import { ShortcutUtils } from "resource://gre/modules/ShortcutUtils.sys.mjs";
|
|
|
|
const STATES = {
|
|
CROSSHAIRS: "crosshairs",
|
|
DRAGGING_READY: "draggingReady",
|
|
DRAGGING: "dragging",
|
|
SELECTED: "selected",
|
|
RESIZING: "resizing",
|
|
};
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "overlayLocalization", () => {
|
|
return new Localization(["browser/screenshots.ftl"], true);
|
|
});
|
|
|
|
const SCREENSHOTS_LAST_SAVED_METHOD_PREF =
|
|
"screenshots.browser.component.last-saved-method";
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"SCREENSHOTS_LAST_SAVED_METHOD",
|
|
SCREENSHOTS_LAST_SAVED_METHOD_PREF,
|
|
"download"
|
|
);
|
|
|
|
const REGION_CHANGE_THRESHOLD = 5;
|
|
const SCROLL_BY_EDGE = 20;
|
|
|
|
export class ScreenshotsOverlay {
|
|
#content;
|
|
#initialized = false;
|
|
#state = "";
|
|
#moverId;
|
|
#cachedEle;
|
|
#lastPageX;
|
|
#lastPageY;
|
|
#lastClientX;
|
|
#lastClientY;
|
|
#previousDimensions;
|
|
#methodsUsed;
|
|
|
|
get markup() {
|
|
let accelString = ShortcutUtils.getModifierString("accel");
|
|
let copyShorcut = accelString + this.copyKey;
|
|
let downloadShortcut = accelString + this.downloadKey;
|
|
|
|
let [
|
|
cancelLabel,
|
|
cancelAttributes,
|
|
instructions,
|
|
downloadAttributes,
|
|
copyAttributes,
|
|
previewFaceAriaLabel,
|
|
] = lazy.overlayLocalization.formatMessagesSync([
|
|
{ id: "screenshots-cancel-button" },
|
|
{ id: "screenshots-component-cancel-button" },
|
|
{ id: "screenshots-instructions" },
|
|
{
|
|
id: "screenshots-component-download-button-2",
|
|
args: { shortcut: downloadShortcut },
|
|
},
|
|
{
|
|
id: "screenshots-component-copy-button-2",
|
|
args: { shortcut: copyShorcut },
|
|
},
|
|
{ id: "screenshots-overlay-preview-face-label" },
|
|
]);
|
|
|
|
return `
|
|
<template>
|
|
<link rel="stylesheet" href="chrome://browser/content/screenshots/overlay/overlay.css" />
|
|
<div id="screenshots-component">
|
|
<div id="preview-container" hidden>
|
|
<div id="face-container" tabindex="0" role="button" aria-label="${previewFaceAriaLabel.attributes[0].value}">
|
|
<div class="eye left"><div id="left-eye" class="eyeball"></div></div>
|
|
<div class="eye right"><div id="right-eye" class="eyeball"></div></div>
|
|
<div class="face"></div>
|
|
</div>
|
|
<div class="preview-instructions">${instructions.value}</div>
|
|
<button class="screenshots-button ghost-button" id="screenshots-cancel-button" title="${cancelAttributes.attributes[0].value}" aria-label="${cancelAttributes.attributes[1].value}">${cancelLabel.value}</button>
|
|
</div>
|
|
<div id="hover-highlight" hidden></div>
|
|
<div id="selection-container" hidden>
|
|
<div id="top-background" class="bghighlight"></div>
|
|
<div id="bottom-background" class="bghighlight"></div>
|
|
<div id="left-background" class="bghighlight"></div>
|
|
<div id="right-background" class="bghighlight"></div>
|
|
<div id="highlight" class="highlight" tabindex="0">
|
|
<div id="mover-topLeft" class="mover-target direction-topLeft" tabindex="0">
|
|
<div class="mover"></div>
|
|
</div>
|
|
<div id="mover-top" class="mover-target direction-top">
|
|
<div class="mover"></div>
|
|
</div>
|
|
<div id="mover-topRight" class="mover-target direction-topRight" tabindex="0">
|
|
<div class="mover"></div>
|
|
</div>
|
|
<div id="mover-right" class="mover-target direction-right">
|
|
<div class="mover"></div>
|
|
</div>
|
|
<div id="mover-bottomRight" class="mover-target direction-bottomRight" tabindex="0">
|
|
<div class="mover"></div>
|
|
</div>
|
|
<div id="mover-bottom" class="mover-target direction-bottom">
|
|
<div class="mover"></div>
|
|
</div>
|
|
<div id="mover-bottomLeft" class="mover-target direction-bottomLeft" tabindex="0">
|
|
<div class="mover"></div>
|
|
</div>
|
|
<div id="mover-left" class="mover-target direction-left">
|
|
<div class="mover"></div>
|
|
</div>
|
|
<div id="selection-size-container">
|
|
<span id="selection-size" dir="ltr"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="buttons-container" hidden>
|
|
<div class="buttons-wrapper">
|
|
<button id="cancel" class="screenshots-button" title="${cancelAttributes.attributes[0].value}" aria-label="${cancelAttributes.attributes[1].value}"><img/></button>
|
|
<button id="copy" class="screenshots-button" title="${copyAttributes.attributes[0].value}" aria-label="${copyAttributes.attributes[1].value}"><img/><label>${copyAttributes.value}</label></button>
|
|
<button id="download" class="screenshots-button primary" title="${downloadAttributes.attributes[0].value}" aria-label="${downloadAttributes.attributes[1].value}"><img/><label>${downloadAttributes.value}</label></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>`;
|
|
}
|
|
|
|
get fragment() {
|
|
if (!this.overlayTemplate) {
|
|
let parser = new DOMParser();
|
|
let doc = parser.parseFromString(this.markup, "text/html");
|
|
this.overlayTemplate = this.document.importNode(
|
|
doc.querySelector("template"),
|
|
true
|
|
);
|
|
}
|
|
let fragment = this.overlayTemplate.content.cloneNode(true);
|
|
return fragment;
|
|
}
|
|
|
|
get initialized() {
|
|
return this.#initialized;
|
|
}
|
|
|
|
get state() {
|
|
return this.#state;
|
|
}
|
|
|
|
get methodsUsed() {
|
|
return this.#methodsUsed;
|
|
}
|
|
|
|
constructor(contentDocument) {
|
|
this.document = contentDocument;
|
|
this.window = contentDocument.ownerGlobal;
|
|
|
|
this.windowDimensions = new WindowDimensions();
|
|
this.selectionRegion = new Region(this.windowDimensions);
|
|
this.hoverElementRegion = new Region(this.windowDimensions);
|
|
this.resetMethodsUsed();
|
|
|
|
let [downloadKey, copyKey] = lazy.overlayLocalization.formatMessagesSync([
|
|
{ id: "screenshots-component-download-key" },
|
|
{ id: "screenshots-component-copy-key" },
|
|
]);
|
|
|
|
this.downloadKey = downloadKey.value;
|
|
this.copyKey = copyKey.value;
|
|
}
|
|
|
|
get content() {
|
|
if (!this.#content || Cu.isDeadWrapper(this.#content)) {
|
|
return null;
|
|
}
|
|
return this.#content;
|
|
}
|
|
|
|
getElementById(id) {
|
|
return this.content.root.getElementById(id);
|
|
}
|
|
|
|
async initialize() {
|
|
if (this.initialized) {
|
|
return;
|
|
}
|
|
|
|
this.windowDimensions.reset();
|
|
|
|
this.#content = this.document.insertAnonymousContent();
|
|
this.#content.root.appendChild(this.fragment);
|
|
|
|
this.initializeElements();
|
|
this.screenshotsContainer.dir = Services.locale.isAppLocaleRTL
|
|
? "rtl"
|
|
: "ltr";
|
|
await this.updateWindowDimensions();
|
|
|
|
this.#setState(STATES.CROSSHAIRS);
|
|
|
|
this.selection = this.window.getSelection();
|
|
this.ranges = [];
|
|
for (let i = 0; i < this.selection.rangeCount; i++) {
|
|
this.ranges.push(this.selection.getRangeAt(i));
|
|
}
|
|
|
|
this.#initialized = true;
|
|
}
|
|
|
|
/**
|
|
* Get all the elements that will be used.
|
|
*/
|
|
initializeElements() {
|
|
this.previewCancelButton = this.getElementById("screenshots-cancel-button");
|
|
this.cancelButton = this.getElementById("cancel");
|
|
this.copyButton = this.getElementById("copy");
|
|
this.downloadButton = this.getElementById("download");
|
|
|
|
this.previewContainer = this.getElementById("preview-container");
|
|
this.previewFace = this.getElementById("face-container");
|
|
this.hoverElementContainer = this.getElementById("hover-highlight");
|
|
this.selectionContainer = this.getElementById("selection-container");
|
|
this.buttonsContainer = this.getElementById("buttons-container");
|
|
this.screenshotsContainer = this.getElementById("screenshots-component");
|
|
|
|
this.leftEye = this.getElementById("left-eye");
|
|
this.rightEye = this.getElementById("right-eye");
|
|
|
|
this.leftBackgroundEl = this.getElementById("left-background");
|
|
this.topBackgroundEl = this.getElementById("top-background");
|
|
this.rightBackgroundEl = this.getElementById("right-background");
|
|
this.bottomBackgroundEl = this.getElementById("bottom-background");
|
|
this.highlightEl = this.getElementById("highlight");
|
|
|
|
this.topLeftMover = this.getElementById("mover-topLeft");
|
|
this.topRightMover = this.getElementById("mover-topRight");
|
|
this.bottomLeftMover = this.getElementById("mover-bottomLeft");
|
|
this.bottomRightMover = this.getElementById("mover-bottomRight");
|
|
|
|
this.selectionSize = this.getElementById("selection-size");
|
|
}
|
|
|
|
/**
|
|
* Removes all event listeners and removes the overlay from the Anonymous Content
|
|
*/
|
|
tearDown(options = {}) {
|
|
if (this.#content) {
|
|
if (!(options.doNotResetMethods === true)) {
|
|
this.resetMethodsUsed();
|
|
}
|
|
try {
|
|
this.document.removeAnonymousContent(this.#content);
|
|
} catch (e) {
|
|
// If the current window isn't the one the content was inserted into, this
|
|
// will fail, but that's fine.
|
|
}
|
|
}
|
|
this.#initialized = false;
|
|
this.#setState("");
|
|
}
|
|
|
|
resetMethodsUsed() {
|
|
this.#methodsUsed = {
|
|
element: 0,
|
|
region: 0,
|
|
move: 0,
|
|
resize: 0,
|
|
};
|
|
}
|
|
|
|
focus(direction) {
|
|
if (direction === "backward") {
|
|
this.previewCancelButton.focus({ focusVisible: true });
|
|
} else {
|
|
this.previewFace.focus({ focusVisible: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the x and y coordinates of the event relative to both the
|
|
* viewport and the page.
|
|
* @param {Event} event The event
|
|
* @returns
|
|
* {
|
|
* clientX: The x position relative to the viewport
|
|
* clientY: The y position relative to the viewport
|
|
* pageX: The x position relative to the entire page
|
|
* pageY: The y position relative to the entire page
|
|
* }
|
|
*/
|
|
getCoordinatesFromEvent(event) {
|
|
let { clientX, clientY, pageX, pageY } = event;
|
|
pageX -= this.windowDimensions.scrollMinX;
|
|
pageY -= this.windowDimensions.scrollMinY;
|
|
|
|
return { clientX, clientY, pageX, pageY };
|
|
}
|
|
|
|
handleEvent(event) {
|
|
switch (event.type) {
|
|
case "click":
|
|
this.handleClick(event);
|
|
break;
|
|
case "pointerdown":
|
|
this.handlePointerDown(event);
|
|
break;
|
|
case "pointermove":
|
|
this.handlePointerMove(event);
|
|
break;
|
|
case "pointerup":
|
|
this.handlePointerUp(event);
|
|
break;
|
|
case "keydown":
|
|
this.handleKeyDown(event);
|
|
break;
|
|
case "keyup":
|
|
this.handleKeyUp(event);
|
|
break;
|
|
case "selectionchange":
|
|
this.handleSelectionChange();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If the event came from the primary button, return false as we should not
|
|
* early return in the event handler function.
|
|
* If the event had another button, set to the crosshairs or selected state
|
|
* and return true to early return from the event handler function.
|
|
* @param {PointerEvent} event
|
|
* @returns true if the event button(s) was the non primary button
|
|
* false otherwise
|
|
*/
|
|
preEventHandler(event) {
|
|
if (event.button > 0 || event.buttons > 1) {
|
|
switch (this.#state) {
|
|
case STATES.DRAGGING_READY:
|
|
this.#setState(STATES.CROSSHAIRS);
|
|
break;
|
|
case STATES.DRAGGING:
|
|
case STATES.RESIZING:
|
|
this.#setState(STATES.SELECTED);
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
handleClick(event) {
|
|
if (this.preEventHandler(event)) {
|
|
return;
|
|
}
|
|
|
|
switch (event.originalTarget.id) {
|
|
case "screenshots-cancel-button":
|
|
case "cancel":
|
|
this.maybeCancelScreenshots();
|
|
break;
|
|
case "copy":
|
|
this.copySelectedRegion();
|
|
break;
|
|
case "download":
|
|
this.downloadSelectedRegion();
|
|
break;
|
|
}
|
|
}
|
|
|
|
maybeCancelScreenshots() {
|
|
if (this.#state === STATES.CROSSHAIRS) {
|
|
this.#dispatchEvent("Screenshots:Close", {
|
|
reason: "OverlayCancel",
|
|
});
|
|
} else {
|
|
this.#setState(STATES.CROSSHAIRS);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles the pointerdown event depending on the state.
|
|
* Early return when a pointer down happens on a button.
|
|
* @param {Event} event The pointerown event
|
|
*/
|
|
handlePointerDown(event) {
|
|
// Early return if the event target is not within the screenshots component
|
|
// element.
|
|
if (!event.originalTarget.closest("#screenshots-component")) {
|
|
return;
|
|
}
|
|
|
|
if (this.preEventHandler(event)) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
event.originalTarget.id === "screenshots-cancel-button" ||
|
|
event.originalTarget.closest("#buttons-container") ===
|
|
this.buttonsContainer
|
|
) {
|
|
event.stopPropagation();
|
|
return;
|
|
}
|
|
|
|
const { pageX, pageY } = this.getCoordinatesFromEvent(event);
|
|
|
|
switch (this.#state) {
|
|
case STATES.CROSSHAIRS: {
|
|
this.crosshairsDragStart(pageX, pageY);
|
|
break;
|
|
}
|
|
case STATES.SELECTED: {
|
|
this.selectedDragStart(pageX, pageY, event.originalTarget.id);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles the pointermove event depending on the state
|
|
* @param {Event} event The pointermove event
|
|
*/
|
|
handlePointerMove(event) {
|
|
if (this.preEventHandler(event)) {
|
|
return;
|
|
}
|
|
|
|
const { pageX, pageY, clientX, clientY } =
|
|
this.getCoordinatesFromEvent(event);
|
|
|
|
switch (this.#state) {
|
|
case STATES.CROSSHAIRS: {
|
|
this.crosshairsMove(clientX, clientY);
|
|
break;
|
|
}
|
|
case STATES.DRAGGING_READY: {
|
|
this.draggingReadyDrag(pageX, pageY);
|
|
break;
|
|
}
|
|
case STATES.DRAGGING: {
|
|
this.draggingDrag(pageX, pageY);
|
|
break;
|
|
}
|
|
case STATES.RESIZING: {
|
|
this.resizingDrag(pageX, pageY);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles the pointerup event depending on the state
|
|
* @param {Event} event The pointerup event
|
|
*/
|
|
handlePointerUp(event) {
|
|
const { pageX, pageY, clientX, clientY } =
|
|
this.getCoordinatesFromEvent(event);
|
|
|
|
switch (this.#state) {
|
|
case STATES.DRAGGING_READY: {
|
|
this.draggingReadyDragEnd(pageX - clientX, pageY - clientY);
|
|
break;
|
|
}
|
|
case STATES.DRAGGING: {
|
|
this.draggingDragEnd(pageX, pageY, event.originalTarget.id);
|
|
break;
|
|
}
|
|
case STATES.RESIZING: {
|
|
this.resizingDragEnd(pageX, pageY);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles when a keydown occurs in the screenshots component.
|
|
* @param {Event} event The keydown event
|
|
*/
|
|
handleKeyDown(event) {
|
|
if (event.key === "Escape") {
|
|
this.maybeCancelScreenshots();
|
|
return;
|
|
}
|
|
|
|
switch (this.#state) {
|
|
case STATES.CROSSHAIRS:
|
|
this.crosshairsKeyDown(event);
|
|
break;
|
|
case STATES.DRAGGING:
|
|
this.draggingKeyDown(event);
|
|
break;
|
|
case STATES.RESIZING:
|
|
this.resizingKeyDown(event);
|
|
break;
|
|
case STATES.SELECTED:
|
|
this.selectedKeyDown(event);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles when a keyup occurs in the screenshots component.
|
|
* All we need to do on keyup is set the state to selected.
|
|
* @param {Event} event The keydown event
|
|
*/
|
|
handleKeyUp(event) {
|
|
switch (this.#state) {
|
|
case STATES.RESIZING:
|
|
switch (event.key) {
|
|
case "ArrowLeft":
|
|
case "ArrowUp":
|
|
case "ArrowRight":
|
|
case "ArrowDown":
|
|
switch (event.originalTarget.id) {
|
|
case "highlight":
|
|
case "mover-bottomLeft":
|
|
case "mover-bottomRight":
|
|
case "mover-topLeft":
|
|
case "mover-topRight":
|
|
this.#setState(STATES.SELECTED, { doNotMoveFocus: true });
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the accel key depending on the platform.
|
|
* metaKey for macOS. ctrlKey for Windows and Linux.
|
|
* @param {Event} event The keydown event
|
|
* @returns {Boolean} True if the accel key is pressed, false otherwise.
|
|
*/
|
|
getAccelKey(event) {
|
|
if (AppConstants.platform === "macosx") {
|
|
return event.metaKey;
|
|
}
|
|
return event.ctrlKey;
|
|
}
|
|
|
|
crosshairsKeyDown(event) {
|
|
switch (event.key) {
|
|
case "ArrowLeft":
|
|
case "ArrowUp":
|
|
case "ArrowRight":
|
|
case "ArrowDown":
|
|
// Do nothing so we can prevent default below
|
|
break;
|
|
case "Tab":
|
|
this.maybeLockFocus(event);
|
|
return;
|
|
case "Enter":
|
|
if (this.handleKeyDownOnButton(event)) {
|
|
return;
|
|
}
|
|
|
|
if (this.hoverElementRegion.isRegionValid) {
|
|
this.draggingReadyStart();
|
|
this.draggingReadyDragEnd();
|
|
return;
|
|
}
|
|
// eslint-disable-next-line no-fallthrough
|
|
case " ": {
|
|
if (this.handleKeyDownOnButton(event)) {
|
|
return;
|
|
}
|
|
|
|
if (Services.appinfo.isWayland) {
|
|
return;
|
|
}
|
|
|
|
// If the preview face is focused, create a region from the preview
|
|
// face and move focus to the bottom right mover for adjustments
|
|
if (Services.focus.focusedElement === this.previewFace) {
|
|
let rect = this.previewFace.getBoundingClientRect();
|
|
this.hoverElementRegion.dimensions = rect;
|
|
this.draggingReadyStart();
|
|
this.draggingReadyDragEnd({ doNotMoveFocus: true });
|
|
this.bottomRightMover.focus({ focusVisible: true });
|
|
return;
|
|
}
|
|
|
|
if (Services.appinfo.isWayland) {
|
|
return;
|
|
}
|
|
|
|
// The left and top coordinates from cursorRegion are relative to
|
|
// the client window so we need to add the scroll offset of the page to
|
|
// get the correct coordinates.
|
|
let x = {};
|
|
let y = {};
|
|
this.window.windowUtils.getLastOverWindowPointerLocationInCSSPixels(
|
|
x,
|
|
y
|
|
);
|
|
this.crosshairsDragStart(
|
|
x.value + this.windowDimensions.scrollX,
|
|
y.value + this.windowDimensions.scrollY
|
|
);
|
|
this.#setState(STATES.DRAGGING);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles a keydown event for the dragging state.
|
|
* @param {Event} event The keydown event
|
|
*/
|
|
draggingKeyDown(event) {
|
|
switch (event.key) {
|
|
case "ArrowLeft":
|
|
this.handleArrowLeftKeyDown(event);
|
|
break;
|
|
case "ArrowUp":
|
|
this.handleArrowUpKeyDown(event);
|
|
break;
|
|
case "ArrowRight":
|
|
this.handleArrowRightKeyDown(event);
|
|
break;
|
|
case "ArrowDown":
|
|
this.handleArrowDownKeyDown(event);
|
|
break;
|
|
case "Enter":
|
|
case " ":
|
|
this.#setState(STATES.SELECTED);
|
|
return;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
this.drawSelectionContainer();
|
|
}
|
|
|
|
/**
|
|
* Handles a keydown event for the resizing state.
|
|
* @param {Event} event The keydown event
|
|
*/
|
|
resizingKeyDown(event) {
|
|
switch (event.key) {
|
|
case "ArrowLeft":
|
|
this.resizingArrowLeftKeyDown(event);
|
|
break;
|
|
case "ArrowUp":
|
|
this.resizingArrowUpKeyDown(event);
|
|
break;
|
|
case "ArrowRight":
|
|
this.resizingArrowRightKeyDown(event);
|
|
break;
|
|
case "ArrowDown":
|
|
this.resizingArrowDownKeyDown(event);
|
|
break;
|
|
}
|
|
}
|
|
|
|
selectedKeyDown(event) {
|
|
let isSelectionElement = event.originalTarget.closest(
|
|
"#selection-container"
|
|
);
|
|
switch (event.key) {
|
|
case "ArrowLeft":
|
|
if (isSelectionElement) {
|
|
this.resizingArrowLeftKeyDown(event);
|
|
}
|
|
break;
|
|
case "ArrowUp":
|
|
if (isSelectionElement) {
|
|
this.resizingArrowUpKeyDown(event);
|
|
}
|
|
break;
|
|
case "ArrowRight":
|
|
if (isSelectionElement) {
|
|
this.resizingArrowRightKeyDown(event);
|
|
}
|
|
break;
|
|
case "ArrowDown":
|
|
if (isSelectionElement) {
|
|
this.resizingArrowDownKeyDown(event);
|
|
}
|
|
break;
|
|
case "Tab":
|
|
this.maybeLockFocus(event);
|
|
break;
|
|
case "Enter":
|
|
case " ":
|
|
this.handleKeyDownOnButton(event);
|
|
break;
|
|
case this.copyKey.toLowerCase():
|
|
if (this.state === "selected" && this.getAccelKey(event)) {
|
|
this.copySelectedRegion();
|
|
}
|
|
break;
|
|
case this.downloadKey.toLowerCase():
|
|
if (this.state === "selected" && this.getAccelKey(event)) {
|
|
this.downloadSelectedRegion();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move the region or its left or right side to the left.
|
|
* Just the arrow key will move the region by 1px.
|
|
* Arrow key + shift will move the region by 10px.
|
|
* Arrow key + control/meta will move to the edge of the window.
|
|
* @param {Event} event The keydown event
|
|
*/
|
|
resizingArrowLeftKeyDown(event) {
|
|
this.handleArrowLeftKeyDown(event);
|
|
|
|
if (this.#state !== STATES.RESIZING) {
|
|
this.#setState(STATES.RESIZING);
|
|
}
|
|
|
|
this.drawSelectionContainer();
|
|
}
|
|
|
|
/**
|
|
* Move the region or its left or right side to the left.
|
|
* Just the arrow key will move the region by 1px.
|
|
* Arrow key + shift will move the region by 10px.
|
|
* Arrow key + control/meta will move to the edge of the window.
|
|
* @param {Event} event The keydown event
|
|
*/
|
|
handleArrowLeftKeyDown(event) {
|
|
let exponent = event.shiftKey ? 1 : 0;
|
|
switch (event.originalTarget.id) {
|
|
case "highlight":
|
|
if (this.getAccelKey(event)) {
|
|
let width = this.selectionRegion.width;
|
|
this.selectionRegion.left = this.windowDimensions.scrollX;
|
|
this.selectionRegion.right = this.windowDimensions.scrollX + width;
|
|
break;
|
|
}
|
|
|
|
this.selectionRegion.right -= 10 ** exponent;
|
|
// eslint-disable-next-line no-fallthrough
|
|
case "mover-topLeft":
|
|
case "mover-bottomLeft":
|
|
if (this.getAccelKey(event)) {
|
|
this.selectionRegion.left = this.windowDimensions.scrollX;
|
|
break;
|
|
}
|
|
|
|
this.selectionRegion.left -= 10 ** exponent;
|
|
this.scrollIfByEdge(
|
|
this.selectionRegion.left,
|
|
this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2
|
|
);
|
|
break;
|
|
case "mover-topRight":
|
|
case "mover-bottomRight":
|
|
if (this.getAccelKey(event)) {
|
|
let left = this.selectionRegion.left;
|
|
this.selectionRegion.left = this.windowDimensions.scrollX;
|
|
this.selectionRegion.right = left;
|
|
if (event.originalTarget.id === "mover-topRight") {
|
|
this.topLeftMover.focus({ focusVisible: true });
|
|
} else if (event.originalTarget.id === "mover-bottomRight") {
|
|
this.bottomLeftMover.focus({ focusVisible: true });
|
|
}
|
|
break;
|
|
}
|
|
|
|
this.selectionRegion.right -= 10 ** exponent;
|
|
if (this.selectionRegion.x1 >= this.selectionRegion.x2) {
|
|
this.selectionRegion.sortCoords();
|
|
if (event.originalTarget.id === "mover-topRight") {
|
|
this.topLeftMover.focus({ focusVisible: true });
|
|
} else if (event.originalTarget.id === "mover-bottomRight") {
|
|
this.bottomLeftMover.focus({ focusVisible: true });
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move the region or its top or bottom side upward.
|
|
* Just the arrow key will move the region by 1px.
|
|
* Arrow key + shift will move the region by 10px.
|
|
* Arrow key + control/meta will move to the edge of the window.
|
|
* @param {Event} event The keydown event
|
|
*/
|
|
resizingArrowUpKeyDown(event) {
|
|
this.handleArrowUpKeyDown(event);
|
|
|
|
if (this.#state !== STATES.RESIZING) {
|
|
this.#setState(STATES.RESIZING);
|
|
}
|
|
|
|
this.drawSelectionContainer();
|
|
}
|
|
|
|
/**
|
|
* Move the region or its top or bottom side upward.
|
|
* Just the arrow key will move the region by 1px.
|
|
* Arrow key + shift will move the region by 10px.
|
|
* Arrow key + control/meta will move to the edge of the window.
|
|
* @param {Event} event The keydown event
|
|
*/
|
|
handleArrowUpKeyDown(event) {
|
|
let exponent = event.shiftKey ? 1 : 0;
|
|
switch (event.originalTarget.id) {
|
|
case "highlight":
|
|
if (this.getAccelKey(event)) {
|
|
let height = this.selectionRegion.height;
|
|
this.selectionRegion.top = this.windowDimensions.scrollY;
|
|
this.selectionRegion.bottom = this.windowDimensions.scrollY + height;
|
|
break;
|
|
}
|
|
|
|
this.selectionRegion.bottom -= 10 ** exponent;
|
|
// eslint-disable-next-line no-fallthrough
|
|
case "mover-topLeft":
|
|
case "mover-topRight":
|
|
if (this.getAccelKey(event)) {
|
|
this.selectionRegion.top = this.windowDimensions.scrollY;
|
|
break;
|
|
}
|
|
|
|
this.selectionRegion.top -= 10 ** exponent;
|
|
this.scrollIfByEdge(
|
|
this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2,
|
|
this.selectionRegion.top
|
|
);
|
|
break;
|
|
case "mover-bottomLeft":
|
|
case "mover-bottomRight":
|
|
if (this.getAccelKey(event)) {
|
|
let top = this.selectionRegion.top;
|
|
this.selectionRegion.top = this.windowDimensions.scrollY;
|
|
this.selectionRegion.bottom = top;
|
|
if (event.originalTarget.id === "mover-bottomLeft") {
|
|
this.topLeftMover.focus({ focusVisible: true });
|
|
} else if (event.originalTarget.id === "mover-bottomRight") {
|
|
this.topRightMover.focus({ focusVisible: true });
|
|
}
|
|
break;
|
|
}
|
|
|
|
this.selectionRegion.bottom -= 10 ** exponent;
|
|
if (this.selectionRegion.y1 >= this.selectionRegion.y2) {
|
|
this.selectionRegion.sortCoords();
|
|
if (event.originalTarget.id === "mover-bottomLeft") {
|
|
this.topLeftMover.focus({ focusVisible: true });
|
|
} else if (event.originalTarget.id === "mover-bottomRight") {
|
|
this.topRightMover.focus({ focusVisible: true });
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move the region or its left or right side to the right.
|
|
* Just the arrow key will move the region by 1px.
|
|
* Arrow key + shift will move the region by 10px.
|
|
* Arrow key + control/meta will move to the edge of the window.
|
|
* @param {Event} event The keydown event
|
|
*/
|
|
resizingArrowRightKeyDown(event) {
|
|
this.handleArrowRightKeyDown(event);
|
|
|
|
if (this.#state !== STATES.RESIZING) {
|
|
this.#setState(STATES.RESIZING);
|
|
}
|
|
|
|
this.drawSelectionContainer();
|
|
}
|
|
|
|
/**
|
|
* Move the region or its left or right side to the right.
|
|
* Just the arrow key will move the region by 1px.
|
|
* Arrow key + shift will move the region by 10px.
|
|
* Arrow key + control/meta will move to the edge of the window.
|
|
* @param {Event} event The keydown event
|
|
*/
|
|
handleArrowRightKeyDown(event) {
|
|
let exponent = event.shiftKey ? 1 : 0;
|
|
switch (event.originalTarget.id) {
|
|
case "highlight":
|
|
if (this.getAccelKey(event)) {
|
|
let width = this.selectionRegion.width;
|
|
let { scrollX, clientWidth } = this.windowDimensions.dimensions;
|
|
this.selectionRegion.right = scrollX + clientWidth;
|
|
this.selectionRegion.left = this.selectionRegion.right - width;
|
|
break;
|
|
}
|
|
|
|
this.selectionRegion.left += 10 ** exponent;
|
|
// eslint-disable-next-line no-fallthrough
|
|
case "mover-topRight":
|
|
case "mover-bottomRight":
|
|
if (this.getAccelKey(event)) {
|
|
this.selectionRegion.right =
|
|
this.windowDimensions.scrollX + this.windowDimensions.clientWidth;
|
|
break;
|
|
}
|
|
|
|
this.selectionRegion.right += 10 ** exponent;
|
|
this.scrollIfByEdge(
|
|
this.selectionRegion.right,
|
|
this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2
|
|
);
|
|
break;
|
|
case "mover-topLeft":
|
|
case "mover-bottomLeft":
|
|
if (this.getAccelKey(event)) {
|
|
let right = this.selectionRegion.right;
|
|
this.selectionRegion.right =
|
|
this.windowDimensions.scrollX + this.windowDimensions.clientWidth;
|
|
this.selectionRegion.left = right;
|
|
if (event.originalTarget.id === "mover-topLeft") {
|
|
this.topRightMover.focus({ focusVisible: true });
|
|
} else if (event.originalTarget.id === "mover-bottomLeft") {
|
|
this.bottomRightMover.focus({ focusVisible: true });
|
|
}
|
|
break;
|
|
}
|
|
|
|
this.selectionRegion.left += 10 ** exponent;
|
|
if (this.selectionRegion.x1 >= this.selectionRegion.x2) {
|
|
this.selectionRegion.sortCoords();
|
|
if (event.originalTarget.id === "mover-topLeft") {
|
|
this.topRightMover.focus({ focusVisible: true });
|
|
} else if (event.originalTarget.id === "mover-bottomLeft") {
|
|
this.bottomRightMover.focus({ focusVisible: true });
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move the region or its top or bottom side downward.
|
|
* Just the arrow key will move the region by 1px.
|
|
* Arrow key + shift will move the region by 10px.
|
|
* Arrow key + control/meta will move to the edge of the window.
|
|
* @param {Event} event The keydown event
|
|
*/
|
|
resizingArrowDownKeyDown(event) {
|
|
this.handleArrowDownKeyDown(event);
|
|
|
|
if (this.#state !== STATES.RESIZING) {
|
|
this.#setState(STATES.RESIZING);
|
|
}
|
|
|
|
this.drawSelectionContainer();
|
|
}
|
|
|
|
handleArrowDownKeyDown(event) {
|
|
let exponent = event.shiftKey ? 1 : 0;
|
|
switch (event.originalTarget.id) {
|
|
case "highlight":
|
|
if (this.getAccelKey(event)) {
|
|
let height = this.selectionRegion.height;
|
|
let { scrollY, clientHeight } = this.windowDimensions.dimensions;
|
|
this.selectionRegion.bottom = scrollY + clientHeight;
|
|
this.selectionRegion.top = this.selectionRegion.bottom - height;
|
|
break;
|
|
}
|
|
|
|
this.selectionRegion.top += 10 ** exponent;
|
|
// eslint-disable-next-line no-fallthrough
|
|
case "mover-bottomLeft":
|
|
case "mover-bottomRight":
|
|
if (this.getAccelKey(event)) {
|
|
this.selectionRegion.bottom =
|
|
this.windowDimensions.scrollY + this.windowDimensions.clientHeight;
|
|
break;
|
|
}
|
|
|
|
this.selectionRegion.bottom += 10 ** exponent;
|
|
this.scrollIfByEdge(
|
|
this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2,
|
|
this.selectionRegion.bottom
|
|
);
|
|
break;
|
|
case "mover-topLeft":
|
|
case "mover-topRight":
|
|
if (this.getAccelKey(event)) {
|
|
let bottom = this.selectionRegion.bottom;
|
|
this.selectionRegion.bottom =
|
|
this.windowDimensions.scrollY + this.windowDimensions.clientHeight;
|
|
this.selectionRegion.top = bottom;
|
|
if (event.originalTarget.id === "mover-topLeft") {
|
|
this.bottomLeftMover.focus({ focusVisible: true });
|
|
} else if (event.originalTarget.id === "mover-topRight") {
|
|
this.bottomRightMover.focus({ focusVisible: true });
|
|
}
|
|
break;
|
|
}
|
|
|
|
this.selectionRegion.top += 10 ** exponent;
|
|
if (this.selectionRegion.y1 >= this.selectionRegion.y2) {
|
|
this.selectionRegion.sortCoords();
|
|
if (event.originalTarget.id === "mover-topLeft") {
|
|
this.bottomLeftMover.focus({ focusVisible: true });
|
|
} else if (event.originalTarget.id === "mover-topRight") {
|
|
this.bottomRightMover.focus({ focusVisible: true });
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* We lock focus to the overlay when a region is selected.
|
|
* Can still escape with shift + F6.
|
|
* @param {Event} event The keydown event
|
|
*/
|
|
maybeLockFocus(event) {
|
|
switch (this.#state) {
|
|
case STATES.CROSSHAIRS:
|
|
if (event.originalTarget === this.previewCancelButton) {
|
|
if (event.shiftKey) {
|
|
this.previewFace.focus({ focusVisible: true });
|
|
} else {
|
|
this.#dispatchEvent("Screenshots:FocusPanel", {
|
|
direction: "forward",
|
|
});
|
|
}
|
|
} else if (event.originalTarget === this.previewFace) {
|
|
if (event.shiftKey) {
|
|
this.#dispatchEvent("Screenshots:FocusPanel", {
|
|
direction: "backward",
|
|
});
|
|
} else {
|
|
this.previewCancelButton.focus({ focusVisible: true });
|
|
}
|
|
}
|
|
break;
|
|
case STATES.SELECTED:
|
|
if (event.originalTarget.id === "highlight" && event.shiftKey) {
|
|
this.downloadButton.focus({ focusVisible: true });
|
|
} else if (event.originalTarget.id === "download" && !event.shiftKey) {
|
|
this.highlightEl.focus({ focusVisible: true });
|
|
} else {
|
|
// The content document can listen for keydown events and prevent moving
|
|
// focus so we manually move focus to the next element here.
|
|
let direction = event.shiftKey
|
|
? Services.focus.MOVEFOCUS_BACKWARD
|
|
: Services.focus.MOVEFOCUS_FORWARD;
|
|
Services.focus.moveFocus(
|
|
this.window,
|
|
null,
|
|
direction,
|
|
Services.focus.FLAG_BYKEY
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the focus to the most recent saved method.
|
|
* This will default to the download button.
|
|
*/
|
|
setFocusToActionButton() {
|
|
if (lazy.SCREENSHOTS_LAST_SAVED_METHOD === "copy") {
|
|
this.copyButton.focus({ focusVisible: true, preventScroll: true });
|
|
} else {
|
|
this.downloadButton.focus({ focusVisible: true, preventScroll: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* We prevent all events in ScreenshotsComponentChild so we need to
|
|
* explicitly handle keydown events on buttons here.
|
|
*
|
|
* @param {KeyEvent} event The keydown event
|
|
*
|
|
* @returns {Boolean} True if the event was handled here, otherwise false.
|
|
*/
|
|
handleKeyDownOnButton(event) {
|
|
switch (event.originalTarget) {
|
|
case this.cancelButton:
|
|
case this.previewCancelButton:
|
|
this.maybeCancelScreenshots();
|
|
break;
|
|
case this.copyButton:
|
|
this.copySelectedRegion();
|
|
break;
|
|
case this.downloadButton:
|
|
this.downloadSelectedRegion();
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* All of the selection ranges were recorded at initialization. The ranges
|
|
* are removed when focus is set to the buttons so we add the selection
|
|
* ranges back so a selected region can be captured.
|
|
*/
|
|
handleSelectionChange() {
|
|
if (this.ranges.length) {
|
|
for (let range of this.ranges) {
|
|
this.selection.addRange(range);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dispatch a custom event to the ScreenshotsComponentChild actor
|
|
* @param {String} eventType The name of the event
|
|
* @param {object} detail Extra details to send to the child actor
|
|
*/
|
|
#dispatchEvent(eventType, detail) {
|
|
this.window.windowUtils.dispatchEventToChromeOnly(
|
|
this.window,
|
|
new CustomEvent(eventType, {
|
|
bubbles: true,
|
|
detail,
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Set a new state for the overlay
|
|
* @param {String} newState
|
|
* @param {Object} options (optional) Options for calling start of state method
|
|
*/
|
|
#setState(newState, options = {}) {
|
|
if (this.#state === STATES.SELECTED && newState === STATES.CROSSHAIRS) {
|
|
this.#dispatchEvent("Screenshots:RecordEvent", {
|
|
eventName: "startedOverlayRetry",
|
|
});
|
|
}
|
|
if (newState !== this.#state) {
|
|
this.#dispatchEvent("Screenshots:OverlaySelection", {
|
|
hasSelection: [
|
|
STATES.DRAGGING_READY,
|
|
STATES.DRAGGING,
|
|
STATES.RESIZING,
|
|
STATES.SELECTED,
|
|
].includes(newState),
|
|
overlayState: newState,
|
|
});
|
|
}
|
|
this.#state = newState;
|
|
|
|
switch (this.#state) {
|
|
case STATES.CROSSHAIRS: {
|
|
this.crosshairsStart();
|
|
break;
|
|
}
|
|
case STATES.DRAGGING_READY: {
|
|
this.draggingReadyStart();
|
|
break;
|
|
}
|
|
case STATES.DRAGGING: {
|
|
this.draggingStart();
|
|
break;
|
|
}
|
|
case STATES.SELECTED: {
|
|
this.selectedStart(options);
|
|
break;
|
|
}
|
|
case STATES.RESIZING: {
|
|
this.resizingStart();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
copySelectedRegion() {
|
|
this.#dispatchEvent("Screenshots:Copy", {
|
|
region: this.selectionRegion.dimensions,
|
|
});
|
|
}
|
|
|
|
downloadSelectedRegion() {
|
|
this.#dispatchEvent("Screenshots:Download", {
|
|
region: this.selectionRegion.dimensions,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hide hover element, selection and buttons containers.
|
|
* Show the preview container and the panel.
|
|
* This is the initial state of the overlay.
|
|
*/
|
|
crosshairsStart() {
|
|
this.hideHoverElementContainer();
|
|
this.hideSelectionContainer();
|
|
this.hideButtonsContainer();
|
|
this.showPreviewContainer();
|
|
this.#dispatchEvent("Screenshots:ShowPanel");
|
|
this.#previousDimensions = null;
|
|
this.#cachedEle = null;
|
|
this.hoverElementRegion.resetDimensions();
|
|
}
|
|
|
|
/**
|
|
* Hide the panel because we have started dragging.
|
|
*/
|
|
draggingReadyStart() {
|
|
this.#dispatchEvent("Screenshots:HidePanel");
|
|
}
|
|
|
|
/**
|
|
* Hide the preview, hover element and buttons containers.
|
|
* Show the selection container.
|
|
*/
|
|
draggingStart() {
|
|
this.hidePreviewContainer();
|
|
this.hideButtonsContainer();
|
|
this.hideHoverElementContainer();
|
|
this.drawSelectionContainer();
|
|
}
|
|
|
|
/**
|
|
* Hide the preview and hover element containers.
|
|
* Draw the selection and buttons containers.
|
|
*
|
|
* @param {object} [options={}] (optional)
|
|
* @param {boolean} doNotMoveFocus True if focus should not be moved to an action button
|
|
*/
|
|
selectedStart(options = {}) {
|
|
this.selectionRegion.sortCoords();
|
|
this.hidePreviewContainer();
|
|
this.hideHoverElementContainer();
|
|
this.drawSelectionContainer();
|
|
this.drawButtonsContainer();
|
|
|
|
if (!options.doNotMoveFocus) {
|
|
this.setFocusToActionButton();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide the buttons container.
|
|
* Store the width and height of the current selected region.
|
|
* The dimensions will be used when moving the region along the edge of the
|
|
* page and for recording telemetry.
|
|
*/
|
|
resizingStart() {
|
|
this.hideButtonsContainer();
|
|
let { width, height } = this.selectionRegion.dimensions;
|
|
this.#previousDimensions = { width, height };
|
|
}
|
|
|
|
/**
|
|
* Dragging has started so we set the initial selection region and set the
|
|
* state to draggingReady.
|
|
* @param {Number} pageX The x position relative to the page
|
|
* @param {Number} pageY The y position relative to the page
|
|
*/
|
|
crosshairsDragStart(pageX, pageY) {
|
|
this.selectionRegion.dimensions = {
|
|
left: pageX,
|
|
top: pageY,
|
|
right: pageX,
|
|
bottom: pageY,
|
|
};
|
|
|
|
this.#setState(STATES.DRAGGING_READY);
|
|
}
|
|
|
|
/**
|
|
* If the background is clicked we set the state to crosshairs
|
|
* otherwise set the state to resizing
|
|
* @param {Number} pageX The x position relative to the page
|
|
* @param {Number} pageY The y position relative to the page
|
|
* @param {String} targetId The id of the event target
|
|
*/
|
|
selectedDragStart(pageX, pageY, targetId) {
|
|
if (targetId === this.screenshotsContainer.id) {
|
|
this.#setState(STATES.CROSSHAIRS);
|
|
return;
|
|
}
|
|
this.#moverId = targetId;
|
|
this.#lastPageX = pageX;
|
|
this.#lastPageY = pageY;
|
|
|
|
this.#setState(STATES.RESIZING);
|
|
}
|
|
|
|
/**
|
|
* Draw the eyes in the preview container and find the element currently
|
|
* being hovered.
|
|
* @param {Number} clientX The x position relative to the viewport
|
|
* @param {Number} clientY The y position relative to the viewport
|
|
*/
|
|
crosshairsMove(clientX, clientY) {
|
|
this.drawPreviewEyes(clientX, clientY);
|
|
|
|
this.handleElementHover(clientX, clientY);
|
|
}
|
|
|
|
/**
|
|
* Set the selection region dimensions and if the region is at least 40
|
|
* pixels diagnally in distance, set the state to dragging.
|
|
* @param {Number} pageX The x position relative to the page
|
|
* @param {Number} pageY The y position relative to the page
|
|
*/
|
|
draggingReadyDrag(pageX, pageY) {
|
|
this.selectionRegion.dimensions = {
|
|
right: pageX,
|
|
bottom: pageY,
|
|
};
|
|
|
|
if (this.selectionRegion.distance > 40) {
|
|
this.#setState(STATES.DRAGGING);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scroll if along the edge of the viewport, update the selection region
|
|
* dimensions and draw the selection container.
|
|
* @param {Number} pageX The x position relative to the page
|
|
* @param {Number} pageY The y position relative to the page
|
|
*/
|
|
draggingDrag(pageX, pageY) {
|
|
this.scrollIfByEdge(pageX, pageY);
|
|
this.selectionRegion.dimensions = {
|
|
right: pageX,
|
|
bottom: pageY,
|
|
};
|
|
|
|
this.drawSelectionContainer();
|
|
}
|
|
|
|
/**
|
|
* Resize the selection region depending on the mover that started the resize.
|
|
* @param {Number} pageX The x position relative to the page
|
|
* @param {Number} pageY The y position relative to the page
|
|
*/
|
|
resizingDrag(pageX, pageY) {
|
|
this.scrollIfByEdge(pageX, pageY);
|
|
switch (this.#moverId) {
|
|
case "mover-topLeft": {
|
|
this.selectionRegion.dimensions = {
|
|
left: pageX,
|
|
top: pageY,
|
|
};
|
|
break;
|
|
}
|
|
case "mover-top": {
|
|
this.selectionRegion.dimensions = { top: pageY };
|
|
break;
|
|
}
|
|
case "mover-topRight": {
|
|
this.selectionRegion.dimensions = {
|
|
top: pageY,
|
|
right: pageX,
|
|
};
|
|
break;
|
|
}
|
|
case "mover-right": {
|
|
this.selectionRegion.dimensions = {
|
|
right: pageX,
|
|
};
|
|
break;
|
|
}
|
|
case "mover-bottomRight": {
|
|
this.selectionRegion.dimensions = {
|
|
right: pageX,
|
|
bottom: pageY,
|
|
};
|
|
break;
|
|
}
|
|
case "mover-bottom": {
|
|
this.selectionRegion.dimensions = {
|
|
bottom: pageY,
|
|
};
|
|
break;
|
|
}
|
|
case "mover-bottomLeft": {
|
|
this.selectionRegion.dimensions = {
|
|
left: pageX,
|
|
bottom: pageY,
|
|
};
|
|
break;
|
|
}
|
|
case "mover-left": {
|
|
this.selectionRegion.dimensions = { left: pageX };
|
|
break;
|
|
}
|
|
case "highlight": {
|
|
let diffX = this.#lastPageX - pageX;
|
|
let diffY = this.#lastPageY - pageY;
|
|
|
|
let newLeft;
|
|
let newRight;
|
|
let newTop;
|
|
let newBottom;
|
|
|
|
// Unpack dimensions to use here
|
|
let {
|
|
left: boxLeft,
|
|
top: boxTop,
|
|
right: boxRight,
|
|
bottom: boxBottom,
|
|
width: boxWidth,
|
|
height: boxHeight,
|
|
} = this.selectionRegion.dimensions;
|
|
let { scrollWidth, scrollHeight } = this.windowDimensions.dimensions;
|
|
|
|
// wait until all 4 if elses have completed before setting box dimensions
|
|
if (boxWidth <= this.#previousDimensions.width && boxLeft === 0) {
|
|
newLeft = boxRight - this.#previousDimensions.width;
|
|
} else {
|
|
newLeft = boxLeft;
|
|
}
|
|
|
|
if (
|
|
boxWidth <= this.#previousDimensions.width &&
|
|
boxRight === scrollWidth
|
|
) {
|
|
newRight = boxLeft + this.#previousDimensions.width;
|
|
} else {
|
|
newRight = boxRight;
|
|
}
|
|
|
|
if (boxHeight <= this.#previousDimensions.height && boxTop === 0) {
|
|
newTop = boxBottom - this.#previousDimensions.height;
|
|
} else {
|
|
newTop = boxTop;
|
|
}
|
|
|
|
if (
|
|
boxHeight <= this.#previousDimensions.height &&
|
|
boxBottom === scrollHeight
|
|
) {
|
|
newBottom = boxTop + this.#previousDimensions.height;
|
|
} else {
|
|
newBottom = boxBottom;
|
|
}
|
|
|
|
this.selectionRegion.dimensions = {
|
|
left: newLeft - diffX,
|
|
top: newTop - diffY,
|
|
right: newRight - diffX,
|
|
bottom: newBottom - diffY,
|
|
};
|
|
|
|
this.#lastPageX = pageX;
|
|
this.#lastPageY = pageY;
|
|
break;
|
|
}
|
|
}
|
|
this.drawSelectionContainer();
|
|
}
|
|
|
|
/**
|
|
* If there is a valid element region, update and draw the selection
|
|
* container and set the state to selected.
|
|
* Otherwise set the state to crosshairs.
|
|
*
|
|
* @param {Object} options (optional) Options for passing to setState method
|
|
*/
|
|
draggingReadyDragEnd(options = {}) {
|
|
if (this.hoverElementRegion.isRegionValid) {
|
|
this.selectionRegion.dimensions = this.hoverElementRegion.dimensions;
|
|
this.#setState(STATES.SELECTED, options);
|
|
this.#dispatchEvent("Screenshots:RecordEvent", {
|
|
eventName: "selectedElement",
|
|
});
|
|
this.#methodsUsed.element += 1;
|
|
} else {
|
|
this.#setState(STATES.CROSSHAIRS);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the selection region dimensions and set the state to selected.
|
|
* @param {Number} pageX The x position relative to the page
|
|
* @param {Number} pageY The y position relative to the page
|
|
*/
|
|
draggingDragEnd(pageX, pageY) {
|
|
this.selectionRegion.dimensions = {
|
|
right: pageX,
|
|
bottom: pageY,
|
|
};
|
|
this.#setState(STATES.SELECTED);
|
|
this.maybeRecordRegionSelected();
|
|
this.#methodsUsed.region += 1;
|
|
}
|
|
|
|
/**
|
|
* Update the selection region dimensions by calling `resizingDrag` and set
|
|
* the state to selected.
|
|
* @param {Number} pageX The x position relative to the page
|
|
* @param {Number} pageY The y position relative to the page
|
|
*/
|
|
resizingDragEnd(pageX, pageY) {
|
|
this.resizingDrag(pageX, pageY);
|
|
this.#setState(STATES.SELECTED);
|
|
this.maybeRecordRegionSelected();
|
|
if (this.#moverId === "highlight") {
|
|
this.#methodsUsed.move += 1;
|
|
} else {
|
|
this.#methodsUsed.resize += 1;
|
|
}
|
|
}
|
|
|
|
maybeRecordRegionSelected() {
|
|
let { width, height } = this.selectionRegion.dimensions;
|
|
|
|
if (
|
|
!this.#previousDimensions ||
|
|
(Math.abs(this.#previousDimensions.width - width) >
|
|
REGION_CHANGE_THRESHOLD &&
|
|
Math.abs(this.#previousDimensions.height - height) >
|
|
REGION_CHANGE_THRESHOLD)
|
|
) {
|
|
this.#dispatchEvent("Screenshots:RecordEvent", {
|
|
eventName: "selectedRegionSelection",
|
|
});
|
|
}
|
|
this.#previousDimensions = { width, height };
|
|
}
|
|
|
|
/**
|
|
* Draw the preview eyes pointer towards the mouse.
|
|
* @param {Number} clientX The x position relative to the viewport
|
|
* @param {Number} clientY The y position relative to the viewport
|
|
*/
|
|
drawPreviewEyes(clientX, clientY) {
|
|
let { clientWidth, clientHeight } = this.windowDimensions.dimensions;
|
|
const xpos = Math.floor((10 * (clientX - clientWidth / 2)) / clientWidth);
|
|
const ypos = Math.floor((10 * (clientY - clientHeight / 2)) / clientHeight);
|
|
const move = `transform:translate(${xpos}px, ${ypos}px);`;
|
|
this.leftEye.style = move;
|
|
this.rightEye.style = move;
|
|
}
|
|
|
|
showPreviewContainer() {
|
|
this.previewContainer.hidden = false;
|
|
}
|
|
|
|
hidePreviewContainer() {
|
|
this.previewContainer.hidden = true;
|
|
}
|
|
|
|
updatePreviewContainer() {
|
|
let { clientWidth, clientHeight } = this.windowDimensions.dimensions;
|
|
this.previewContainer.style.width = `${clientWidth}px`;
|
|
this.previewContainer.style.height = `${clientHeight}px`;
|
|
}
|
|
|
|
/**
|
|
* Update the screenshots overlay container based on the window dimensions.
|
|
*/
|
|
updateScreenshotsOverlayContainer() {
|
|
let { scrollWidth, scrollHeight, scrollMinX } =
|
|
this.windowDimensions.dimensions;
|
|
this.screenshotsContainer.style = `left:${scrollMinX};width:${scrollWidth}px;height:${scrollHeight}px;`;
|
|
}
|
|
|
|
showScreenshotsOverlayContainer() {
|
|
this.screenshotsContainer.hidden = false;
|
|
}
|
|
|
|
hideScreenshotsOverlayContainer() {
|
|
this.screenshotsContainer.hidden = true;
|
|
}
|
|
|
|
/**
|
|
* Draw the hover element container based on the hover element region.
|
|
*/
|
|
drawHoverElementRegion() {
|
|
this.showHoverElementContainer();
|
|
|
|
let { top, left, width, height } = this.hoverElementRegion.dimensions;
|
|
|
|
this.hoverElementContainer.style = `top:${top}px;left:${left}px;width:${width}px;height:${height}px;`;
|
|
}
|
|
|
|
showHoverElementContainer() {
|
|
this.hoverElementContainer.hidden = false;
|
|
}
|
|
|
|
hideHoverElementContainer() {
|
|
this.hoverElementContainer.hidden = true;
|
|
}
|
|
|
|
/**
|
|
* Draw each background element and the highlight element base on the
|
|
* selection region.
|
|
*/
|
|
drawSelectionContainer() {
|
|
this.showSelectionContainer();
|
|
|
|
let { top, left, right, bottom, width, height } =
|
|
this.selectionRegion.dimensions;
|
|
|
|
this.highlightEl.style = `top:${top}px;left:${left}px;width:${width}px;height:${height}px;`;
|
|
|
|
this.leftBackgroundEl.style = `top:${top}px;width:${left}px;height:${height}px;`;
|
|
this.topBackgroundEl.style.height = `${top}px`;
|
|
this.rightBackgroundEl.style = `top:${top}px;left:${right}px;width:calc(100% - ${right}px);height:${height}px;`;
|
|
this.bottomBackgroundEl.style = `top:${bottom}px;height:calc(100% - ${bottom}px);`;
|
|
|
|
this.updateSelectionSizeText();
|
|
}
|
|
|
|
/**
|
|
* Update the size of the selected region. Use the zoom to correctly display
|
|
* the region dimensions.
|
|
*/
|
|
updateSelectionSizeText() {
|
|
let { width, height } = this.selectionRegion.dimensions;
|
|
let zoom = Math.round(this.window.browsingContext.fullZoom * 100) / 100;
|
|
|
|
let [selectionSizeTranslation] =
|
|
lazy.overlayLocalization.formatMessagesSync([
|
|
{
|
|
id: "screenshots-overlay-selection-region-size-3",
|
|
args: {
|
|
width: Math.floor(width * zoom),
|
|
height: Math.floor(height * zoom),
|
|
},
|
|
},
|
|
]);
|
|
this.selectionSize.textContent = selectionSizeTranslation.value;
|
|
}
|
|
|
|
showSelectionContainer() {
|
|
this.selectionContainer.hidden = false;
|
|
}
|
|
|
|
hideSelectionContainer() {
|
|
this.selectionContainer.hidden = true;
|
|
}
|
|
|
|
/**
|
|
* Draw the buttons container in the bottom right corner of the selection
|
|
* container if possible.
|
|
* The buttons will be visible in the viewport if the selection container
|
|
* is within the viewport, otherwise skip drawing the buttons.
|
|
*/
|
|
drawButtonsContainer() {
|
|
this.showButtonsContainer();
|
|
|
|
let {
|
|
left: boxLeft,
|
|
top: boxTop,
|
|
right: boxRight,
|
|
bottom: boxBottom,
|
|
} = this.selectionRegion.dimensions;
|
|
|
|
let { clientWidth, clientHeight, scrollX, scrollY, scrollWidth } =
|
|
this.windowDimensions.dimensions;
|
|
|
|
if (!this.windowDimensions.isInViewport(this.selectionRegion.dimensions)) {
|
|
// The box is entirely offscreen so need to draw the buttons
|
|
|
|
return;
|
|
}
|
|
|
|
let top = boxBottom;
|
|
|
|
if (scrollY + clientHeight - boxBottom < 70) {
|
|
if (boxBottom < scrollY + clientHeight) {
|
|
top = boxBottom - 60;
|
|
} else if (scrollY + clientHeight - boxTop < 70) {
|
|
top = boxTop - 60;
|
|
} else {
|
|
top = scrollY + clientHeight - 60;
|
|
}
|
|
}
|
|
|
|
if (!this.buttonsContainerRect) {
|
|
this.buttonsContainerRect = this.buttonsContainer.getBoundingClientRect();
|
|
}
|
|
|
|
let viewportLeft = scrollX;
|
|
let viewportRight = scrollX + clientWidth;
|
|
|
|
let left, right;
|
|
let isLTR = !Services.locale.isAppLocaleRTL;
|
|
if (isLTR) {
|
|
left = Math.max(
|
|
Math.min(viewportRight, boxRight),
|
|
viewportLeft + Math.ceil(this.buttonsContainerRect.width)
|
|
);
|
|
right = scrollWidth - left;
|
|
|
|
this.buttonsContainer.style.right = `${right}px`;
|
|
this.buttonsContainer.style.left = "";
|
|
} else {
|
|
left = Math.min(
|
|
Math.max(viewportLeft, boxLeft),
|
|
viewportRight - Math.ceil(this.buttonsContainerRect.width)
|
|
);
|
|
|
|
this.buttonsContainer.style.left = `${left}px`;
|
|
this.buttonsContainer.style.right = "";
|
|
}
|
|
|
|
this.buttonsContainer.style.top = `${top}px`;
|
|
}
|
|
|
|
showButtonsContainer() {
|
|
this.buttonsContainer.hidden = false;
|
|
}
|
|
|
|
hideButtonsContainer() {
|
|
this.buttonsContainer.hidden = true;
|
|
}
|
|
|
|
updateCursorRegion(left, top) {
|
|
this.cursorRegion = { left, top, right: left, bottom: top };
|
|
}
|
|
|
|
/**
|
|
* Set the pointer events to none on the screenshots elements so
|
|
* elementFromPoint can find the real element at the given point.
|
|
*/
|
|
setPointerEventsNone() {
|
|
this.screenshotsContainer.style.pointerEvents = "none";
|
|
}
|
|
|
|
resetPointerEvents() {
|
|
this.screenshotsContainer.style.pointerEvents = "";
|
|
}
|
|
|
|
/**
|
|
* Try to find a reasonable element for a given point.
|
|
* If a reasonable element is found, draw the hover element container for
|
|
* that element region.
|
|
* @param {Number} clientX The x position relative to the viewport
|
|
* @param {Number} clientY The y position relative to the viewport
|
|
*/
|
|
async handleElementHover(clientX, clientY) {
|
|
this.setPointerEventsNone();
|
|
let promise = getElementFromPoint(clientX, clientY, this.document);
|
|
this.resetPointerEvents();
|
|
let { ele, rect } = await promise;
|
|
|
|
if (
|
|
this.#cachedEle &&
|
|
!this.window.HTMLIFrameElement.isInstance(this.#cachedEle) &&
|
|
this.#cachedEle === ele
|
|
) {
|
|
// Still hovering over the same element
|
|
return;
|
|
}
|
|
this.#cachedEle = ele;
|
|
|
|
if (!rect) {
|
|
// this means we found an element that wasn't an iframe
|
|
rect = getBestRectForElement(ele, this.document);
|
|
}
|
|
|
|
if (rect) {
|
|
let { scrollX, scrollY } = this.windowDimensions.dimensions;
|
|
let { left, top, right, bottom } = rect;
|
|
let newRect = {
|
|
left: left + scrollX,
|
|
top: top + scrollY,
|
|
right: right + scrollX,
|
|
bottom: bottom + scrollY,
|
|
};
|
|
this.hoverElementRegion.dimensions = newRect;
|
|
this.drawHoverElementRegion();
|
|
} else {
|
|
this.hoverElementRegion.resetDimensions();
|
|
this.hideHoverElementContainer();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scroll the viewport if near one or both of the edges.
|
|
* @param {Number} pageX The x position relative to the page
|
|
* @param {Number} pageY The y position relative to the page
|
|
*/
|
|
scrollIfByEdge(pageX, pageY) {
|
|
let { scrollX, scrollY, clientWidth, clientHeight } =
|
|
this.windowDimensions.dimensions;
|
|
|
|
if (pageY - scrollY < SCROLL_BY_EDGE) {
|
|
// Scroll up
|
|
this.scrollWindow(0, -(SCROLL_BY_EDGE - (pageY - scrollY)));
|
|
} else if (scrollY + clientHeight - pageY < SCROLL_BY_EDGE) {
|
|
// Scroll down
|
|
this.scrollWindow(0, SCROLL_BY_EDGE - (scrollY + clientHeight - pageY));
|
|
}
|
|
|
|
if (pageX - scrollX <= SCROLL_BY_EDGE) {
|
|
// Scroll left
|
|
this.scrollWindow(-(SCROLL_BY_EDGE - (pageX - scrollX)), 0);
|
|
} else if (scrollX + clientWidth - pageX <= SCROLL_BY_EDGE) {
|
|
// Scroll right
|
|
this.scrollWindow(SCROLL_BY_EDGE - (scrollX + clientWidth - pageX), 0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scroll the window by the given amount.
|
|
* @param {Number} x The x amount to scroll
|
|
* @param {Number} y The y amount to scroll
|
|
*/
|
|
scrollWindow(x, y) {
|
|
this.window.scrollBy(x, y);
|
|
this.updateScreenshotsOverlayDimensions("scroll");
|
|
}
|
|
|
|
/**
|
|
* The page was resized or scrolled. We need to update the screenshots
|
|
* container size so we don't draw outside the page bounds.
|
|
* @param {String} eventType will be "scroll" or "resize"
|
|
*/
|
|
async updateScreenshotsOverlayDimensions(eventType) {
|
|
let updateWindowDimensionsPromise = this.updateWindowDimensions();
|
|
|
|
if (this.#state === STATES.CROSSHAIRS) {
|
|
if (eventType === "resize") {
|
|
this.hideHoverElementContainer();
|
|
this.#cachedEle = null;
|
|
} else if (eventType === "scroll") {
|
|
if (this.#lastClientX && this.#lastClientY) {
|
|
this.#cachedEle = null;
|
|
this.handleElementHover(this.#lastClientX, this.#lastClientY);
|
|
}
|
|
}
|
|
} else if (this.#state === STATES.SELECTED) {
|
|
await updateWindowDimensionsPromise;
|
|
this.selectionRegion.shift();
|
|
this.drawSelectionContainer();
|
|
this.drawButtonsContainer();
|
|
this.updateSelectionSizeText();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the window's dimensions for the current window.
|
|
*
|
|
* @return {Object} An object containing window dimensions
|
|
* {
|
|
* clientWidth: The width of the viewport
|
|
* clientHeight: The height of the viewport
|
|
* scrollWidth: The width of the enitre page
|
|
* scrollHeight: The height of the entire page
|
|
* scrollX: The X scroll offset of the viewport
|
|
* scrollY: The Y scroll offest of the viewport
|
|
* scrollMinX: The X minimum the viewport can scroll to
|
|
* scrollMinY: The Y minimum the viewport can scroll to
|
|
* scrollMaxX: The X maximum the viewport can scroll to
|
|
* scrollMaxY: The Y maximum the viewport can scroll to
|
|
* }
|
|
*/
|
|
getDimensionsFromWindow() {
|
|
let {
|
|
innerHeight,
|
|
innerWidth,
|
|
scrollMaxY,
|
|
scrollMaxX,
|
|
scrollMinY,
|
|
scrollMinX,
|
|
scrollY,
|
|
scrollX,
|
|
} = this.window;
|
|
|
|
let scrollWidth = innerWidth + scrollMaxX - scrollMinX;
|
|
let scrollHeight = innerHeight + scrollMaxY - scrollMinY;
|
|
let clientHeight = innerHeight;
|
|
let clientWidth = innerWidth;
|
|
|
|
const scrollbarHeight = {};
|
|
const scrollbarWidth = {};
|
|
this.window.windowUtils.getScrollbarSize(
|
|
false,
|
|
scrollbarWidth,
|
|
scrollbarHeight
|
|
);
|
|
scrollWidth -= scrollbarWidth.value;
|
|
scrollHeight -= scrollbarHeight.value;
|
|
clientWidth -= scrollbarWidth.value;
|
|
clientHeight -= scrollbarHeight.value;
|
|
|
|
return {
|
|
clientWidth,
|
|
clientHeight,
|
|
scrollWidth,
|
|
scrollHeight,
|
|
scrollX,
|
|
scrollY,
|
|
scrollMinX,
|
|
scrollMinY,
|
|
scrollMaxX,
|
|
scrollMaxY,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* We have to be careful not to draw the overlay larger than the document
|
|
* because the overlay is absolutely position and within the document so we
|
|
* can cause the document to overflow when it shouldn't. To mitigate this,
|
|
* we will temporarily position the overlay to position fixed with width and
|
|
* height 100% so the overlay is within the document bounds. Then we will get
|
|
* the dimensions of the document to correctly draw the overlay.
|
|
*/
|
|
async updateWindowDimensions() {
|
|
// Setting the screenshots container attribute "resizing" will make the
|
|
// overlay fixed position with width and height of 100% percent so it
|
|
// does not draw outside the actual document.
|
|
this.screenshotsContainer.toggleAttribute("resizing", true);
|
|
|
|
await new Promise(r => this.window.requestAnimationFrame(r));
|
|
|
|
let {
|
|
clientWidth,
|
|
clientHeight,
|
|
scrollWidth,
|
|
scrollHeight,
|
|
scrollX,
|
|
scrollY,
|
|
scrollMinX,
|
|
scrollMinY,
|
|
scrollMaxX,
|
|
scrollMaxY,
|
|
} = this.getDimensionsFromWindow();
|
|
this.screenshotsContainer.toggleAttribute("resizing", false);
|
|
|
|
this.windowDimensions.dimensions = {
|
|
clientWidth,
|
|
clientHeight,
|
|
scrollWidth,
|
|
scrollHeight,
|
|
scrollX,
|
|
scrollY,
|
|
scrollMinX,
|
|
scrollMinY,
|
|
scrollMaxX,
|
|
scrollMaxY,
|
|
devicePixelRatio: this.window.devicePixelRatio,
|
|
};
|
|
|
|
this.updatePreviewContainer();
|
|
this.updateScreenshotsOverlayContainer();
|
|
|
|
setMaxDetectHeight(Math.max(clientHeight + 100, 700));
|
|
setMaxDetectWidth(Math.max(clientWidth + 100, 1000));
|
|
}
|
|
}
|