diff options
Diffstat (limited to 'browser/extensions/screenshots/selector/ui.js')
-rw-r--r-- | browser/extensions/screenshots/selector/ui.js | 912 |
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; |