summaryrefslogtreecommitdiffstats
path: root/browser/extensions/screenshots/selector/ui.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/extensions/screenshots/selector/ui.js')
-rw-r--r--browser/extensions/screenshots/selector/ui.js912
1 files changed, 912 insertions, 0 deletions
diff --git a/browser/extensions/screenshots/selector/ui.js b/browser/extensions/screenshots/selector/ui.js
new file mode 100644
index 0000000000..06915483bd
--- /dev/null
+++ b/browser/extensions/screenshots/selector/ui.js
@@ -0,0 +1,912 @@
+/* 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 browser, log, util, catcher, inlineSelectionCss, callBackground, assertIsTrusted, assertIsBlankDocument, blobConverters */
+
+"use strict";
+
+this.ui = (function() {
+ // eslint-disable-line no-unused-vars
+ const exports = {};
+ const SAVE_BUTTON_HEIGHT = 50;
+
+ const { watchFunction } = catcher;
+
+ exports.isHeader = function(el) {
+ while (el) {
+ if (
+ el.classList &&
+ (el.classList.contains("visible") ||
+ el.classList.contains("full-page") ||
+ el.classList.contains("cancel-shot"))
+ ) {
+ return true;
+ }
+ el = el.parentNode;
+ }
+ return false;
+ };
+
+ const substitutedCss = inlineSelectionCss.replace(
+ /MOZ_EXTENSION([^"]+)/g,
+ (match, filename) => {
+ return browser.runtime.getURL(filename);
+ }
+ );
+
+ function makeEl(tagName, className) {
+ if (!iframe.document()) {
+ throw new Error("Attempted makeEl before iframe was initialized");
+ }
+ const el = iframe.document().createElement(tagName);
+ if (className) {
+ el.className = className;
+ }
+ return el;
+ }
+
+ function onResize() {
+ if (this.sizeTracking.windowDelayer) {
+ clearTimeout(this.sizeTracking.windowDelayer);
+ }
+ this.sizeTracking.windowDelayer = setTimeout(
+ watchFunction(() => {
+ this.updateElementSize(true);
+ }),
+ 50
+ );
+ }
+
+ function initializeIframe() {
+ const el = document.createElement("iframe");
+ el.src = browser.runtime.getURL("blank.html");
+ el.style.zIndex = "99999999999";
+ el.style.border = "none";
+ el.style.top = "0";
+ el.style.left = "0";
+ el.style.margin = "0";
+ el.scrolling = "no";
+ el.style.clip = "auto";
+ el.style.backgroundColor = "transparent";
+ el.style.colorScheme = "light";
+ return el;
+ }
+
+ const iframeSelection = (exports.iframeSelection = {
+ element: null,
+ addClassName: "",
+ sizeTracking: {
+ timer: null,
+ windowDelayer: null,
+ lastHeight: null,
+ lastWidth: null,
+ },
+ document: null,
+ window: null,
+ display(installHandlerOnDocument) {
+ return new Promise((resolve, reject) => {
+ if (!this.element) {
+ this.element = initializeIframe();
+ this.element.id = "firefox-screenshots-selection-iframe";
+ this.element.style.display = "none";
+ this.element.style.setProperty("max-width", "none", "important");
+ this.element.style.setProperty("max-height", "none", "important");
+ this.element.style.setProperty("position", "absolute", "important");
+ this.element.setAttribute("role", "dialog");
+ this.updateElementSize();
+ this.element.addEventListener(
+ "load",
+ watchFunction(() => {
+ this.document = this.element.contentDocument;
+ this.window = this.element.contentWindow;
+ assertIsBlankDocument(this.document);
+ // eslint-disable-next-line no-unsanitized/property
+ this.document.documentElement.innerHTML = `
+ <head>
+ <style>${substitutedCss}</style>
+ <title></title>
+ </head>
+ <body></body>`;
+ installHandlerOnDocument(this.document);
+ if (this.addClassName) {
+ this.document.body.className = this.addClassName;
+ }
+ this.document.documentElement.dir = browser.i18n.getMessage(
+ "@@bidi_dir"
+ );
+ this.document.documentElement.lang = browser.i18n.getMessage(
+ "@@ui_locale"
+ );
+ resolve();
+ }),
+ { once: true }
+ );
+ document.body.appendChild(this.element);
+ } else {
+ resolve();
+ }
+ });
+ },
+
+ hide() {
+ this.element.style.display = "none";
+ this.stopSizeWatch();
+ },
+
+ unhide() {
+ this.updateElementSize();
+ this.element.style.display = "block";
+ this.initSizeWatch();
+ this.element.focus();
+ },
+
+ updateElementSize(force) {
+ // Note: if someone sizes down the page, then the iframe will keep the
+ // document from naturally shrinking. We use force to temporarily hide
+ // the element so that we can tell if the document shrinks
+ const visible = this.element.style.display !== "none";
+ if (force && visible) {
+ this.element.style.display = "none";
+ }
+ const height = Math.max(
+ document.documentElement.clientHeight,
+ document.body.clientHeight,
+ document.documentElement.scrollHeight,
+ document.body.scrollHeight
+ );
+ if (height !== this.sizeTracking.lastHeight) {
+ this.sizeTracking.lastHeight = height;
+ this.element.style.height = height + "px";
+ }
+ // Do not use window.innerWidth since that includes the width of the
+ // scroll bar.
+ const width = Math.max(
+ document.documentElement.clientWidth,
+ document.body.clientWidth,
+ document.documentElement.scrollWidth,
+ document.body.scrollWidth
+ );
+ if (width !== this.sizeTracking.lastWidth) {
+ this.sizeTracking.lastWidth = width;
+ this.element.style.width = width + "px";
+ // Since this frame has an absolute position relative to the parent
+ // document, if the parent document's body has a relative position and
+ // left and/or top not at 0, then the left and/or top of the parent
+ // document's body is not at (0, 0) of the viewport. That makes the
+ // frame shifted relative to the viewport. These margins negates that.
+ if (window.getComputedStyle(document.body).position === "relative") {
+ const docBoundingRect = document.documentElement.getBoundingClientRect();
+ const bodyBoundingRect = document.body.getBoundingClientRect();
+ this.element.style.marginLeft = `-${bodyBoundingRect.left -
+ docBoundingRect.left}px`;
+ this.element.style.marginTop = `-${bodyBoundingRect.top -
+ docBoundingRect.top}px`;
+ }
+ }
+ if (force && visible) {
+ this.element.style.display = "block";
+ }
+ },
+
+ initSizeWatch() {
+ this.stopSizeWatch();
+ this.sizeTracking.timer = setInterval(
+ watchFunction(this.updateElementSize.bind(this)),
+ 2000
+ );
+ window.addEventListener("resize", this.onResize, true);
+ },
+
+ stopSizeWatch() {
+ if (this.sizeTracking.timer) {
+ clearTimeout(this.sizeTracking.timer);
+ this.sizeTracking.timer = null;
+ }
+ if (this.sizeTracking.windowDelayer) {
+ clearTimeout(this.sizeTracking.windowDelayer);
+ this.sizeTracking.windowDelayer = null;
+ }
+ this.sizeTracking.lastHeight = this.sizeTracking.lastWidth = null;
+ window.removeEventListener("resize", this.onResize, true);
+ },
+
+ getElementFromPoint(x, y) {
+ this.element.style.pointerEvents = "none";
+ let el;
+ try {
+ el = document.elementFromPoint(x, y);
+ } finally {
+ this.element.style.pointerEvents = "";
+ }
+ return el;
+ },
+
+ remove() {
+ this.stopSizeWatch();
+ util.removeNode(this.element);
+ this.element = this.document = this.window = null;
+ },
+ });
+
+ iframeSelection.onResize = watchFunction(
+ assertIsTrusted(onResize.bind(iframeSelection)),
+ true
+ );
+
+ const iframePreSelection = (exports.iframePreSelection = {
+ element: null,
+ document: null,
+ window: null,
+ display(installHandlerOnDocument, standardOverlayCallbacks) {
+ return new Promise((resolve, reject) => {
+ if (!this.element) {
+ this.element = initializeIframe();
+ this.element.id = "firefox-screenshots-preselection-iframe";
+ this.element.style.setProperty("position", "fixed", "important");
+ this.element.style.width = "100%";
+ this.element.style.height = "100%";
+ this.element.style.setProperty("max-width", "none", "important");
+ this.element.style.setProperty("max-height", "none", "important");
+ this.element.setAttribute("role", "dialog");
+ this.element.addEventListener(
+ "load",
+ watchFunction(() => {
+ this.document = this.element.contentDocument;
+ this.window = this.element.contentWindow;
+ assertIsBlankDocument(this.document);
+ // eslint-disable-next-line no-unsanitized/property
+ this.document.documentElement.innerHTML = `
+ <head>
+ <link rel="localization" href="browser/screenshots.ftl">
+ <style>${substitutedCss}</style>
+ <title></title>
+ </head>
+ <body>
+ <div class="preview-overlay precision-cursor">
+ <div class="fixed-container">
+ <div class="face-container">
+ <div class="eye left"><div class="eyeball"></div></div>
+ <div class="eye right"><div class="eyeball"></div></div>
+ <div class="face"></div>
+ </div>
+ <div class="preview-instructions" data-l10n-id="screenshots-instructions"></div>
+ <button class="cancel-shot" data-l10n-id="screenshots-cancel-button"></button>
+ <div class="all-buttons-container">
+ <button class="visible" tabindex="2" data-l10n-id="screenshots-save-visible-button"></button>
+ <button class="full-page" tabindex="1" data-l10n-id="screenshots-save-page-button"></button>
+ </div>
+ </div>
+ </div>
+ </body>`;
+ installHandlerOnDocument(this.document);
+ if (this.addClassName) {
+ this.document.body.className = this.addClassName;
+ }
+ this.document.documentElement.dir = browser.i18n.getMessage(
+ "@@bidi_dir"
+ );
+ this.document.documentElement.lang = browser.i18n.getMessage(
+ "@@ui_locale"
+ );
+ const overlay = this.document.querySelector(".preview-overlay");
+ overlay
+ .querySelector(".visible")
+ .addEventListener(
+ "click",
+ watchFunction(
+ assertIsTrusted(standardOverlayCallbacks.onClickVisible)
+ )
+ );
+ overlay
+ .querySelector(".full-page")
+ .addEventListener(
+ "click",
+ watchFunction(
+ assertIsTrusted(standardOverlayCallbacks.onClickFullPage)
+ )
+ );
+ overlay
+ .querySelector(".cancel-shot")
+ .addEventListener(
+ "click",
+ watchFunction(
+ assertIsTrusted(standardOverlayCallbacks.onClickCancel)
+ )
+ );
+
+ resolve();
+ }),
+ { once: true }
+ );
+ document.body.appendChild(this.element);
+ } else {
+ resolve();
+ }
+ });
+ },
+
+ hide() {
+ window.removeEventListener(
+ "scroll",
+ watchFunction(assertIsTrusted(this.onScroll))
+ );
+ window.removeEventListener("resize", this.onResize, true);
+ if (this.element) {
+ this.element.style.display = "none";
+ }
+ },
+
+ unhide() {
+ window.addEventListener(
+ "scroll",
+ watchFunction(assertIsTrusted(this.onScroll))
+ );
+ window.addEventListener("resize", this.onResize, true);
+ this.element.style.display = "block";
+ this.element.focus();
+ },
+
+ onScroll() {
+ exports.HoverBox.hide();
+ },
+
+ getElementFromPoint(x, y) {
+ this.element.style.pointerEvents = "none";
+ let el;
+ try {
+ el = document.elementFromPoint(x, y);
+ } finally {
+ this.element.style.pointerEvents = "";
+ }
+ return el;
+ },
+
+ remove() {
+ this.hide();
+ util.removeNode(this.element);
+ this.element = this.document = this.window = null;
+ },
+ });
+
+ let msgsPromise = callBackground("getStrings", [
+ "screenshots-cancel-button",
+ "screenshots-copy-button-tooltip",
+ "screenshots-download-button-tooltip",
+ "screenshots-copy-button",
+ "screenshots-download-button",
+ ]);
+
+ const iframePreview = (exports.iframePreview = {
+ element: null,
+ document: null,
+ window: null,
+ display(installHandlerOnDocument, standardOverlayCallbacks) {
+ return new Promise((resolve, reject) => {
+ if (!this.element) {
+ this.element = initializeIframe();
+ this.element.id = "firefox-screenshots-preview-iframe";
+ this.element.style.display = "none";
+ this.element.style.setProperty("position", "fixed", "important");
+ this.element.style.height = "100%";
+ this.element.style.width = "100%";
+ this.element.style.setProperty("max-width", "none", "important");
+ this.element.style.setProperty("max-height", "none", "important");
+ this.element.setAttribute("role", "dialog");
+ this.element.onload = watchFunction(() => {
+ msgsPromise.then(([cancelTitle, copyTitle, downloadTitle]) => {
+ assertIsBlankDocument(this.element.contentDocument);
+ this.document = this.element.contentDocument;
+ this.window = this.element.contentWindow;
+ // eslint-disable-next-line no-unsanitized/property
+ this.document.documentElement.innerHTML = `
+ <head>
+ <link rel="localization" href="browser/screenshots.ftl">
+ <style>${substitutedCss}</style>
+ <title></title>
+ </head>
+ <body>
+ <div class="preview-overlay">
+ <div class="preview-image">
+ <div class="preview-buttons">
+ <button class="highlight-button-cancel" title="${cancelTitle}">
+ <img src="chrome://browser/content/screenshots/cancel.svg"/>
+ </button>
+ <button class="highlight-button-copy" title="${copyTitle}">
+ <img src="chrome://browser/content/screenshots/copy.svg"/>
+ <span data-l10n-id="screenshots-copy-button"/>
+ </button>
+ <button class="highlight-button-download" title="${downloadTitle}">
+ <img src="chrome://browser/content/screenshots/download-white.svg"/>
+ <span data-l10n-id="screenshots-download-button"/>
+ </button>
+ </div>
+ <div class="preview-image-wrapper"></div>
+ </div>
+ <div class="loader" style="display:none">
+ <div class="loader-inner"></div>
+ </div>
+ </div>
+ </body>`;
+
+ installHandlerOnDocument(this.document);
+ this.document.documentElement.dir = browser.i18n.getMessage(
+ "@@bidi_dir"
+ );
+ this.document.documentElement.lang = browser.i18n.getMessage(
+ "@@ui_locale"
+ );
+
+ const overlay = this.document.querySelector(".preview-overlay");
+ overlay
+ .querySelector(".highlight-button-copy")
+ .addEventListener(
+ "click",
+ watchFunction(
+ assertIsTrusted(standardOverlayCallbacks.onCopyPreview)
+ )
+ );
+ overlay
+ .querySelector(".highlight-button-download")
+ .addEventListener(
+ "click",
+ watchFunction(
+ assertIsTrusted(standardOverlayCallbacks.onDownloadPreview)
+ )
+ );
+ overlay
+ .querySelector(".highlight-button-cancel")
+ .addEventListener(
+ "click",
+ watchFunction(
+ assertIsTrusted(standardOverlayCallbacks.cancel)
+ )
+ );
+ resolve();
+ });
+ });
+ document.body.appendChild(this.element);
+ } else {
+ resolve();
+ }
+ });
+ },
+
+ hide() {
+ if (this.element) {
+ this.element.style.display = "none";
+ }
+ },
+
+ unhide() {
+ this.element.style.display = "block";
+ this.element.focus();
+ },
+
+ showLoader() {
+ this.document.body.querySelector(".preview-image").style.display = "none";
+ this.document.body.querySelector(".loader").style.display = "";
+ },
+
+ remove() {
+ this.hide();
+ util.removeNode(this.element);
+ this.element = null;
+ this.document = null;
+ },
+ });
+
+ iframePreSelection.onResize = watchFunction(
+ onResize.bind(iframePreSelection),
+ true
+ );
+
+ const iframe = (exports.iframe = {
+ currentIframe: iframePreSelection,
+ display(installHandlerOnDocument, standardOverlayCallbacks) {
+ return iframeSelection
+ .display(installHandlerOnDocument)
+ .then(() =>
+ iframePreSelection.display(
+ installHandlerOnDocument,
+ standardOverlayCallbacks
+ )
+ )
+ .then(() =>
+ iframePreview.display(
+ installHandlerOnDocument,
+ standardOverlayCallbacks
+ )
+ );
+ },
+
+ hide() {
+ this.currentIframe.hide();
+ },
+
+ unhide() {
+ this.currentIframe.unhide();
+ },
+
+ showLoader() {
+ if (this.currentIframe.showLoader) {
+ this.currentIframe.showLoader();
+ this.currentIframe.unhide();
+ }
+ },
+
+ getElementFromPoint(x, y) {
+ return this.currentIframe.getElementFromPoint(x, y);
+ },
+
+ remove() {
+ iframeSelection.remove();
+ iframePreSelection.remove();
+ iframePreview.remove();
+ },
+
+ getContentWindow() {
+ return this.currentIframe.element.contentWindow;
+ },
+
+ document() {
+ return this.currentIframe.document;
+ },
+
+ useSelection() {
+ if (
+ this.currentIframe === iframePreSelection ||
+ this.currentIframe === iframePreview
+ ) {
+ this.hide();
+ }
+ this.currentIframe = iframeSelection;
+ this.unhide();
+ },
+
+ usePreSelection() {
+ if (
+ this.currentIframe === iframeSelection ||
+ this.currentIframe === iframePreview
+ ) {
+ this.hide();
+ }
+ this.currentIframe = iframePreSelection;
+ this.unhide();
+ },
+
+ usePreview() {
+ if (
+ this.currentIframe === iframeSelection ||
+ this.currentIframe === iframePreSelection
+ ) {
+ this.hide();
+ }
+ this.currentIframe = iframePreview;
+ this.unhide();
+ },
+ });
+
+ const movements = [
+ "topLeft",
+ "top",
+ "topRight",
+ "left",
+ "right",
+ "bottomLeft",
+ "bottom",
+ "bottomRight",
+ ];
+
+ /** Creates the selection box */
+ exports.Box = {
+ async display(pos, callbacks) {
+ await this._createEl();
+ if (callbacks !== undefined && callbacks.cancel) {
+ // We use onclick here because we don't want addEventListener
+ // to add multiple event handlers to the same button
+ this.cancel.onclick = watchFunction(assertIsTrusted(callbacks.cancel));
+ this.cancel.style.display = "";
+ } else {
+ this.cancel.style.display = "none";
+ }
+ if (callbacks !== undefined && callbacks.download) {
+ this.download.removeAttribute("disabled");
+ this.download.onclick = watchFunction(
+ assertIsTrusted(e => {
+ this.download.setAttribute("disabled", true);
+ callbacks.download(e);
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ })
+ );
+ this.download.style.display = "";
+ } else {
+ this.download.style.display = "none";
+ }
+ if (callbacks !== undefined && callbacks.copy) {
+ this.copy.removeAttribute("disabled");
+ this.copy.onclick = watchFunction(
+ assertIsTrusted(e => {
+ this.copy.setAttribute("disabled", true);
+ callbacks.copy(e);
+ e.preventDefault();
+ e.stopPropagation();
+ })
+ );
+ this.copy.style.display = "";
+ } else {
+ this.copy.style.display = "none";
+ }
+
+ const winBottom = window.innerHeight;
+ const pageYOffset = window.pageYOffset;
+
+ if (pos.right - pos.left < 78 || pos.bottom - pos.top < 78) {
+ this.el.classList.add("small-selection");
+ } else {
+ this.el.classList.remove("small-selection");
+ }
+
+ // if the selection bounding box is w/in SAVE_BUTTON_HEIGHT px of the bottom of
+ // the window, flip controls into the box
+ if (pos.bottom > winBottom + pageYOffset - SAVE_BUTTON_HEIGHT) {
+ this.el.classList.add("bottom-selection");
+ } else {
+ this.el.classList.remove("bottom-selection");
+ }
+
+ if (pos.right < 200) {
+ this.el.classList.add("left-selection");
+ } else {
+ this.el.classList.remove("left-selection");
+ }
+ this.el.style.top = `${pos.top}px`;
+ this.el.style.left = `${pos.left}px`;
+ this.el.style.height = `${pos.bottom - pos.top}px`;
+ this.el.style.width = `${pos.right - pos.left}px`;
+ this.bgTop.style.top = "0px";
+ this.bgTop.style.height = `${pos.top}px`;
+ this.bgTop.style.left = "0px";
+ this.bgTop.style.width = "100%";
+ this.bgBottom.style.top = `${pos.bottom}px`;
+ this.bgBottom.style.height = `calc(100vh - ${pos.bottom}px)`;
+ this.bgBottom.style.left = "0px";
+ this.bgBottom.style.width = "100%";
+ this.bgLeft.style.top = `${pos.top}px`;
+ this.bgLeft.style.height = `${pos.bottom - pos.top}px`;
+ this.bgLeft.style.left = "0px";
+ this.bgLeft.style.width = `${pos.left}px`;
+ this.bgRight.style.top = `${pos.top}px`;
+ this.bgRight.style.height = `${pos.bottom - pos.top}px`;
+ this.bgRight.style.left = `${pos.right}px`;
+ this.bgRight.style.width = `calc(100% - ${pos.right}px)`;
+ },
+
+ // used to eventually move the download-only warning
+ // when a user ends scrolling or ends resizing a window
+ delayExecution(delay, cb) {
+ let timer;
+ return function() {
+ if (typeof timer !== "undefined") {
+ clearTimeout(timer);
+ }
+ timer = setTimeout(cb, delay);
+ };
+ },
+
+ remove() {
+ for (const name of ["el", "bgTop", "bgLeft", "bgRight", "bgBottom"]) {
+ if (name in this) {
+ util.removeNode(this[name]);
+ this[name] = null;
+ }
+ }
+ },
+
+ async _createEl() {
+ let boxEl = this.el;
+ if (boxEl) {
+ return;
+ }
+ let [
+ cancelTitle,
+ copyTitle,
+ downloadTitle,
+ copyText,
+ downloadText,
+ ] = await msgsPromise;
+ boxEl = makeEl("div", "highlight");
+ const buttons = makeEl("div", "highlight-buttons");
+ const cancel = makeEl("button", "highlight-button-cancel");
+ const cancelImg = makeEl("img");
+ cancelImg.src = "chrome://browser/content/screenshots/cancel.svg";
+ cancel.title = cancelTitle;
+ cancel.appendChild(cancelImg);
+ buttons.appendChild(cancel);
+
+ const copy = makeEl("button", "highlight-button-copy");
+ copy.title = copyTitle;
+ const copyImg = makeEl("img");
+ const copyString = makeEl("span");
+ copyString.textContent = copyText;
+ copyImg.src = "chrome://browser/content/screenshots/copy.svg";
+ copy.appendChild(copyImg);
+ copy.appendChild(copyString);
+ buttons.appendChild(copy);
+
+ const download = makeEl("button", "highlight-button-download");
+ const downloadImg = makeEl("img");
+ downloadImg.src =
+ "chrome://browser/content/screenshots/download-white.svg";
+ download.appendChild(downloadImg);
+ download.append(downloadText);
+ download.title = downloadTitle;
+ buttons.appendChild(download);
+ this.cancel = cancel;
+ this.download = download;
+ this.copy = copy;
+
+ boxEl.appendChild(buttons);
+ for (const name of movements) {
+ const elTarget = makeEl("div", "mover-target direction-" + name);
+ const elMover = makeEl("div", "mover");
+ elTarget.appendChild(elMover);
+ boxEl.appendChild(elTarget);
+ }
+ this.bgTop = makeEl("div", "bghighlight");
+ iframe.document().body.appendChild(this.bgTop);
+ this.bgLeft = makeEl("div", "bghighlight");
+ iframe.document().body.appendChild(this.bgLeft);
+ this.bgRight = makeEl("div", "bghighlight");
+ iframe.document().body.appendChild(this.bgRight);
+ this.bgBottom = makeEl("div", "bghighlight");
+ iframe.document().body.appendChild(this.bgBottom);
+ iframe.document().body.appendChild(boxEl);
+ this.el = boxEl;
+ },
+
+ draggerDirection(target) {
+ while (target) {
+ if (target.nodeType === document.ELEMENT_NODE) {
+ if (target.classList.contains("mover-target")) {
+ for (const name of movements) {
+ if (target.classList.contains("direction-" + name)) {
+ return name;
+ }
+ }
+ catcher.unhandled(new Error("Surprising mover element"), {
+ element: target.outerHTML,
+ });
+ log.warn("Got mover-target that wasn't a specific direction");
+ }
+ }
+ target = target.parentNode;
+ }
+ return null;
+ },
+
+ isSelection(target) {
+ while (target) {
+ if (target.tagName === "BUTTON") {
+ return false;
+ }
+ if (
+ target.nodeType === document.ELEMENT_NODE &&
+ target.classList.contains("highlight")
+ ) {
+ return true;
+ }
+ target = target.parentNode;
+ }
+ return false;
+ },
+
+ isControl(target) {
+ while (target) {
+ if (
+ target.nodeType === document.ELEMENT_NODE &&
+ target.classList.contains("highlight-buttons")
+ ) {
+ return true;
+ }
+ target = target.parentNode;
+ }
+ return false;
+ },
+
+ el: null,
+ boxTopEl: null,
+ boxLeftEl: null,
+ boxRightEl: null,
+ boxBottomEl: null,
+ };
+
+ exports.HoverBox = {
+ el: null,
+
+ display(rect) {
+ if (!this.el) {
+ this.el = makeEl("div", "hover-highlight");
+ iframe.document().body.appendChild(this.el);
+ }
+ this.el.style.display = "";
+ this.el.style.top = rect.top - 1 + "px";
+ this.el.style.left = rect.left - 1 + "px";
+ this.el.style.width = rect.right - rect.left + 2 + "px";
+ this.el.style.height = rect.bottom - rect.top + 2 + "px";
+ },
+
+ hide() {
+ if (this.el) {
+ this.el.style.display = "none";
+ }
+ },
+
+ remove() {
+ util.removeNode(this.el);
+ this.el = null;
+ },
+ };
+
+ exports.PixelDimensions = {
+ el: null,
+ xEl: null,
+ yEl: null,
+ display(xPos, yPos, x, y) {
+ if (!this.el) {
+ this.el = makeEl("div", "pixel-dimensions");
+ this.xEl = makeEl("div");
+ this.el.appendChild(this.xEl);
+ this.yEl = makeEl("div");
+ this.el.appendChild(this.yEl);
+ iframe.document().body.appendChild(this.el);
+ }
+ this.xEl.textContent = Math.round(x);
+ this.yEl.textContent = Math.round(y);
+ this.el.style.top = yPos + 12 + "px";
+ this.el.style.left = xPos + 12 + "px";
+ },
+ remove() {
+ util.removeNode(this.el);
+ this.el = this.xEl = this.yEl = null;
+ },
+ };
+
+ exports.Preview = {
+ display(dataUrl) {
+ const img = makeEl("IMG");
+ const imgBlob = blobConverters.dataUrlToBlob(dataUrl);
+ img.src = iframe.getContentWindow().URL.createObjectURL(imgBlob);
+ iframe
+ .document()
+ .querySelector(".preview-image-wrapper")
+ .appendChild(img);
+ },
+ };
+
+ /** Removes every UI this module creates */
+ exports.remove = function() {
+ for (const name in exports) {
+ if (name.startsWith("iframe")) {
+ continue;
+ }
+ if (typeof exports[name] === "object" && exports[name].remove) {
+ exports[name].remove();
+ }
+ }
+ exports.iframe.remove();
+ };
+
+ exports.triggerDownload = function(url, filename) {
+ return catcher.watchPromise(
+ callBackground("downloadShot", { url, filename })
+ );
+ };
+
+ exports.unload = exports.remove;
+
+ return exports;
+})();
+null;