forked from mirrors/gecko-dev
		
	Screenshots is now part of the browser first-run experience so get rid of the onboarding slides embedded in screenshots. This means fewer strings that need to be ported to fluent. Differential Revision: https://phabricator.services.mozilla.com/D58815 --HG-- extra : moz-landing-system : lando
		
			
				
	
	
		
			916 lines
		
	
	
	
		
			28 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			916 lines
		
	
	
	
		
			28 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/. */
 | 
						|
 | 
						|
/* 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();
 | 
						|
    }, save: () => {
 | 
						|
      sendEvent("save-shot", "overlay-save-button");
 | 
						|
      shooter.takeShot("selection", selectedPos);
 | 
						|
    }, 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");
 | 
						|
    },
 | 
						|
    onSavePreview: () => {
 | 
						|
      sendEvent(`save-${captureType.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()}`, "save-preview-button");
 | 
						|
      if (captureType === "fullPageTruncated") {
 | 
						|
        selectedPos = new Selection(
 | 
						|
          0, 0,
 | 
						|
          Math.min(selectedPos.right, MAX_PAGE_WIDTH),
 | 
						|
          Math.min(selectedPos.bottom, MAX_PAGE_HEIGHT));
 | 
						|
        dataUrl = null;
 | 
						|
      }
 | 
						|
      shooter.takeShot(captureType, selectedPos, dataUrl);
 | 
						|
    },
 | 
						|
    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() {
 | 
						|
      dataUrl = shooter.screenshotPage(selectedPos, captureType);
 | 
						|
      ui.iframe.usePreview();
 | 
						|
      ui.Preview.display(dataUrl, captureType === "fullPageTruncated");
 | 
						|
    },
 | 
						|
  };
 | 
						|
 | 
						|
  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;
 | 
						|
        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;
 |