summaryrefslogtreecommitdiffstats
path: root/browser/extensions/screenshots/selector/uicontrol.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/extensions/screenshots/selector/uicontrol.js')
-rw-r--r--browser/extensions/screenshots/selector/uicontrol.js901
1 files changed, 901 insertions, 0 deletions
diff --git a/browser/extensions/screenshots/selector/uicontrol.js b/browser/extensions/screenshots/selector/uicontrol.js
new file mode 100644
index 0000000000..f3aff2c5c8
--- /dev/null
+++ b/browser/extensions/screenshots/selector/uicontrol.js
@@ -0,0 +1,901 @@
+/* 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/. */
+
+/* globals log, catcher, util, ui, slides */
+/* globals shooter, callBackground, selectorLoader, assertIsTrusted, buildSettings, selection */
+
+"use strict";
+
+this.uicontrol = (function() {
+ const exports = {};
+
+ /** ********************************************************
+ * selection
+ */
+
+ /* 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
+ "cancelled":
+ Everything has been cancelled
+ "previewing":
+ The user is previewing the full-screen/visible image
+
+ A mousedown goes from crosshairs to dragging.
+ A mouseup goes from dragging to selected
+ A click outside of the selection goes from selected to crosshairs
+ A click on one of the draggers goes from selected to resizing
+
+ State variables:
+
+ state (string, one of the above)
+ mousedownPos (object with x/y during draggingReady, shows where the selection started)
+ selectedPos (object with x/y/h/w during selected or dragging, gives the entire selection)
+ resizeDirection (string: top, topLeft, etc, during resizing)
+ resizeStartPos (x/y position where resizing started)
+ mouseupNoAutoselect (true if a mouseup in draggingReady should not trigger autoselect)
+
+ */
+
+ const { watchFunction, watchPromise } = catcher;
+
+ const MAX_PAGE_HEIGHT = buildSettings.maxImageHeight;
+ const MAX_PAGE_WIDTH = buildSettings.maxImageWidth;
+ // An autoselection smaller than these will be ignored entirely:
+ const MIN_DETECT_ABSOLUTE_HEIGHT = 10;
+ const MIN_DETECT_ABSOLUTE_WIDTH = 30;
+ // An autoselection smaller than these will not be preferred:
+ const MIN_DETECT_HEIGHT = 30;
+ const MIN_DETECT_WIDTH = 100;
+ // An autoselection bigger than either of these will be ignored:
+ const MAX_DETECT_HEIGHT = Math.max(window.innerHeight + 100, 700);
+ const MAX_DETECT_WIDTH = Math.max(window.innerWidth + 100, 1000);
+ // This is how close (in pixels) you can get to the edge of the window and then
+ // it will scroll:
+ const SCROLL_BY_EDGE = 20;
+ // This is how wide the inboard scrollbars are, generally 0 except on Mac
+ const SCROLLBAR_WIDTH = (window.navigator.platform.match(/Mac/i)) ? 17 : 0;
+
+
+ const { Selection } = selection;
+ const { sendEvent } = shooter;
+ const log = global.log;
+
+ function round10(n) {
+ return Math.floor(n / 10) * 10;
+ }
+
+ function eventOptionsForBox(box) {
+ return {
+ cd1: round10(Math.abs(box.bottom - box.top)),
+ cd2: round10(Math.abs(box.right - box.left)),
+ };
+ }
+
+ function eventOptionsForResize(boxStart, boxEnd) {
+ return {
+ cd1: round10(
+ (boxEnd.bottom - boxEnd.top)
+ - (boxStart.bottom - boxStart.top)),
+ cd2: round10(
+ (boxEnd.right - boxEnd.left)
+ - (boxStart.right - boxStart.left)),
+ };
+ }
+
+ function eventOptionsForMove(posStart, posEnd) {
+ return {
+ cd1: round10(posEnd.y - posStart.y),
+ cd2: round10(posEnd.x - posStart.x),
+ };
+ }
+
+ function downloadShot() {
+ const previewDataUrl = (captureType === "fullPageTruncated") ? null : dataUrl;
+ // Downloaded shots don't have dimension limits
+ removeDimensionLimitsOnFullPageShot();
+ shooter.downloadShot(selectedPos, previewDataUrl, captureType);
+ }
+
+ function copyShot() {
+ const previewDataUrl = (captureType === "fullPageTruncated") ? null : dataUrl;
+ // Copied shots don't have dimension limits
+ removeDimensionLimitsOnFullPageShot();
+ shooter.copyShot(selectedPos, previewDataUrl, captureType);
+ }
+
+ /** *********************************************
+ * State and stateHandlers infrastructure
+ */
+
+ // This enumerates all the anchors on the selection, and what part of the
+ // selection they move:
+ const movements = {
+ topLeft: ["x1", "y1"],
+ top: [null, "y1"],
+ topRight: ["x2", "y1"],
+ left: ["x1", null],
+ right: ["x2", null],
+ bottomLeft: ["x1", "y2"],
+ bottom: [null, "y2"],
+ bottomRight: ["x2", "y2"],
+ move: ["*", "*"],
+ };
+
+ const doNotAutoselectTags = {
+ H1: true,
+ H2: true,
+ H3: true,
+ H4: true,
+ H5: true,
+ H6: true,
+ };
+
+ let captureType;
+
+ function removeDimensionLimitsOnFullPageShot() {
+ if (captureType === "fullPageTruncated") {
+ captureType = "fullPage";
+ selectedPos = new Selection(
+ 0, 0,
+ getDocumentWidth(), getDocumentHeight());
+ }
+ }
+
+ const standardDisplayCallbacks = {
+ cancel: () => {
+ sendEvent("cancel-shot", "overlay-cancel-button");
+ exports.deactivate();
+ }, download: () => {
+ sendEvent("download-shot", "overlay-download-button");
+ downloadShot();
+ }, copy: () => {
+ sendEvent("copy-shot", "overlay-copy-button");
+ copyShot();
+ },
+ };
+
+ const standardOverlayCallbacks = {
+ cancel: () => {
+ sendEvent("cancel-shot", "cancel-preview-button");
+ exports.deactivate();
+ },
+ onClickCancel: e => {
+ sendEvent("cancel-shot", "cancel-selection-button");
+ e.preventDefault();
+ e.stopPropagation();
+ exports.deactivate();
+ },
+ onOpenMyShots: () => {
+ sendEvent("goto-myshots", "selection-button");
+ callBackground("openMyShots")
+ .then(() => exports.deactivate())
+ .catch(() => {
+ // Handled in communication.js
+ });
+ },
+ onClickVisible: () => {
+ sendEvent("capture-visible", "selection-button");
+ selectedPos = new Selection(
+ window.scrollX, window.scrollY,
+ window.scrollX + document.documentElement.clientWidth, window.scrollY + window.innerHeight);
+ captureType = "visible";
+ setState("previewing");
+ },
+ onClickFullPage: () => {
+ sendEvent("capture-full-page", "selection-button");
+ captureType = "fullPage";
+ const width = getDocumentWidth();
+ if (width > MAX_PAGE_WIDTH) {
+ captureType = "fullPageTruncated";
+ }
+ const height = getDocumentHeight();
+ if (height > MAX_PAGE_HEIGHT) {
+ captureType = "fullPageTruncated";
+ }
+ selectedPos = new Selection(
+ 0, 0,
+ width, height);
+ setState("previewing");
+ },
+ onDownloadPreview: () => {
+ sendEvent(`download-${captureType.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()}`, "download-preview-button");
+ downloadShot();
+ },
+ onCopyPreview: () => {
+ sendEvent(`copy-${captureType.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()}`, "copy-preview-button");
+ copyShot();
+ },
+ };
+
+ /** Holds all the objects that handle events for each state: */
+ const stateHandlers = {};
+
+ function getState() {
+ return getState.state;
+ }
+ getState.state = "cancel";
+
+ function setState(s) {
+ if (!stateHandlers[s]) {
+ throw new Error("Unknown state: " + s);
+ }
+ const cur = getState.state;
+ const handler = stateHandlers[cur];
+ if (handler.end) {
+ handler.end();
+ }
+ getState.state = s;
+ if (stateHandlers[s].start) {
+ stateHandlers[s].start();
+ }
+ }
+
+ /** Various values that the states use: */
+ let mousedownPos;
+ let selectedPos;
+ let resizeDirection;
+ let resizeStartPos;
+ let resizeStartSelected;
+ let resizeHasMoved;
+ let mouseupNoAutoselect = false;
+ let autoDetectRect;
+
+ /** Represents a single x/y point, typically for a mouse click that doesn't have a drag: */
+ class Pos {
+ constructor(x, y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ elementFromPoint() {
+ return ui.iframe.getElementFromPoint(
+ this.x - window.pageXOffset,
+ this.y - window.pageYOffset
+ );
+ }
+
+ distanceTo(x, y) {
+ return Math.sqrt(Math.pow(this.x - x, 2) + Math.pow(this.y - y, 2));
+ }
+ }
+
+ /** *********************************************
+ * all stateHandlers
+ */
+
+ let dataUrl;
+
+ stateHandlers.previewing = {
+ start() {
+ shooter.preview(selectedPos, captureType);
+ },
+ };
+
+ stateHandlers.crosshairs = {
+
+ cachedEl: null,
+
+ start() {
+ selectedPos = mousedownPos = null;
+ this.cachedEl = null;
+ watchPromise(ui.iframe.display(installHandlersOnDocument, standardOverlayCallbacks).then(() => {
+ ui.iframe.usePreSelection();
+ ui.Box.remove();
+ }));
+ },
+
+ mousemove(event) {
+ ui.PixelDimensions.display(event.pageX, event.pageY, event.pageX, event.pageY);
+ if (event.target.classList &&
+ (!event.target.classList.contains("preview-overlay"))) {
+ // User is hovering over a toolbar button or control
+ autoDetectRect = null;
+ if (this.cachedEl) {
+ this.cachedEl = null;
+ }
+ ui.HoverBox.hide();
+ return;
+ }
+ let el;
+ if (event.target.classList && event.target.classList.contains("preview-overlay")) {
+ // The hover is on the overlay, so we need to figure out the real element
+ el = ui.iframe.getElementFromPoint(
+ event.pageX + window.scrollX - window.pageXOffset,
+ event.pageY + window.scrollY - window.pageYOffset
+ );
+ const xpos = Math.floor(10 * (event.pageX - window.innerWidth / 2) / window.innerWidth);
+ const ypos = Math.floor(10 * (event.pageY - window.innerHeight / 2) / window.innerHeight);
+
+ for (let i = 0; i < 2; i++) {
+ const move = `translate(${xpos}px, ${ypos}px)`;
+ event.target.getElementsByClassName("eyeball")[i].style.transform = move;
+ }
+ } else {
+ // The hover is on the element we care about, so we use that
+ el = event.target;
+ }
+ if (this.cachedEl && this.cachedEl === el) {
+ // Still hovering over the same element
+ return;
+ }
+ this.cachedEl = el;
+ this.setAutodetectBasedOnElement(el);
+ },
+
+ setAutodetectBasedOnElement(el) {
+ let lastRect;
+ let lastNode;
+ let rect;
+ let attemptExtend = false;
+ let node = el;
+ while (node) {
+ rect = Selection.getBoundingClientRect(node);
+ if (!rect) {
+ rect = lastRect;
+ break;
+ }
+ if (rect.width < MIN_DETECT_WIDTH || rect.height < MIN_DETECT_HEIGHT) {
+ // Avoid infinite loop for elements with zero or nearly zero height,
+ // like non-clearfixed float parents with or without borders.
+ break;
+ }
+ if (rect.width > MAX_DETECT_WIDTH || rect.height > MAX_DETECT_HEIGHT) {
+ // Then the last rectangle is better
+ rect = lastRect;
+ attemptExtend = true;
+ break;
+ }
+ if (rect.width >= MIN_DETECT_WIDTH && rect.height >= MIN_DETECT_HEIGHT) {
+ if (!doNotAutoselectTags[node.tagName]) {
+ break;
+ }
+ }
+ lastRect = rect;
+ lastNode = node;
+ node = node.parentNode;
+ }
+ if (rect && node) {
+ const evenBetter = this.evenBetterElement(node, rect);
+ if (evenBetter) {
+ node = lastNode = evenBetter;
+ rect = Selection.getBoundingClientRect(evenBetter);
+ attemptExtend = false;
+ }
+ }
+ if (rect && attemptExtend) {
+ let extendNode = lastNode.nextSibling;
+ while (extendNode) {
+ if (extendNode.nodeType === document.ELEMENT_NODE) {
+ break;
+ }
+ extendNode = extendNode.nextSibling;
+ if (!extendNode) {
+ const parent = lastNode.parentNode;
+ for (let i = 0; i < parent.childNodes.length; i++) {
+ if (parent.childNodes[i] === lastNode) {
+ extendNode = parent.childNodes[i + 1];
+ }
+ }
+ }
+ }
+ if (extendNode) {
+ const extendSelection = Selection.getBoundingClientRect(extendNode);
+ const extendRect = rect.union(extendSelection);
+ if (extendRect.width <= MAX_DETECT_WIDTH && extendRect.height <= MAX_DETECT_HEIGHT) {
+ rect = extendRect;
+ }
+ }
+ }
+
+ if (rect && (rect.width < MIN_DETECT_ABSOLUTE_WIDTH || rect.height < MIN_DETECT_ABSOLUTE_HEIGHT)) {
+ rect = null;
+ }
+ if (!rect) {
+ ui.HoverBox.hide();
+ } else {
+ ui.HoverBox.display(rect);
+ }
+ autoDetectRect = rect;
+ },
+
+ /** When we find an element, maybe there's one that's just a little bit better... */
+ evenBetterElement(node, origRect) {
+ let el = node.parentNode;
+ const ELEMENT_NODE = document.ELEMENT_NODE;
+ while (el && el.nodeType === ELEMENT_NODE) {
+ if (!el.getAttribute) {
+ return null;
+ }
+ const role = el.getAttribute("role");
+ if (role === "article" || (el.className && typeof el.className === "string" && el.className.search("tweet ") !== -1)) {
+ const rect = Selection.getBoundingClientRect(el);
+ if (!rect) {
+ return null;
+ }
+ if (rect.width <= MAX_DETECT_WIDTH && rect.height <= MAX_DETECT_HEIGHT) {
+ return el;
+ }
+ return null;
+ }
+ el = el.parentNode;
+ }
+ return null;
+ },
+
+ mousedown(event) {
+ // FIXME: this is happening but we don't know why, we'll track it now
+ // but avoid popping up messages:
+ if (typeof ui === "undefined") {
+ const exc = new Error("Undefined ui in mousedown");
+ exc.unloadTime = unloadTime;
+ exc.nowTime = Date.now();
+ exc.noPopup = true;
+ exc.noReport = true;
+ throw exc;
+ }
+ if (ui.isHeader(event.target)) {
+ return undefined;
+ }
+ // If the pageX is greater than this, then probably it's an attempt to get
+ // to the scrollbar, or an actual scroll, and not an attempt to start the
+ // selection:
+ const maxX = window.innerWidth - SCROLLBAR_WIDTH;
+ if (event.pageX >= maxX) {
+ event.stopPropagation();
+ event.preventDefault();
+ return false;
+ }
+
+ mousedownPos = new Pos(event.pageX + window.scrollX, event.pageY + window.scrollY);
+ setState("draggingReady");
+ event.stopPropagation();
+ event.preventDefault();
+ return false;
+ },
+
+ end() {
+ ui.HoverBox.remove();
+ ui.PixelDimensions.remove();
+ },
+ };
+
+ stateHandlers.draggingReady = {
+ minMove: 40, // px
+ minAutoImageWidth: 40,
+ minAutoImageHeight: 40,
+ maxAutoElementWidth: 800,
+ maxAutoElementHeight: 600,
+
+ start() {
+ ui.iframe.usePreSelection();
+ ui.Box.remove();
+ },
+
+ mousemove(event) {
+ if (mousedownPos.distanceTo(event.pageX, event.pageY) > this.minMove) {
+ selectedPos = new Selection(
+ mousedownPos.x,
+ mousedownPos.y,
+ event.pageX + window.scrollX,
+ event.pageY + window.scrollY);
+ mousedownPos = null;
+ setState("dragging");
+ }
+ },
+
+ mouseup(event) {
+ // If we don't get into "dragging" then we attempt an autoselect
+ if (mouseupNoAutoselect) {
+ sendEvent("cancel-selection", "selection-background-mousedown");
+ setState("crosshairs");
+ return false;
+ }
+ if (autoDetectRect) {
+ selectedPos = autoDetectRect;
+ selectedPos.x1 += window.scrollX;
+ selectedPos.y1 += window.scrollY;
+ selectedPos.x2 += window.scrollX;
+ selectedPos.y2 += window.scrollY;
+ autoDetectRect = null;
+ mousedownPos = null;
+ ui.iframe.useSelection();
+ ui.Box.display(selectedPos, standardDisplayCallbacks);
+ sendEvent("make-selection", "selection-click", eventOptionsForBox(selectedPos));
+ setState("selected");
+ sendEvent("autoselect");
+ } else {
+ sendEvent("no-selection", "no-element-found");
+ setState("crosshairs");
+ }
+ return undefined;
+ },
+
+ click(event) {
+ this.mouseup(event);
+ },
+
+ findGoodEl() {
+ let el = mousedownPos.elementFromPoint();
+ if (!el) {
+ return null;
+ }
+ const isGoodEl = (el) => {
+ if (el.nodeType !== document.ELEMENT_NODE) {
+ return false;
+ }
+ if (el.tagName === "IMG") {
+ const rect = el.getBoundingClientRect();
+ return rect.width >= this.minAutoImageWidth && rect.height >= this.minAutoImageHeight;
+ }
+ const display = window.getComputedStyle(el).display;
+ if (["block", "inline-block", "table"].includes(display)) {
+ return true;
+ // FIXME: not sure if this is useful:
+ // let rect = el.getBoundingClientRect();
+ // return rect.width <= this.maxAutoElementWidth && rect.height <= this.maxAutoElementHeight;
+ }
+ return false;
+ };
+ while (el) {
+ if (isGoodEl(el)) {
+ return el;
+ }
+ el = el.parentNode;
+ }
+ return null;
+ },
+
+ end() {
+ mouseupNoAutoselect = false;
+ },
+
+ };
+
+ stateHandlers.dragging = {
+
+ start() {
+ ui.iframe.useSelection();
+ ui.Box.display(selectedPos);
+ },
+
+ mousemove(event) {
+ selectedPos.x2 = util.truncateX(event.pageX);
+ selectedPos.y2 = util.truncateY(event.pageY);
+ scrollIfByEdge(event.pageX, event.pageY);
+ ui.Box.display(selectedPos);
+ ui.PixelDimensions.display(event.pageX, event.pageY, selectedPos.width, selectedPos.height);
+ },
+
+ mouseup(event) {
+ selectedPos.x2 = util.truncateX(event.pageX);
+ selectedPos.y2 = util.truncateY(event.pageY);
+ ui.Box.display(selectedPos, standardDisplayCallbacks);
+ sendEvent(
+ "make-selection", "selection-drag",
+ eventOptionsForBox({
+ top: selectedPos.y1,
+ bottom: selectedPos.y2,
+ left: selectedPos.x1,
+ right: selectedPos.x2,
+ }));
+ setState("selected");
+ },
+
+ end() {
+ ui.PixelDimensions.remove();
+ },
+ };
+
+ stateHandlers.selected = {
+ start() {
+ ui.iframe.useSelection();
+ },
+
+ mousedown(event) {
+ const target = event.target;
+ if (target.tagName === "HTML") {
+ // This happens when you click on the scrollbar
+ return undefined;
+ }
+ const direction = ui.Box.draggerDirection(target);
+ if (direction) {
+ sendEvent("start-resize-selection", "handle");
+ stateHandlers.resizing.startResize(event, direction);
+ } else if (ui.Box.isSelection(target)) {
+ sendEvent("start-move-selection", "selection");
+ stateHandlers.resizing.startResize(event, "move");
+ } else if (!ui.Box.isControl(target)) {
+ mousedownPos = new Pos(event.pageX, event.pageY);
+ setState("crosshairs");
+ }
+ event.preventDefault();
+ return false;
+ },
+ };
+
+ stateHandlers.resizing = {
+ start() {
+ ui.iframe.useSelection();
+ selectedPos.sortCoords();
+ },
+
+ startResize(event, direction) {
+ selectedPos.sortCoords();
+ resizeDirection = direction;
+ resizeStartPos = new Pos(event.pageX, event.pageY);
+ resizeStartSelected = selectedPos.clone();
+ resizeHasMoved = false;
+ setState("resizing");
+ },
+
+ mousemove(event) {
+ this._resize(event);
+ if (resizeDirection !== "move") {
+ ui.PixelDimensions.display(event.pageX, event.pageY, selectedPos.width, selectedPos.height);
+ }
+ return false;
+ },
+
+ mouseup(event) {
+ this._resize(event);
+ sendEvent("selection-resized");
+ ui.Box.display(selectedPos, standardDisplayCallbacks);
+ if (resizeHasMoved) {
+ if (resizeDirection === "move") {
+ const startPos = new Pos(resizeStartSelected.left, resizeStartSelected.top);
+ const endPos = new Pos(selectedPos.left, selectedPos.top);
+ sendEvent(
+ "move-selection", "mouseup",
+ eventOptionsForMove(startPos, endPos));
+ } else {
+ sendEvent(
+ "resize-selection", "mouseup",
+ eventOptionsForResize(resizeStartSelected, selectedPos));
+ }
+ } else if (resizeDirection === "move") {
+ sendEvent("keep-resize-selection", "mouseup");
+ } else {
+ sendEvent("keep-move-selection", "mouseup");
+ }
+ setState("selected");
+ },
+
+ _resize(event) {
+ const diffX = event.pageX - resizeStartPos.x;
+ const diffY = event.pageY - resizeStartPos.y;
+ const movement = movements[resizeDirection];
+ if (movement[0]) {
+ let moveX = movement[0];
+ moveX = moveX === "*" ? ["x1", "x2"] : [moveX];
+ for (const moveDir of moveX) {
+ selectedPos[moveDir] = util.truncateX(resizeStartSelected[moveDir] + diffX);
+ }
+ }
+ if (movement[1]) {
+ let moveY = movement[1];
+ moveY = moveY === "*" ? ["y1", "y2"] : [moveY];
+ for (const moveDir of moveY) {
+ selectedPos[moveDir] = util.truncateY(resizeStartSelected[moveDir] + diffY);
+ }
+ }
+ if (diffX || diffY) {
+ resizeHasMoved = true;
+ }
+ scrollIfByEdge(event.pageX, event.pageY);
+ ui.Box.display(selectedPos);
+ },
+
+ end() {
+ resizeDirection = resizeStartPos = resizeStartSelected = null;
+ selectedPos.sortCoords();
+ ui.PixelDimensions.remove();
+ },
+ };
+
+ stateHandlers.cancel = {
+ start() {
+ ui.iframe.hide();
+ ui.Box.remove();
+ },
+ };
+
+ function getDocumentWidth() {
+ return Math.max(
+ document.body && document.body.clientWidth,
+ document.documentElement.clientWidth,
+ document.body && document.body.scrollWidth,
+ document.documentElement.scrollWidth);
+ }
+ function getDocumentHeight() {
+ return Math.max(
+ document.body && document.body.clientHeight,
+ document.documentElement.clientHeight,
+ document.body && document.body.scrollHeight,
+ document.documentElement.scrollHeight);
+ }
+
+ function scrollIfByEdge(pageX, pageY) {
+ const top = window.scrollY;
+ const bottom = top + window.innerHeight;
+ const left = window.scrollX;
+ const right = left + window.innerWidth;
+ if (pageY + SCROLL_BY_EDGE >= bottom && bottom < getDocumentHeight()) {
+ window.scrollBy(0, SCROLL_BY_EDGE);
+ } else if (pageY - SCROLL_BY_EDGE <= top) {
+ window.scrollBy(0, -SCROLL_BY_EDGE);
+ }
+ if (pageX + SCROLL_BY_EDGE >= right && right < getDocumentWidth()) {
+ window.scrollBy(SCROLL_BY_EDGE, 0);
+ } else if (pageX - SCROLL_BY_EDGE <= left) {
+ window.scrollBy(-SCROLL_BY_EDGE, 0);
+ }
+ }
+
+ /** *********************************************
+ * Selection communication
+ */
+
+ exports.activate = function() {
+ if (!document.body) {
+ callBackground("abortStartShot");
+ const tagName = String(document.documentElement.tagName || "").replace(/[^a-z0-9]/ig, "");
+ sendEvent("abort-start-shot", `document-is-${tagName}`);
+ selectorLoader.unloadModules();
+ return;
+ }
+ if (isFrameset()) {
+ callBackground("abortStartShot");
+ sendEvent("abort-start-shot", "frame-page");
+ selectorLoader.unloadModules();
+ return;
+ }
+ addHandlers();
+ setState("crosshairs");
+ };
+
+ function isFrameset() {
+ return document.body.tagName === "FRAMESET";
+ }
+
+ exports.deactivate = function() {
+ try {
+ sendEvent("internal", "deactivate");
+ setState("cancel");
+ callBackground("closeSelector");
+ selectorLoader.unloadModules();
+ } catch (e) {
+ log.error("Error in deactivate", e);
+ // Sometimes this fires so late that the document isn't available
+ // We don't care about the exception, so we swallow it here
+ }
+ };
+
+ let unloadTime = 0;
+
+ exports.unload = function() {
+ // Note that ui.unload() will be called on its own
+ unloadTime = Date.now();
+ removeHandlers();
+ };
+
+ /** *********************************************
+ * Event handlers
+ */
+
+ const primedDocumentHandlers = new Map();
+ let registeredDocumentHandlers = [];
+
+ function addHandlers() {
+ ["mouseup", "mousedown", "mousemove", "click"].forEach((eventName) => {
+ const fn = watchFunction(assertIsTrusted((function(eventName, event) {
+ if (typeof event.button === "number" && event.button !== 0) {
+ // Not a left click
+ return undefined;
+ }
+ if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
+ // Modified click of key
+ return undefined;
+ }
+ const state = getState();
+ const handler = stateHandlers[state];
+ if (handler[eventName]) {
+ return handler[eventName](event);
+ }
+ return undefined;
+ }).bind(null, eventName)));
+ primedDocumentHandlers.set(eventName, fn);
+ });
+ primedDocumentHandlers.set("keyup", watchFunction(assertIsTrusted(keyupHandler)));
+ primedDocumentHandlers.set("keydown", watchFunction(assertIsTrusted(keydownHandler)));
+ window.document.addEventListener("visibilitychange", visibilityChangeHandler);
+ window.addEventListener("beforeunload", beforeunloadHandler);
+ }
+
+ let mousedownSetOnDocument = false;
+
+ function installHandlersOnDocument(docObj) {
+ for (const [eventName, handler] of primedDocumentHandlers) {
+ const watchHandler = watchFunction(handler);
+ const useCapture = eventName !== "keyup";
+ docObj.addEventListener(eventName, watchHandler, useCapture);
+ registeredDocumentHandlers.push({name: eventName, doc: docObj, handler: watchHandler, useCapture});
+ }
+ if (!mousedownSetOnDocument) {
+ const mousedownHandler = primedDocumentHandlers.get("mousedown");
+ document.addEventListener("mousedown", mousedownHandler, true);
+ registeredDocumentHandlers.push({name: "mousedown", doc: document, handler: mousedownHandler, useCapture: true});
+ mousedownSetOnDocument = true;
+ }
+ }
+
+ function beforeunloadHandler() {
+ sendEvent("cancel-shot", "tab-load");
+ exports.deactivate();
+ }
+
+ function keydownHandler(event) {
+ // In MacOS, the keyup event for 'c' is not fired when performing cmd+c.
+ if (event.code === "KeyC" && (event.ctrlKey || event.metaKey)
+ && ["previewing", "selected"].includes(getState.state)) {
+ catcher.watchPromise(callBackground("getPlatformOs").then(os => {
+ if ((event.ctrlKey && os !== "mac") ||
+ (event.metaKey && os === "mac")) {
+ sendEvent("copy-shot", "keyboard-copy");
+ copyShot();
+ }
+ }));
+ }
+ }
+
+ function keyupHandler(event) {
+ if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) {
+ // unused modifier keys
+ return;
+ }
+ if ((event.key || event.code) === "Escape") {
+ sendEvent("cancel-shot", "keyboard-escape");
+ exports.deactivate();
+ }
+ // Enter to trigger Save or Download by default. But if the user tabbed to
+ // select another button, then we do not want this.
+ if ((event.key || event.code) === "Enter"
+ && getState.state === "selected"
+ && ui.iframe.document().activeElement.tagName === "BODY") {
+ sendEvent("download-shot", "keyboard-enter");
+ downloadShot();
+ }
+ }
+
+ function visibilityChangeHandler(event) {
+ // The document is the event target
+ if (event.target.hidden) {
+ sendEvent("internal", "document-hidden");
+ }
+ }
+
+ function removeHandlers() {
+ window.removeEventListener("beforeunload", beforeunloadHandler);
+ window.document.removeEventListener("visibilitychange", visibilityChangeHandler);
+ for (const {name, doc, handler, useCapture} of registeredDocumentHandlers) {
+ doc.removeEventListener(name, handler, !!useCapture);
+ }
+ registeredDocumentHandlers = [];
+ }
+
+ catcher.watchFunction(exports.activate)();
+
+ return exports;
+})();
+
+null;