/* 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 => { 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 = ` `; 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 => { 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 = `
`; 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 => { 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 = `
`; 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;