/* 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, global */
/* globals shooter, callBackground, selectorLoader, assertIsTrusted, 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 = 10000;
const MAX_PAGE_WIDTH = 10000;
// 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();
},
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) {
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() {
// 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 = element => {
if (element.nodeType !== document.ELEMENT_NODE) {
return false;
}
if (element.tagName ===
"IMG") {
const rect = element.getBoundingClientRect();
return (
rect.width >=
this.minAutoImageWidth &&
rect.height >=
this.minAutoImageHeight
);
}
const display = window.getComputedStyle(element).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]/gi,
""
);
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");
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 (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[event.type]) {
return handler[event.type](event);
}
return undefined;
})
);
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 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;