forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			2067 lines
		
	
	
	
		
			55 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			2067 lines
		
	
	
	
		
			55 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| /**
 | |
|  * The Screenshots overlay is inserted into the document's
 | |
|  * canvasFrame anonymous content container (see dom/webidl/Document.webidl).
 | |
|  *
 | |
|  * This container gets cleared automatically when the document navigates.
 | |
|  *
 | |
|  * Since the overlay markup is inserted in the canvasFrame using
 | |
|  * insertAnonymousContent, this means that it can be modified using the API
 | |
|  * described in AnonymousContent.webidl.
 | |
|  *
 | |
|  * Any mutation of this content must be via the AnonymousContent API.
 | |
|  * This is similar in design to [devtools' highlighters](https://firefox-source-docs.mozilla.org/devtools/tools/highlighters.html#inserting-content-in-the-page),
 | |
|  * though as Screenshots doesnt need to work on XUL documents, or allow multiple kinds of
 | |
|  * highlight/overlay our case is a little simpler.
 | |
|  *
 | |
|  * To retrieve the AnonymousContent instance, use the `content` getter.
 | |
|  */
 | |
| 
 | |
| /* 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
 | |
| 
 | |
|   A pointerdown goes from crosshairs to dragging.
 | |
|   A pointerup goes from dragging to selected
 | |
|   A click outside of the selection goes from selected to crosshairs
 | |
|   A pointerdown on one of the draggers goes from selected to resizing
 | |
| 
 | |
|   */
 | |
| 
 | |
| import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| XPCOMUtils.defineLazyGetter(lazy, "overlayLocalization", () => {
 | |
|   return new Localization(["browser/screenshotsOverlay.ftl"], true);
 | |
| });
 | |
| 
 | |
| const STYLESHEET_URL =
 | |
|   "chrome://browser/content/screenshots/overlay/overlay.css";
 | |
| 
 | |
| // 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:
 | |
| let MAX_DETECT_HEIGHT = 700;
 | |
| let MAX_DETECT_WIDTH = 1000;
 | |
| 
 | |
| const REGION_CHANGE_THRESHOLD = 5;
 | |
| 
 | |
| const doNotAutoselectTags = {
 | |
|   H1: true,
 | |
|   H2: true,
 | |
|   H3: true,
 | |
|   H4: true,
 | |
|   H5: true,
 | |
|   H6: true,
 | |
| };
 | |
| 
 | |
| class AnonymousContentOverlay {
 | |
|   constructor(contentDocument, screenshotsChild) {
 | |
|     this.listeners = new Map();
 | |
|     this.elements = new Map();
 | |
| 
 | |
|     this.screenshotsChild = screenshotsChild;
 | |
| 
 | |
|     this.contentDocument = contentDocument;
 | |
|     // aliased for easier diffs/maintenance of the event management code borrowed from devtools highlighters
 | |
|     this.pageListenerTarget = contentDocument.ownerGlobal;
 | |
| 
 | |
|     this.overlayFragment = null;
 | |
| 
 | |
|     this.overlayId = "screenshots-overlay-container";
 | |
|     this.previewId = "preview-container";
 | |
|     this.selectionId = "selection-container";
 | |
|     this.hoverBoxId = "hover-highlight";
 | |
| 
 | |
|     this._initialized = false;
 | |
| 
 | |
|     this.moverIds = [
 | |
|       "mover-left",
 | |
|       "mover-top",
 | |
|       "mover-right",
 | |
|       "mover-bottom",
 | |
|       "mover-topLeft",
 | |
|       "mover-topRight",
 | |
|       "mover-bottomLeft",
 | |
|       "mover-bottomRight",
 | |
|     ];
 | |
|   }
 | |
|   get content() {
 | |
|     if (!this._content || Cu.isDeadWrapper(this._content)) {
 | |
|       return null;
 | |
|     }
 | |
|     return this._content;
 | |
|   }
 | |
|   async initialize() {
 | |
|     if (this._initialized) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let document = this.contentDocument;
 | |
|     let window = document.ownerGlobal;
 | |
| 
 | |
|     // Inject stylesheet
 | |
|     if (!this.overlayFragment) {
 | |
|       try {
 | |
|         window.windowUtils.loadSheetUsingURIString(
 | |
|           STYLESHEET_URL,
 | |
|           window.windowUtils.AGENT_SHEET
 | |
|         );
 | |
|       } catch {
 | |
|         // The method fails if the url is already loaded.
 | |
|       }
 | |
|       // Inject markup for the overlay UI
 | |
|       this.overlayFragment = this.buildOverlay();
 | |
|     }
 | |
| 
 | |
|     this._content = document.insertAnonymousContent(
 | |
|       this.overlayFragment.children[0]
 | |
|     );
 | |
| 
 | |
|     this.addEventListeners();
 | |
| 
 | |
|     this.hoverElementBox = new HoverElementBox(
 | |
|       this.hoverBoxId,
 | |
|       this.content,
 | |
|       document
 | |
|     );
 | |
| 
 | |
|     this.previewLayer = new PreviewLayer(this.previewId, this.content);
 | |
|     this.selectionLayer = new SelectionLayer(
 | |
|       this.selectionId,
 | |
|       this.content,
 | |
|       this.hoverElementBox
 | |
|     );
 | |
| 
 | |
|     this.screenshotsContainer = new ScreenshotsContainerLayer(
 | |
|       this.overlayId,
 | |
|       this.content,
 | |
|       this.previewLayer,
 | |
|       this.selectionLayer
 | |
|     );
 | |
| 
 | |
|     this.stateHandler = new StateHandler(
 | |
|       this.screenshotsContainer,
 | |
|       this.screenshotsChild
 | |
|     );
 | |
| 
 | |
|     this.screenshotsContainer.updateSize(window);
 | |
| 
 | |
|     this.stateHandler.setState("crosshairs");
 | |
| 
 | |
|     this._initialized = true;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * The Anonymous Content doesn't shrink when the window is resized so we need
 | |
|    * to find the largest element that isn't the Anonymous Content and we will
 | |
|    * use that width and height.
 | |
|    * Otherwise we will fallback to the documentElement scroll width and height
 | |
|    * @param eventType If "resize", we called this from a resize event so we will
 | |
|    *  try shifting the SelectionBox.
 | |
|    *  If "scroll", we called this from a scroll event so we will redraw the buttons
 | |
|    */
 | |
|   updateScreenshotsSize(eventType) {
 | |
|     this.stateHandler.updateScreenshotsContainerSize(
 | |
|       this.contentDocument.ownerGlobal,
 | |
|       eventType
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Add required event listeners to the overlay
 | |
|    */
 | |
|   addEventListeners() {
 | |
|     let cancelScreenshotsFunciton = () => {
 | |
|       this.screenshotsChild.requestCancelScreenshot("overlay_cancel");
 | |
|     };
 | |
|     this.addEventListenerForElement(
 | |
|       "screenshots-cancel-button",
 | |
|       "click",
 | |
|       cancelScreenshotsFunciton
 | |
|     );
 | |
|     this.addEventListenerForElement(
 | |
|       "cancel",
 | |
|       "click",
 | |
|       cancelScreenshotsFunciton
 | |
|     );
 | |
|     this.addEventListenerForElement("copy", "click", (event, targetId) => {
 | |
|       this.screenshotsChild.requestCopyScreenshot(
 | |
|         this.screenshotsContainer.getSelectionLayerBoxDimensions()
 | |
|       );
 | |
|     });
 | |
|     this.addEventListenerForElement("download", "click", (event, targetId) => {
 | |
|       this.screenshotsChild.requestDownloadScreenshot(
 | |
|         this.screenshotsContainer.getSelectionLayerBoxDimensions()
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     // The pointerdown event is added to the selection buttons to prevent the
 | |
|     // pointerdown event from occurring on the "screenshots-overlay-container"
 | |
|     this.addEventListenerForElement(
 | |
|       "cancel",
 | |
|       "pointerdown",
 | |
|       (event, targetId) => {
 | |
|         event.stopPropagation();
 | |
|       }
 | |
|     );
 | |
|     this.addEventListenerForElement(
 | |
|       "copy",
 | |
|       "pointerdown",
 | |
|       (event, targetId) => {
 | |
|         event.stopPropagation();
 | |
|       }
 | |
|     );
 | |
|     this.addEventListenerForElement(
 | |
|       "download",
 | |
|       "pointerdown",
 | |
|       (event, targetId) => {
 | |
|         event.stopPropagation();
 | |
|       }
 | |
|     );
 | |
| 
 | |
|     this.addEventListenerForElement(
 | |
|       this.overlayId,
 | |
|       "pointerdown",
 | |
|       (event, targetId) => {
 | |
|         this.dragStart(event, targetId);
 | |
|       }
 | |
|     );
 | |
|     this.addEventListenerForElement(
 | |
|       this.overlayId,
 | |
|       "pointerup",
 | |
|       (event, targetId) => {
 | |
|         this.dragEnd(event, targetId);
 | |
|       }
 | |
|     );
 | |
|     this.addEventListenerForElement(
 | |
|       this.overlayId,
 | |
|       "pointermove",
 | |
|       (event, targetId) => {
 | |
|         this.drag(event, targetId);
 | |
|       }
 | |
|     );
 | |
| 
 | |
|     for (let id of this.moverIds.concat(["highlight"])) {
 | |
|       this.addEventListenerForElement(id, "pointerdown", (event, targetId) => {
 | |
|         this.dragStart(event, targetId);
 | |
|       });
 | |
|       this.addEventListenerForElement(id, "pointerup", (event, targetId) => {
 | |
|         this.dragEnd(event, targetId);
 | |
|       });
 | |
|       this.addEventListenerForElement(id, "pointermove", (event, targetId) => {
 | |
|         this.drag(event, targetId);
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Removes all event listeners and removes the overlay from the Anonymous Content
 | |
|    */
 | |
|   tearDown() {
 | |
|     if (this._content) {
 | |
|       this._removeAllListeners();
 | |
|       try {
 | |
|         this.contentDocument.removeAnonymousContent(this._content);
 | |
|       } catch (e) {
 | |
|         // If the current window isn't the one the content was inserted into, this
 | |
|         // will fail, but that's fine.
 | |
|       }
 | |
|     }
 | |
|     this._initialized = false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Creates the document fragment that will be added to the Anonymous Content
 | |
|    * @returns document fragment that can be injected into the Anonymous Content
 | |
|    */
 | |
|   buildOverlay() {
 | |
|     let [
 | |
|       cancel,
 | |
|       instructions,
 | |
|       download,
 | |
|       copy,
 | |
|     ] = lazy.overlayLocalization.formatMessagesSync([
 | |
|       { id: "screenshots-overlay-cancel-button" },
 | |
|       { id: "screenshots-overlay-instructions" },
 | |
|       { id: "screenshots-overlay-download-button" },
 | |
|       { id: "screenshots-overlay-copy-button" },
 | |
|     ]);
 | |
| 
 | |
|     const htmlString = `
 | |
|     <div id="screenshots-component">
 | |
|       <div id="${this.overlayId}">
 | |
|         <div id="${this.previewId}">
 | |
|           <div class="fixed-container">
 | |
|             <div class="face-container">
 | |
|               <div class="eye left"><div id="left-eye" class="eyeball"></div></div>
 | |
|               <div class="eye right"><div id="right-eye" class="eyeball"></div></div>
 | |
|               <div class="face"></div>
 | |
|             </div>
 | |
|             <div class="preview-instructions">${instructions.value}</div>
 | |
|             <button class="cancel-shot" id="screenshots-cancel-button">${cancel.value}</button>
 | |
|           </div>
 | |
|         </div>
 | |
|         <div id="${this.hoverBoxId}"></div>
 | |
|         <div id="${this.selectionId}" style="display:none;">
 | |
|           <div id="bgTop" class="bghighlight" style="display:none;"></div>
 | |
|           <div id="bgBottom" class="bghighlight" style="display:none;"></div>
 | |
|           <div id="bgLeft" class="bghighlight" style="display:none;"></div>
 | |
|           <div id="bgRight" class="bghighlight" style="display:none;"></div>
 | |
|           <div id="highlight" class="highlight" style="display:none;">
 | |
|             <div id="mover-topLeft" class="mover-target direction-topLeft">
 | |
|               <div class="mover"></div>
 | |
|             </div>
 | |
|             <div id="mover-top" class="mover-target direction-top">
 | |
|               <div class="mover"></div>
 | |
|             </div>
 | |
|             <div id="mover-topRight" class="mover-target direction-topRight">
 | |
|               <div class="mover"></div>
 | |
|             </div>
 | |
|             <div id="mover-left" class="mover-target direction-left">
 | |
|               <div class="mover"></div>
 | |
|             </div>
 | |
|             <div id="mover-right" class="mover-target direction-right">
 | |
|               <div class="mover"></div>
 | |
|             </div>
 | |
|             <div id="mover-bottomLeft" class="mover-target direction-bottomLeft">
 | |
|               <div class="mover"></div>
 | |
|             </div>
 | |
|             <div id="mover-bottom" class="mover-target direction-bottom">
 | |
|               <div class="mover"></div>
 | |
|             </div>
 | |
|             <div id="mover-bottomRight" class="mover-target direction-bottomRight">
 | |
|               <div class="mover"></div>
 | |
|             </div>
 | |
|           </div>
 | |
|           <div id="buttons" style="display:none;">
 | |
|             <button id="cancel" class="screenshots-button" title="${cancel.value}" aria-label="${cancel.value}"><img/></button>
 | |
|             <button id="copy" class="screenshots-button" title="${copy.value}" aria-label="${copy.value}"><img/>${copy.value}</button>
 | |
|             <button id="download" class="screenshots-button" title="${download.value}" aria-label="${download.value}"><img/>${download.value}</button>
 | |
|           </div>
 | |
|         </div>
 | |
|       </div>
 | |
|     </div>`;
 | |
| 
 | |
|     const parser = new this.contentDocument.ownerGlobal.DOMParser();
 | |
|     const tmpDoc = parser.parseFromSafeString(htmlString, "text/html");
 | |
|     const fragment = this.contentDocument.createDocumentFragment();
 | |
| 
 | |
|     fragment.appendChild(tmpDoc.body.children[0]);
 | |
|     return fragment;
 | |
|   }
 | |
| 
 | |
|   // The event tooling is borrowed directly from devtools' highlighters (CanvasFrameAnonymousContentHelper)
 | |
|   /**
 | |
|    * Add an event listener to one of the elements inserted in the canvasFrame
 | |
|    * native anonymous container.
 | |
|    * Like other methods in this helper, this requires the ID of the element to
 | |
|    * be passed in.
 | |
|    *
 | |
|    * Note that if the content page navigates, the event listeners won't be
 | |
|    * added again.
 | |
|    *
 | |
|    * Also note that unlike traditional DOM events, the events handled by
 | |
|    * listeners added here will propagate through the document only through
 | |
|    * bubbling phase, so the useCapture parameter isn't supported.
 | |
|    * It is possible however to call e.stopPropagation() to stop the bubbling.
 | |
|    *
 | |
|    * IMPORTANT: the chrome-only canvasFrame insertion API takes great care of
 | |
|    * not leaking references to inserted elements to chrome JS code. That's
 | |
|    * because otherwise, chrome JS code could freely modify native anon elements
 | |
|    * inside the canvasFrame and probably change things that are assumed not to
 | |
|    * change by the C++ code managing this frame.
 | |
|    * See https://wiki.mozilla.org/DevTools/Highlighter#The_AnonymousContent_API
 | |
|    * Unfortunately, the inserted nodes are still available via
 | |
|    * event.originalTarget, and that's what the event handler here uses to check
 | |
|    * that the event actually occured on the right element, but that also means
 | |
|    * consumers of this code would be able to access the inserted elements.
 | |
|    * Therefore, the originalTarget property will be nullified before the event
 | |
|    * is passed to your handler.
 | |
|    *
 | |
|    * IMPL DETAIL: A single event listener is added per event types only, at
 | |
|    * browser level and if the event originalTarget is found to have the provided
 | |
|    * ID, the callback is executed (and then IDs of parent nodes of the
 | |
|    * originalTarget are checked too).
 | |
|    *
 | |
|    * @param {String} id
 | |
|    * @param {String} type
 | |
|    * @param {Function} handler
 | |
|    */
 | |
|   addEventListenerForElement(id, type, handler) {
 | |
|     if (typeof id !== "string") {
 | |
|       throw new Error(
 | |
|         "Expected a string ID in addEventListenerForElement but got: " + id
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     // If no one is listening for this type of event yet, add one listener.
 | |
|     if (!this.listeners.has(type)) {
 | |
|       const target = this.pageListenerTarget;
 | |
|       target.addEventListener(type, this, true);
 | |
|       // Each type entry in the map is a map of ids:handlers.
 | |
|       this.listeners.set(type, new Map());
 | |
|     }
 | |
| 
 | |
|     const listeners = this.listeners.get(type);
 | |
|     listeners.set(id, handler);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Remove an event listener from one of the elements inserted in the
 | |
|    * canvasFrame native anonymous container.
 | |
|    * @param {String} id
 | |
|    * @param {String} type
 | |
|    */
 | |
|   removeEventListenerForElement(id, type) {
 | |
|     const listeners = this.listeners.get(type);
 | |
|     if (!listeners) {
 | |
|       return;
 | |
|     }
 | |
|     listeners.delete(id);
 | |
| 
 | |
|     // If no one is listening for event type anymore, remove the listener.
 | |
|     if (!listeners.size) {
 | |
|       const target = this.pageListenerTarget;
 | |
|       target.removeEventListener(type, this, true);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   handleEvent(event) {
 | |
|     const listeners = this.listeners.get(event.type);
 | |
|     if (!listeners) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Hide the originalTarget property to avoid exposing references to native
 | |
|     // anonymous elements. See addEventListenerForElement's comment.
 | |
|     let isPropagationStopped = false;
 | |
|     const eventProxy = new Proxy(event, {
 | |
|       get: (obj, name) => {
 | |
|         if (name === "originalTarget") {
 | |
|           return null;
 | |
|         } else if (name === "stopPropagation") {
 | |
|           return () => {
 | |
|             isPropagationStopped = true;
 | |
|           };
 | |
|         }
 | |
|         return obj[name];
 | |
|       },
 | |
|     });
 | |
| 
 | |
|     // Start at originalTarget, bubble through ancestors and call handlers when
 | |
|     // needed.
 | |
|     let node = event.originalTarget;
 | |
|     while (node) {
 | |
|       let nodeId = node.id;
 | |
|       if (nodeId) {
 | |
|         const handler = listeners.get(node.id);
 | |
|         if (handler) {
 | |
|           handler(eventProxy, nodeId);
 | |
|           if (isPropagationStopped) {
 | |
|             break;
 | |
|           }
 | |
|         }
 | |
|         if (nodeId == this.overlayId) {
 | |
|           break;
 | |
|         }
 | |
|       }
 | |
|       node = node.parentNode;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _removeAllListeners() {
 | |
|     if (this.pageListenerTarget) {
 | |
|       const target = this.pageListenerTarget;
 | |
|       for (const [type] of this.listeners) {
 | |
|         target.removeEventListener(type, this, true);
 | |
|       }
 | |
|     }
 | |
|     this.listeners.clear();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Pass the pointer down event to the state handler
 | |
|    * @param event The pointer down event
 | |
|    * @param targetId The target element id
 | |
|    */
 | |
|   dragStart(event, targetId) {
 | |
|     this.stateHandler.dragStart(event, targetId);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Pass the pointer move event to the state handler
 | |
|    * @param event The pointer move event
 | |
|    * @param targetId The target element id
 | |
|    */
 | |
|   drag(event, targetId) {
 | |
|     this.stateHandler.drag(event, targetId);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Pass the pointer up event to the state handler
 | |
|    * @param event The pointer up event
 | |
|    * @param targetId The target element id
 | |
|    */
 | |
|   dragEnd(event, targetId) {
 | |
|     this.stateHandler.dragEnd(event);
 | |
|   }
 | |
| }
 | |
| 
 | |
| export var ScreenshotsOverlayChild = {
 | |
|   AnonymousContentOverlay,
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * The StateHandler class handles the state of the overlay
 | |
|  */
 | |
| class StateHandler {
 | |
|   #state;
 | |
|   #lastBox;
 | |
|   #moverId;
 | |
|   #lastX;
 | |
|   #lastY;
 | |
|   #screenshotsContainer;
 | |
|   #screenshotsChild;
 | |
|   #previousDimensions;
 | |
| 
 | |
|   constructor(screenshotsContainer, screenshotsChild) {
 | |
|     this.#state = "crosshairs";
 | |
|     this.#lastBox = {};
 | |
| 
 | |
|     this.#screenshotsContainer = screenshotsContainer;
 | |
|     this.#screenshotsChild = screenshotsChild;
 | |
|   }
 | |
| 
 | |
|   setState(newState) {
 | |
|     if (this.#state === "selected" && newState === "crosshairs") {
 | |
|       this.#screenshotsChild.recordTelemetryEvent(
 | |
|         "started",
 | |
|         "overlay_retry",
 | |
|         {}
 | |
|       );
 | |
|     }
 | |
|     this.#state = newState;
 | |
|     this.start();
 | |
|   }
 | |
| 
 | |
|   getState() {
 | |
|     return this.#state;
 | |
|   }
 | |
| 
 | |
|   getHoverElementBoxRect() {
 | |
|     return this.#screenshotsContainer.hoverElementBoxRect;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * At the start of the some states we need to perform some actions
 | |
|    */
 | |
|   start() {
 | |
|     switch (this.#state) {
 | |
|       case "crosshairs": {
 | |
|         this.crosshairsStart();
 | |
|         break;
 | |
|       }
 | |
|       case "draggingReady": {
 | |
|         this.draggingReadyStart();
 | |
|         break;
 | |
|       }
 | |
|       case "dragging": {
 | |
|         this.draggingStart();
 | |
|         break;
 | |
|       }
 | |
|       case "selected": {
 | |
|         this.selectedStart();
 | |
|         break;
 | |
|       }
 | |
|       case "resizing": {
 | |
|         this.resizingStart();
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the x and y coordinates of the event
 | |
|    * @param event The mouse or touch event
 | |
|    * @returns object containing the x and y coordinates of the mouse
 | |
|    */
 | |
|   getCoordinates(event) {
 | |
|     const { clientX: viewX, clientY: viewY, pageX, pageY } = event;
 | |
| 
 | |
|     MAX_DETECT_HEIGHT = Math.max(event.target.clientHeight + 100, 700);
 | |
|     MAX_DETECT_WIDTH = Math.max(event.target.clientWidth + 100, 1000);
 | |
| 
 | |
|     return { viewX, viewY, pageX, pageY };
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handles the mousedown/touchstart event depending on the state
 | |
|    * @param event The mousedown or touchstart event
 | |
|    * @param targetId The id of the event target
 | |
|    */
 | |
|   dragStart(event, targetId) {
 | |
|     const { pageX, pageY } = this.getCoordinates(event);
 | |
| 
 | |
|     switch (this.#state) {
 | |
|       case "crosshairs": {
 | |
|         this.crosshairsDragStart(pageX, pageY);
 | |
|         break;
 | |
|       }
 | |
|       case "selected": {
 | |
|         this.selectedDragStart(pageX, pageY, targetId);
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handles the move event depending on the state
 | |
|    * @param event The mousemove or touchmove event
 | |
|    * @param targetId The id of the event target
 | |
|    */
 | |
|   drag(event, targetId) {
 | |
|     const { pageX, pageY, viewX, viewY } = this.getCoordinates(event);
 | |
| 
 | |
|     switch (this.#state) {
 | |
|       case "crosshairs": {
 | |
|         this.crosshairsMove(pageX, pageY, viewX, viewY, targetId);
 | |
|         break;
 | |
|       }
 | |
|       case "draggingReady": {
 | |
|         this.draggingReadyDrag(pageX, pageY);
 | |
|         break;
 | |
|       }
 | |
|       case "dragging": {
 | |
|         this.draggingDrag(pageX, pageY);
 | |
|         break;
 | |
|       }
 | |
|       case "resizing": {
 | |
|         this.resizingDrag(pageX, pageY);
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handles the move event depending on the state
 | |
|    * @param event The mouseup event
 | |
|    * @param targetId The id of the event target
 | |
|    */
 | |
|   dragEnd(event, targetId) {
 | |
|     const { pageX, pageY, viewX, viewY } = this.getCoordinates(event);
 | |
| 
 | |
|     switch (this.#state) {
 | |
|       case "draggingReady": {
 | |
|         this.draggingReadyDragEnd(pageX - viewX, pageY - viewY);
 | |
|         break;
 | |
|       }
 | |
|       case "dragging": {
 | |
|         this.draggingDragEnd(pageX, pageY, targetId);
 | |
|         break;
 | |
|       }
 | |
|       case "resizing": {
 | |
|         this.resizingDragEnd(pageX, pageY, targetId);
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Hide the box and highlighter and show the overlay at the start of crosshairs state
 | |
|    */
 | |
|   crosshairsStart() {
 | |
|     this.#screenshotsContainer.hideSelectionLayer();
 | |
|     this.#screenshotsContainer.showPreviewLayer();
 | |
|     this.#screenshotsChild.showPanel();
 | |
|     this.#previousDimensions = null;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    *
 | |
|    */
 | |
|   draggingReadyStart() {
 | |
|     this.#screenshotsChild.hidePanel();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Hide the overlay and draw the box at the start of dragging state
 | |
|    */
 | |
|   draggingStart() {
 | |
|     this.#screenshotsContainer.hidePreviewLayer();
 | |
|     this.#screenshotsContainer.hideButtonsLayer();
 | |
|     this.#screenshotsContainer.drawSelectionBox();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Show the buttons at the start of the selected state
 | |
|    */
 | |
|   selectedStart() {
 | |
|     this.#screenshotsContainer.drawButtonsLayer();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Hide the buttons and store width and height of box at the start of the resizing state
 | |
|    */
 | |
|   resizingStart() {
 | |
|     this.#screenshotsContainer.hideButtonsLayer();
 | |
|     let {
 | |
|       width,
 | |
|       height,
 | |
|     } = this.#screenshotsContainer.getSelectionLayerBoxDimensions();
 | |
|     this.#lastBox = {
 | |
|       width,
 | |
|       height,
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Set the initial box coordinates and set the state to "draggingReady"
 | |
|    * @param clientX x coordinate
 | |
|    * @param clientY y coordinate
 | |
|    */
 | |
|   crosshairsDragStart(clientX, clientY) {
 | |
|     this.#screenshotsContainer.setSelectionBoxDimensions({
 | |
|       left: clientX,
 | |
|       top: clientY,
 | |
|       right: clientX,
 | |
|       bottom: clientY,
 | |
|     });
 | |
| 
 | |
|     this.setState("draggingReady");
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * If the background is clicked we set the state to crosshairs
 | |
|    * otherwise set the state to resizing
 | |
|    * @param clientX x coordinate
 | |
|    * @param clientY y coordinate
 | |
|    * @param targetId The id of the event target
 | |
|    */
 | |
|   selectedDragStart(clientX, clientY, targetId) {
 | |
|     if (targetId === this.#screenshotsContainer.id) {
 | |
|       this.setState("crosshairs");
 | |
|       return;
 | |
|     }
 | |
|     this.#moverId = targetId;
 | |
|     this.#lastX = clientX;
 | |
|     this.#lastY = clientY;
 | |
| 
 | |
|     this.setState("resizing");
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handles the pointer move for the crosshairs state
 | |
|    * @param pageX x pointer position
 | |
|    * @param pageY y pointer position
 | |
|    * @param viewX x pointer position in viewport
 | |
|    * @param viewY y pointer position in viewport
 | |
|    * @param targetId The id of the target element
 | |
|    */
 | |
|   crosshairsMove(pageX, pageY, viewX, viewY, targetId) {
 | |
|     this.#screenshotsContainer.drawPreviewEyes(pageX, pageY);
 | |
| 
 | |
|     this.#screenshotsContainer.handleElementHover(viewX, viewY, targetId);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Set the bottom and right coordinates of the box and draw the box
 | |
|    * @param clientX x coordinate
 | |
|    * @param clientY y coordinate
 | |
|    */
 | |
|   draggingDrag(clientX, clientY) {
 | |
|     this.#screenshotsContainer.setSelectionBoxDimensions({
 | |
|       right: clientX,
 | |
|       bottom: clientY,
 | |
|     });
 | |
| 
 | |
|     this.#screenshotsContainer.drawSelectionBox();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * If the mouse has moved at least 40 pixels then set the state to "dragging"
 | |
|    * @param clientX x coordinate
 | |
|    * @param clientY y coordinate
 | |
|    */
 | |
|   draggingReadyDrag(clientX, clientY) {
 | |
|     this.#screenshotsContainer.setSelectionBoxDimensions({
 | |
|       right: clientX,
 | |
|       bottom: clientY,
 | |
|     });
 | |
| 
 | |
|     if (this.#screenshotsContainer.selectionBoxDistance() > 40) {
 | |
|       this.setState("dragging");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Depending on what mover was selected we will resize the box accordingly
 | |
|    * @param clientX x coordinate
 | |
|    * @param clientY y coordinate
 | |
|    */
 | |
|   resizingDrag(clientX, clientY) {
 | |
|     switch (this.#moverId) {
 | |
|       case "mover-topLeft": {
 | |
|         this.#screenshotsContainer.setSelectionBoxDimensions({
 | |
|           left: clientX,
 | |
|           top: clientY,
 | |
|         });
 | |
|         break;
 | |
|       }
 | |
|       case "mover-top": {
 | |
|         this.#screenshotsContainer.setSelectionBoxDimensions({ top: clientY });
 | |
|         break;
 | |
|       }
 | |
|       case "mover-topRight": {
 | |
|         this.#screenshotsContainer.setSelectionBoxDimensions({
 | |
|           top: clientY,
 | |
|           right: clientX,
 | |
|         });
 | |
|         break;
 | |
|       }
 | |
|       case "mover-right": {
 | |
|         this.#screenshotsContainer.setSelectionBoxDimensions({
 | |
|           right: clientX,
 | |
|         });
 | |
|         break;
 | |
|       }
 | |
|       case "mover-bottomRight": {
 | |
|         this.#screenshotsContainer.setSelectionBoxDimensions({
 | |
|           right: clientX,
 | |
|           bottom: clientY,
 | |
|         });
 | |
|         break;
 | |
|       }
 | |
|       case "mover-bottom": {
 | |
|         this.#screenshotsContainer.setSelectionBoxDimensions({
 | |
|           bottom: clientY,
 | |
|         });
 | |
|         break;
 | |
|       }
 | |
|       case "mover-bottomLeft": {
 | |
|         this.#screenshotsContainer.setSelectionBoxDimensions({
 | |
|           left: clientX,
 | |
|           bottom: clientY,
 | |
|         });
 | |
|         break;
 | |
|       }
 | |
|       case "mover-left": {
 | |
|         this.#screenshotsContainer.setSelectionBoxDimensions({ left: clientX });
 | |
|         break;
 | |
|       }
 | |
|       case "highlight": {
 | |
|         let lastBox = this.#lastBox;
 | |
|         let diffX = this.#lastX - clientX;
 | |
|         let diffY = this.#lastY - clientY;
 | |
| 
 | |
|         let newLeft;
 | |
|         let newRight;
 | |
|         let newTop;
 | |
|         let newBottom;
 | |
| 
 | |
|         // Unpack SelectionBox dimensions to use here
 | |
|         let {
 | |
|           boxLeft,
 | |
|           boxTop,
 | |
|           boxRight,
 | |
|           boxBottom,
 | |
|           boxWidth,
 | |
|           boxHeight,
 | |
|           scrollWidth,
 | |
|           scrollHeight,
 | |
|         } = this.#screenshotsContainer.getSelectionLayerDimensions();
 | |
| 
 | |
|         // wait until all 4 if elses have completed before setting box dimensions
 | |
|         if (boxWidth <= lastBox.width && boxLeft === 0) {
 | |
|           newLeft = boxRight - lastBox.width;
 | |
|         } else {
 | |
|           newLeft = boxLeft;
 | |
|         }
 | |
| 
 | |
|         if (boxWidth <= lastBox.width && boxRight === scrollWidth) {
 | |
|           newRight = boxLeft + lastBox.width;
 | |
|         } else {
 | |
|           newRight = boxRight;
 | |
|         }
 | |
| 
 | |
|         if (boxHeight <= lastBox.height && boxTop === 0) {
 | |
|           newTop = boxBottom - lastBox.height;
 | |
|         } else {
 | |
|           newTop = boxTop;
 | |
|         }
 | |
| 
 | |
|         if (boxHeight <= lastBox.height && boxBottom === scrollHeight) {
 | |
|           newBottom = boxTop + lastBox.height;
 | |
|         } else {
 | |
|           newBottom = boxBottom;
 | |
|         }
 | |
| 
 | |
|         this.#screenshotsContainer.setSelectionBoxDimensions({
 | |
|           left: newLeft - diffX,
 | |
|           top: newTop - diffY,
 | |
|           right: newRight - diffX,
 | |
|           bottom: newBottom - diffY,
 | |
|         });
 | |
| 
 | |
|         this.#lastX = clientX;
 | |
|         this.#lastY = clientY;
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
|     this.#screenshotsContainer.drawSelectionBox();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Draw the selection box from the hover element box if it exists
 | |
|    * Else set the state to "crosshairs"
 | |
|    */
 | |
|   draggingReadyDragEnd(scrollX, scrollY) {
 | |
|     if (this.#screenshotsContainer.hoverElementBoxRect) {
 | |
|       this.#screenshotsContainer.hidePreviewLayer();
 | |
|       this.#screenshotsContainer.updateSelectionBoxFromRect(scrollX, scrollY);
 | |
|       this.#screenshotsContainer.drawSelectionBox();
 | |
|       this.setState("selected");
 | |
|       this.#screenshotsChild.recordTelemetryEvent("selected", "element", {});
 | |
|     } else {
 | |
|       this.setState("crosshairs");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Draw the box one last time and set the state to "selected"
 | |
|    * @param clientX x coordinate
 | |
|    * @param clientY y coordinate
 | |
|    */
 | |
|   draggingDragEnd(clientX, clientY) {
 | |
|     this.#screenshotsContainer.setSelectionBoxDimensions({
 | |
|       right: clientX,
 | |
|       bottom: clientY,
 | |
|     });
 | |
|     this.#screenshotsContainer.sortSelectionLayerBoxCoords();
 | |
|     this.setState("selected");
 | |
| 
 | |
|     let {
 | |
|       width,
 | |
|       height,
 | |
|     } = this.#screenshotsContainer.getSelectionLayerBoxDimensions();
 | |
| 
 | |
|     if (
 | |
|       !this.#previousDimensions ||
 | |
|       (Math.abs(this.#previousDimensions.width - width) >
 | |
|         REGION_CHANGE_THRESHOLD &&
 | |
|         Math.abs(this.#previousDimensions.height - height) >
 | |
|           REGION_CHANGE_THRESHOLD)
 | |
|     ) {
 | |
|       this.#screenshotsChild.recordTelemetryEvent(
 | |
|         "selected",
 | |
|         "region_selection",
 | |
|         {}
 | |
|       );
 | |
|     }
 | |
|     this.#previousDimensions = { width, height };
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Draw the box one last time and set the state to "selected"
 | |
|    * @param clientX x coordinate
 | |
|    * @param clientY y coordinate
 | |
|    */
 | |
|   resizingDragEnd(clientX, clientY, targetId) {
 | |
|     this.resizingDrag(clientX, clientY, targetId);
 | |
|     this.#screenshotsContainer.sortSelectionLayerBoxCoords();
 | |
|     this.setState("selected");
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * The page was resized or scrolled. We need to update the
 | |
|    * ScreenshotsContainer size so we don't draw outside the window bounds
 | |
|    * If the current state is "selected" and this was called from a resize event
 | |
|    * then we need to maybe shift the SelectionBox
 | |
|    * @param win The window object of the page
 | |
|    * @param eventType If this was called from a resize event
 | |
|    */
 | |
|   updateScreenshotsContainerSize(win, eventType) {
 | |
|     this.#screenshotsContainer.updateSize(win);
 | |
| 
 | |
|     if (this.#state === "selected" && eventType === "resize") {
 | |
|       this.#screenshotsContainer.shiftSelectionLayerBox();
 | |
|     } else if (this.#state && eventType === "scroll") {
 | |
|       this.#screenshotsContainer.drawButtonsLayer();
 | |
|       if (this.#state === "crosshairs") {
 | |
|         this.#screenshotsContainer.handleElementScroll();
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| class AnonLayer {
 | |
|   id;
 | |
|   content;
 | |
| 
 | |
|   constructor(id, content) {
 | |
|     this.id = id;
 | |
|     this.content = content;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Show element with id this.id
 | |
|    */
 | |
|   show() {
 | |
|     this.content.removeAttributeForElement(this.id, "style");
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Hide element with id this.id
 | |
|    */
 | |
|   hide() {
 | |
|     this.content.setAttributeForElement(this.id, "style", "display:none;");
 | |
|   }
 | |
| }
 | |
| 
 | |
| class HoverElementBox extends AnonLayer {
 | |
|   #document;
 | |
|   #rect;
 | |
|   #lastX;
 | |
|   #lastY;
 | |
| 
 | |
|   constructor(id, content, document) {
 | |
|     super(id, content);
 | |
| 
 | |
|     this.#document = document;
 | |
|   }
 | |
| 
 | |
|   get rect() {
 | |
|     return this.#rect;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Draws the hover box over an element from the given rect
 | |
|    * @param rect The rect to draw the hover element box
 | |
|    */
 | |
|   drawHoverBox(rect) {
 | |
|     if (!rect) {
 | |
|       this.hide();
 | |
|     } else {
 | |
|       let maxHeight = this.selectionLayer.scrollHeight;
 | |
|       let maxWidth = this.selectionLayer.scrollWidth;
 | |
|       let top = this.#document.documentElement.scrollTop + rect.top;
 | |
|       top = top > 0 ? top : 0;
 | |
|       let left = this.#document.documentElement.scrollLeft + rect.left;
 | |
|       left = left > 0 ? left : 0;
 | |
|       let height =
 | |
|         rect.top + rect.height > maxHeight ? maxHeight - rect.top : rect.height;
 | |
|       let width =
 | |
|         rect.left + rect.width > maxWidth ? maxWidth - rect.left : rect.width;
 | |
| 
 | |
|       this.content.setAttributeForElement(
 | |
|         this.id,
 | |
|         "style",
 | |
|         `top:${top}px;left:${left}px;height:${height}px;width:${width}px;`
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handles when the user moves the mouse over an element
 | |
|    * @param viewX The viewport x coordinate
 | |
|    * @param viewY The viewport y coordinate
 | |
|    * @param targetId The target element id
 | |
|    */
 | |
|   handleElementHover(viewX, viewY, targetId) {
 | |
|     if (targetId === "screenshots-overlay-container") {
 | |
|       let ele = this.getElementFromPoint(viewX, viewY);
 | |
| 
 | |
|       if (this.cachedEle && this.cachedEle === ele) {
 | |
|         // Still hovering over the same element
 | |
|         return;
 | |
|       }
 | |
|       this.cachedEle = ele;
 | |
| 
 | |
|       this.getBestRectForElement(ele);
 | |
| 
 | |
|       this.#lastX = viewX;
 | |
|       this.#lastY = viewY;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handles moving the rect when the user has scrolled but not moved the mouse
 | |
|    * It uses the last x and y viewport coordinates to find the new element at the mouse position
 | |
|    */
 | |
|   handleElementScroll() {
 | |
|     if (this.#lastX && this.#lastY) {
 | |
|       this.cachedEle = null;
 | |
|       this.handleElementHover(
 | |
|         this.#lastX,
 | |
|         this.#lastY,
 | |
|         "screenshots-overlay-container"
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Finds an element for the given coordinates within the viewport
 | |
|    * @param x The viewport x coordinate
 | |
|    * @param y The viewport y coordinate
 | |
|    * @returns An element location at the given coordinates
 | |
|    */
 | |
|   getElementFromPoint(x, y) {
 | |
|     this.setPointerEventsNone();
 | |
|     let ele;
 | |
|     try {
 | |
|       ele = this.#document.elementFromPoint(x, y);
 | |
|     } finally {
 | |
|       this.resetPointerEvents();
 | |
|     }
 | |
| 
 | |
|     return ele;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Gets the rect for an element if getBoundingClientRect exists
 | |
|    * @param ele The element to get the rect from
 | |
|    * @returns The bounding client rect of the element or null
 | |
|    */
 | |
|   getBoundingClientRect(ele) {
 | |
|     if (!ele.getBoundingClientRect) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     return ele.getBoundingClientRect();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * This function takes an element and finds a suitable rect to draw the hover box on
 | |
|    * @param ele The element to find a suitale rect of
 | |
|    */
 | |
|   getBestRectForElement(ele) {
 | |
|     let lastRect;
 | |
|     let lastNode;
 | |
|     let rect;
 | |
|     let attemptExtend = false;
 | |
|     let node = ele;
 | |
|     while (node) {
 | |
|       rect = this.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);
 | |
|       if (evenBetter) {
 | |
|         node = lastNode = evenBetter;
 | |
|         rect = this.getBoundingClientRect(evenBetter);
 | |
|         attemptExtend = false;
 | |
|       }
 | |
|     }
 | |
|     if (rect && attemptExtend) {
 | |
|       let extendNode = lastNode.nextSibling;
 | |
|       while (extendNode) {
 | |
|         if (extendNode.nodeType === this.#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 extendRect = this.getBoundingClientRect(extendNode);
 | |
|         let x = Math.min(rect.x, extendRect.x);
 | |
|         let y = Math.min(rect.y, extendRect.y);
 | |
|         let width = Math.max(rect.right, extendRect.right) - x;
 | |
|         let height = Math.max(rect.bottom, extendRect.bottom) - y;
 | |
|         const combinedRect = new DOMRect(x, y, width, height);
 | |
|         if (
 | |
|           combinedRect.width <= MAX_DETECT_WIDTH &&
 | |
|           combinedRect.height <= MAX_DETECT_HEIGHT
 | |
|         ) {
 | |
|           rect = combinedRect;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (
 | |
|       rect &&
 | |
|       (rect.width < MIN_DETECT_ABSOLUTE_WIDTH ||
 | |
|         rect.height < MIN_DETECT_ABSOLUTE_HEIGHT)
 | |
|     ) {
 | |
|       rect = null;
 | |
|     }
 | |
| 
 | |
|     if (!rect) {
 | |
|       this.hide();
 | |
|     } else {
 | |
|       this.drawHoverBox(rect);
 | |
|     }
 | |
| 
 | |
|     this.#rect = rect;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * This finds a better element by looking for elements with role article
 | |
|    * @param node The currently hovered node
 | |
|    * @returns A better node or null
 | |
|    */
 | |
|   evenBetterElement(node) {
 | |
|     let el = node.parentNode;
 | |
|     const ELEMENT_NODE = this.#document.ELEMENT_NODE;
 | |
|     while (el && el.nodeType === ELEMENT_NODE) {
 | |
|       if (!el.getAttribute) {
 | |
|         return null;
 | |
|       }
 | |
|       if (el.getAttribute("role") === "article") {
 | |
|         const rect = this.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;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * The pointer events need to be removed temporarily so we can find the
 | |
|    * correct element from document.elementFromPoint()
 | |
|    * If the pointer events are on for the screenshots elements, then we will always
 | |
|    * get the screenshots elements as the elements from a given point
 | |
|    */
 | |
|   setPointerEventsNone() {
 | |
|     this.content.setAttributeForElement(
 | |
|       "screenshots-component",
 | |
|       "style",
 | |
|       "pointer-events:none;"
 | |
|     );
 | |
| 
 | |
|     let temp = this.content.getAttributeForElement(
 | |
|       "screenshots-overlay-container",
 | |
|       "style"
 | |
|     );
 | |
|     this.content.setAttributeForElement(
 | |
|       "screenshots-overlay-container",
 | |
|       "style",
 | |
|       temp + "pointer-events:none;"
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Return the pointer events to the original state because we found the element
 | |
|    */
 | |
|   resetPointerEvents() {
 | |
|     this.content.setAttributeForElement("screenshots-component", "style", "");
 | |
| 
 | |
|     let temp = this.content.getAttributeForElement(
 | |
|       "screenshots-overlay-container",
 | |
|       "style"
 | |
|     );
 | |
|     this.content.setAttributeForElement(
 | |
|       "screenshots-overlay-container",
 | |
|       "style",
 | |
|       temp.replace("pointer-events:none;", "")
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class SelectionLayer extends AnonLayer {
 | |
|   #selectionBox;
 | |
|   #hoverElementBox;
 | |
|   #buttons;
 | |
|   #hidden;
 | |
|   /**
 | |
|    * the documentDimensions follows the below structure
 | |
|    * {
 | |
|    *    scrollWidth: the total document width
 | |
|    *    scrollHeight: the total document height
 | |
|    *    scrollX: the x scrolled offset
 | |
|    *    scrollY: the y scrolled offset
 | |
|    *    innerWidth: the viewport width
 | |
|    *    innerHeight: the viewport height
 | |
|    * }
 | |
|    */
 | |
|   #documentDimensions;
 | |
| 
 | |
|   constructor(id, content, hoverElementBox) {
 | |
|     super(id, content);
 | |
|     this.#selectionBox = new SelectionBox(content, this);
 | |
|     this.#buttons = new ButtonsLayer("buttons", content, this);
 | |
|     this.#hoverElementBox = hoverElementBox;
 | |
|     this.#hoverElementBox.selectionLayer = this;
 | |
| 
 | |
|     this.#hidden = true;
 | |
|     this.#documentDimensions = {};
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Hide the buttons layer
 | |
|    */
 | |
|   hideButtons() {
 | |
|     this.#buttons.hide();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Call
 | |
|    */
 | |
|   drawButtonsLayer() {
 | |
|     this.#buttons.show();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Hide the selection-container element
 | |
|    */
 | |
|   hide() {
 | |
|     super.hide();
 | |
|     this.#hidden = true;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Draw the SelectionBox
 | |
|    */
 | |
|   drawSelectionBox() {
 | |
|     if (this.#hidden) {
 | |
|       this.show();
 | |
|       this.#hidden = false;
 | |
|     }
 | |
|     this.#selectionBox.show();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Sort the SelectionBox coordinates
 | |
|    */
 | |
|   sortSelectionBoxCoords() {
 | |
|     this.#selectionBox.sortCoords();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Sets the SelectionBox dimensions
 | |
|    * @param {Object} dims The new box dimensions
 | |
|    *  {
 | |
|    *    left: new left dimension value or undefined
 | |
|    *    top: new top dimension value or undefined
 | |
|    *    right: new right dimension value or undefined
 | |
|    *    bottom: new bottom dimension value or undefined
 | |
|    *   }
 | |
|    */
 | |
|   setSelectionBoxDimensions(dims) {
 | |
|     if (dims.left) {
 | |
|       this.#selectionBox.left = dims.left;
 | |
|     }
 | |
|     if (dims.top) {
 | |
|       this.#selectionBox.top = dims.top;
 | |
|     }
 | |
|     if (dims.right) {
 | |
|       this.#selectionBox.right = dims.right;
 | |
|     }
 | |
|     if (dims.bottom) {
 | |
|       this.#selectionBox.bottom = dims.bottom;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Gets the selections box dimensions
 | |
|    * @returns {Object}
 | |
|    *  {
 | |
|    *    x1: the left dimension value
 | |
|    *    y1: the top dimension value
 | |
|    *    width: the width of the selected region
 | |
|    *    height: the height of the selected region
 | |
|    *  }
 | |
|    */
 | |
|   getSelectionBoxDimensions() {
 | |
|     return this.#selectionBox.getDimensions();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the box dimensions and the page dimensions
 | |
|    * @returns {Object}
 | |
|    *  {
 | |
|    *    boxLeft: the left position of the box
 | |
|    *    boxTop: the top position of the box
 | |
|    *    boxRight: the right position of the box
 | |
|    *    boxBottom: the bottom position of the box
 | |
|    *    scrollWidth: the total document width
 | |
|    *    scrollHeight: the total document height
 | |
|    *    scrollX: the x scrolled offset
 | |
|    *    scrollY: the y scrolled offset
 | |
|    *    innerWidth: the viewport width
 | |
|    *    innerHeight: the viewport height
 | |
|    *  }
 | |
|    */
 | |
|   getDimensions() {
 | |
|     return {
 | |
|       boxLeft: this.#selectionBox.left,
 | |
|       boxTop: this.#selectionBox.top,
 | |
|       boxRight: this.#selectionBox.right,
 | |
|       boxBottom: this.#selectionBox.bottom,
 | |
|       boxWidth: this.#selectionBox.width,
 | |
|       boxHeight: this.#selectionBox.height,
 | |
|       ...this.#documentDimensions,
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Gets the diagonal distance of the SelectionBox
 | |
|    * @returns The diagonal distance of the SelectionBox
 | |
|    */
 | |
|   getSelectionBoxDistance() {
 | |
|     return this.#selectionBox.distance;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Shift the SelectionBox so that it is always within the document
 | |
|    */
 | |
|   shiftSelectionBox() {
 | |
|     this.#selectionBox.shiftBox();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Update the box coordinates from the hover element rect
 | |
|    */
 | |
|   updateSelectionBoxFromRect(scrollX, scrollY) {
 | |
|     this.#selectionBox.updateBoxFromRect(
 | |
|       this.#hoverElementBox.rect,
 | |
|       scrollX,
 | |
|       scrollY
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handles when the user moves the mouse over an element
 | |
|    * @param viewX The viewport x coordinate
 | |
|    * @param viewY The viewport y coordinate
 | |
|    * @param targetId The target element id
 | |
|    */
 | |
|   handleElementHover(viewX, viewY, targetId) {
 | |
|     this.#hoverElementBox.handleElementHover(viewX, viewY, targetId);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handles moving the rect when the user has scrolled but not moved the mouse
 | |
|    * It uses the last x and y viewport coordinates to find the new element at the mouse position
 | |
|    */
 | |
|   handleElementScroll() {
 | |
|     this.#hoverElementBox.handleElementScroll();
 | |
|   }
 | |
| 
 | |
|   hideHoverElementSelection() {
 | |
|     this.#hoverElementBox.hide();
 | |
|   }
 | |
| 
 | |
|   get hoverElementBoxRect() {
 | |
|     return this.#hoverElementBox.rect;
 | |
|   }
 | |
| 
 | |
|   get scrollWidth() {
 | |
|     return this.#documentDimensions.scrollWidth;
 | |
|   }
 | |
|   set scrollWidth(val) {
 | |
|     this.#documentDimensions.scrollWidth = val;
 | |
|   }
 | |
| 
 | |
|   get scrollHeight() {
 | |
|     return this.#documentDimensions.scrollHeight;
 | |
|   }
 | |
|   set scrollHeight(val) {
 | |
|     this.#documentDimensions.scrollHeight = val;
 | |
|   }
 | |
| 
 | |
|   get scrollX() {
 | |
|     return this.#documentDimensions.scrollX;
 | |
|   }
 | |
|   set scrollX(val) {
 | |
|     this.#documentDimensions.scrollX = val;
 | |
|   }
 | |
| 
 | |
|   get scrollY() {
 | |
|     return this.#documentDimensions.scrollY;
 | |
|   }
 | |
|   set scrollY(val) {
 | |
|     this.#documentDimensions.scrollY = val;
 | |
|   }
 | |
| 
 | |
|   get innerWidth() {
 | |
|     return this.#documentDimensions.innerWidth;
 | |
|   }
 | |
|   set innerWidth(val) {
 | |
|     this.#documentDimensions.innerWidth = val;
 | |
|   }
 | |
| 
 | |
|   get innerHeight() {
 | |
|     return this.#documentDimensions.innerHeight;
 | |
|   }
 | |
|   set innerHeight(val) {
 | |
|     this.#documentDimensions.innerHeight = val;
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * The SelectionBox class handles drawing the highlight and background
 | |
|  */
 | |
| class SelectionBox extends AnonLayer {
 | |
|   #x1;
 | |
|   #x2;
 | |
|   #y1;
 | |
|   #y2;
 | |
|   #xOffset;
 | |
|   #yOffset;
 | |
|   #selectionLayer;
 | |
| 
 | |
|   constructor(content, selectionLayer) {
 | |
|     super("", content);
 | |
| 
 | |
|     this.#selectionLayer = selectionLayer;
 | |
| 
 | |
|     this.#x1 = 0;
 | |
|     this.#x2 = 0;
 | |
|     this.#y1 = 0;
 | |
|     this.#y2 = 0;
 | |
|     this.#xOffset = 0;
 | |
|     this.#yOffset = 0;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Draw the selected region for screenshotting
 | |
|    */
 | |
|   show() {
 | |
|     this.content.setAttributeForElement(
 | |
|       "highlight",
 | |
|       "style",
 | |
|       `top:${this.top}px;left:${this.left}px;height:${this.height}px;width:${this.width}px;`
 | |
|     );
 | |
| 
 | |
|     this.content.setAttributeForElement(
 | |
|       "bgTop",
 | |
|       "style",
 | |
|       `top:0px;height:${this.top}px;left:0px;width:100%;`
 | |
|     );
 | |
| 
 | |
|     this.content.setAttributeForElement(
 | |
|       "bgBottom",
 | |
|       "style",
 | |
|       `top:${this.bottom}px;height:calc(100% - ${this.bottom}px);left:0px;width:100%;`
 | |
|     );
 | |
| 
 | |
|     this.content.setAttributeForElement(
 | |
|       "bgLeft",
 | |
|       "style",
 | |
|       `top:${this.top}px;height:${this.height}px;left:0px;width:${this.left}px;`
 | |
|     );
 | |
| 
 | |
|     this.content.setAttributeForElement(
 | |
|       "bgRight",
 | |
|       "style",
 | |
|       `top:${this.top}px;height:${this.height}px;left:${this.right}px;width:calc(100% - ${this.right}px);`
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Update the box coordinates from the rect
 | |
|    * @param rect The hover element box
 | |
|    * @param scrollX The x offset the page is scrolled
 | |
|    * @param scrollY The y offset the page is scrolled
 | |
|    */
 | |
|   updateBoxFromRect(rect, scrollX, scrollY) {
 | |
|     this.top = rect.top + scrollY;
 | |
|     this.left = rect.left + scrollX;
 | |
|     this.right = rect.right + scrollX;
 | |
|     this.bottom = rect.bottom + scrollY;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Hide the selected region
 | |
|    */
 | |
|   hide() {
 | |
|     this.content.setAttributeForElement("highlight", "style", "display:none;");
 | |
|     this.content.setAttributeForElement("bgTop", "style", "display:none;");
 | |
|     this.content.setAttributeForElement("bgBottom", "style", "display:none;");
 | |
|     this.content.setAttributeForElement("bgLeft", "style", "display:none;");
 | |
|     this.content.setAttributeForElement("bgRight", "style", "display:none;");
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * The box should never appear outside the document so the SelectionBox will
 | |
|    * be shifted if the bounds of the box are outside the documents width or height
 | |
|    */
 | |
|   shiftBox() {
 | |
|     let didShift = false;
 | |
|     let xDiff = this.right - this.#selectionLayer.scrollWidth;
 | |
|     if (xDiff > 0) {
 | |
|       this.right -= xDiff;
 | |
|       this.left -= xDiff;
 | |
| 
 | |
|       didShift = true;
 | |
|     }
 | |
| 
 | |
|     let yDiff = this.bottom - this.#selectionLayer.scrollHeight;
 | |
|     if (yDiff > 0) {
 | |
|       let curWidth = this.width;
 | |
| 
 | |
|       this.bottom -= yDiff;
 | |
|       this.top = this.bottom - curWidth;
 | |
| 
 | |
|       didShift = true;
 | |
|     }
 | |
| 
 | |
|     if (didShift) {
 | |
|       this.show();
 | |
|       this.#selectionLayer.drawButtonsLayer();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Sort the coordinates so x1 < x2 and y1 < y2
 | |
|    */
 | |
|   sortCoords() {
 | |
|     if (this.#x1 > this.#x2) {
 | |
|       [this.#x1, this.#x2] = [this.#x2, this.#x1];
 | |
|     }
 | |
|     if (this.#y1 > this.#y2) {
 | |
|       [this.#y1, this.#y2] = [this.#y2, this.#y1];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Gets the dimensions of the currently selected region
 | |
|    * @returns {Object}
 | |
|    *  {
 | |
|    *    x1: the left dimension value
 | |
|    *    y1: the top dimension value
 | |
|    *    width: the width of the selected region
 | |
|    *    height: the height of the selected region
 | |
|    *  }
 | |
|    */
 | |
|   getDimensions() {
 | |
|     return {
 | |
|       x1: this.left,
 | |
|       y1: this.top,
 | |
|       width: this.width,
 | |
|       height: this.height,
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   get distance() {
 | |
|     return Math.sqrt(Math.pow(this.width, 2) + Math.pow(this.height, 2));
 | |
|   }
 | |
| 
 | |
|   get xOffset() {
 | |
|     return this.#xOffset;
 | |
|   }
 | |
|   set xOffset(val) {
 | |
|     this.#xOffset = val;
 | |
|   }
 | |
| 
 | |
|   get yOffset() {
 | |
|     return this.#yOffset;
 | |
|   }
 | |
|   set yOffset(val) {
 | |
|     this.#yOffset = val;
 | |
|   }
 | |
| 
 | |
|   get top() {
 | |
|     return Math.min(this.#y1, this.#y2);
 | |
|   }
 | |
|   set top(val) {
 | |
|     this.#y1 = val > 0 ? val : 0;
 | |
|   }
 | |
| 
 | |
|   get left() {
 | |
|     return Math.min(this.#x1, this.#x2);
 | |
|   }
 | |
|   set left(val) {
 | |
|     this.#x1 = val > 0 ? val : 0;
 | |
|   }
 | |
| 
 | |
|   get right() {
 | |
|     return Math.max(this.#x1, this.#x2);
 | |
|   }
 | |
|   set right(val) {
 | |
|     this.#x2 =
 | |
|       val > this.#selectionLayer.scrollWidth
 | |
|         ? this.#selectionLayer.scrollWidth
 | |
|         : val;
 | |
|   }
 | |
| 
 | |
|   get bottom() {
 | |
|     return Math.max(this.#y1, this.#y2);
 | |
|   }
 | |
|   set bottom(val) {
 | |
|     this.#y2 =
 | |
|       val > this.#selectionLayer.scrollHeight
 | |
|         ? this.#selectionLayer.scrollHeight
 | |
|         : val;
 | |
|   }
 | |
| 
 | |
|   get width() {
 | |
|     return Math.abs(this.#x2 - this.#x1);
 | |
|   }
 | |
|   get height() {
 | |
|     return Math.abs(this.#y2 - this.#y1);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class ButtonsLayer extends AnonLayer {
 | |
|   #selectionLayer;
 | |
| 
 | |
|   constructor(id, content, selectionLayer) {
 | |
|     super(id, content);
 | |
| 
 | |
|     this.#selectionLayer = selectionLayer;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Draw the buttons. Check if the box is too near the bottom or left of the
 | |
|    * viewport and adjust the buttons accordingly
 | |
|    */
 | |
|   show() {
 | |
|     let {
 | |
|       boxLeft,
 | |
|       boxTop,
 | |
|       boxRight,
 | |
|       boxBottom,
 | |
|       scrollX,
 | |
|       scrollY,
 | |
|       innerWidth,
 | |
|       innerHeight,
 | |
|     } = this.#selectionLayer.getDimensions();
 | |
| 
 | |
|     if (
 | |
|       boxTop > scrollY + innerHeight ||
 | |
|       boxBottom < scrollY ||
 | |
|       boxLeft > scrollX + innerWidth ||
 | |
|       boxRight < scrollX
 | |
|     ) {
 | |
|       // The box is offscreen so need to draw the buttons
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let top = boxBottom;
 | |
|     let leftOrRight = `right:calc(100% - ${boxRight}px);`;
 | |
| 
 | |
|     if (scrollY + innerHeight - boxBottom < 70) {
 | |
|       if (boxBottom < scrollY + innerHeight) {
 | |
|         top = boxBottom - 60;
 | |
|       } else if (scrollY + innerHeight - boxTop < 70) {
 | |
|         top = boxTop - 60;
 | |
|       } else {
 | |
|         top = scrollY + innerHeight - 60;
 | |
|       }
 | |
|     }
 | |
|     if (boxRight < 265) {
 | |
|       leftOrRight = `left:${boxLeft}px;`;
 | |
|     }
 | |
| 
 | |
|     this.content.setAttributeForElement(
 | |
|       "buttons",
 | |
|       "style",
 | |
|       `top:${top}px;${leftOrRight}`
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class PreviewLayer extends AnonLayer {
 | |
|   constructor(id, content) {
 | |
|     super(id, content);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Draw the eyeballs facing the mouse
 | |
|    * @param clientX x pointer position
 | |
|    * @param clientY y pointer position
 | |
|    * @param width width of the viewport
 | |
|    * @param height height of the viewport
 | |
|    */
 | |
|   drawEyes(clientX, clientY, width, height) {
 | |
|     const xpos = Math.floor((10 * (clientX - width / 2)) / width);
 | |
|     const ypos = Math.floor((10 * (clientY - height / 2)) / height);
 | |
|     const move = `transform:translate(${xpos}px, ${ypos}px);`;
 | |
|     this.content.setAttributeForElement("left-eye", "style", move);
 | |
|     this.content.setAttributeForElement("right-eye", "style", move);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class ScreenshotsContainerLayer extends AnonLayer {
 | |
|   #width;
 | |
|   #height;
 | |
|   #previewLayer;
 | |
|   #selectionLayer;
 | |
| 
 | |
|   constructor(id, content, previewLayer, selectionLayer) {
 | |
|     super(id, content);
 | |
| 
 | |
|     this.#previewLayer = previewLayer;
 | |
|     this.#selectionLayer = selectionLayer;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Hide the SelectionLayer
 | |
|    */
 | |
|   hideSelectionLayer() {
 | |
|     this.#selectionLayer.hide();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Show the PreviewLayer
 | |
|    */
 | |
|   showPreviewLayer() {
 | |
|     this.#previewLayer.show();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Hide the PreviewLayer
 | |
|    */
 | |
|   hidePreviewLayer() {
 | |
|     this.#previewLayer.hide();
 | |
|     this.#selectionLayer.hideHoverElementSelection();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Show the ButtonsLayer
 | |
|    */
 | |
|   drawButtonsLayer() {
 | |
|     this.#selectionLayer.drawButtonsLayer();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Hide the ButtonsLayer
 | |
|    */
 | |
|   hideButtonsLayer() {
 | |
|     this.#selectionLayer.hideButtons();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Show the SelectionBox
 | |
|    */
 | |
|   drawSelectionBox() {
 | |
|     this.#selectionLayer.drawSelectionBox();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Update the box coordinates from the hover element rect
 | |
|    */
 | |
|   updateSelectionBoxFromRect(scrollX, scrollY) {
 | |
|     this.#selectionLayer.updateSelectionBoxFromRect(scrollX, scrollY);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handles when the user moves the mouse over an element
 | |
|    * @param viewX The viewport x coordinate
 | |
|    * @param viewY The viewport y coordinate
 | |
|    * @param targetId The target element id
 | |
|    */
 | |
|   handleElementHover(viewX, viewY, targetId) {
 | |
|     this.#selectionLayer.handleElementHover(viewX, viewY, targetId);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handles moving the rect when the user has scrolled but not moved the mouse
 | |
|    * It uses the last x and y viewport coordinates to find the new element at the mouse position
 | |
|    */
 | |
|   handleElementScroll() {
 | |
|     this.#selectionLayer.handleElementScroll();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Draw the eyes in the PreviewLayer
 | |
|    * @param clientX The x mouse position
 | |
|    * @param clientY The y mouse position
 | |
|    */
 | |
|   drawPreviewEyes(clientX, clientY) {
 | |
|     this.#previewLayer.drawEyes(
 | |
|       clientX - this.#selectionLayer.scrollX,
 | |
|       clientY - this.#selectionLayer.scrollY,
 | |
|       this.#selectionLayer.innerWidth,
 | |
|       this.#selectionLayer.innerHeight
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Get the diagonal distance of the SelectionBox
 | |
|    * @returns The diagonal distance of the currently selected region
 | |
|    */
 | |
|   selectionBoxDistance() {
 | |
|     return this.#selectionLayer.getSelectionBoxDistance();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Sort the coordinates of the SelectionBox
 | |
|    */
 | |
|   sortSelectionLayerBoxCoords() {
 | |
|     this.#selectionLayer.sortSelectionBoxCoords();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Get the SelectionLayer dimensions
 | |
|    * @returns {Object}
 | |
|    *  {
 | |
|    *    x1: the left dimension value
 | |
|    *    y1: the top dimension value
 | |
|    *    width: the width of the selected region
 | |
|    *    height: the height of the selected region
 | |
|    *  }
 | |
|    */
 | |
|   getSelectionLayerBoxDimensions() {
 | |
|     return this.#selectionLayer.getSelectionBoxDimensions();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Gets the SelectionBox and page dimensions
 | |
|    * @returns {Object}
 | |
|    *  {
 | |
|    *    boxLeft: the left position of the box
 | |
|    *    boxTop: the top position of the box
 | |
|    *    boxRight: the right position of the box
 | |
|    *    boxBottom: the bottom position of the box
 | |
|    *    scrollWidth: the total document width
 | |
|    *    scrollHeight: the total document height
 | |
|    *    scrollX: the x scrolled offset
 | |
|    *    scrollY: the y scrolled offset
 | |
|    *    innerWidth: the viewport width
 | |
|    *    innerHeight: the viewport height
 | |
|    *  }
 | |
|    */
 | |
|   getSelectionLayerDimensions() {
 | |
|     return this.#selectionLayer.getDimensions();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Shift the SelectionBox
 | |
|    */
 | |
|   shiftSelectionLayerBox() {
 | |
|     this.#selectionLayer.shiftSelectionBox();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Set the respective dimensions of the SelectionBox
 | |
|    * @param {Object} boxDimensionObject The new box dimensions
 | |
|    *  {
 | |
|    *    left: new left dimension value or undefined
 | |
|    *    top: new top dimension value or undefined
 | |
|    *    right: new right dimension value or undefined
 | |
|    *    bottom: new bottom dimension value or undefined
 | |
|    *   }
 | |
|    */
 | |
|   setSelectionBoxDimensions(boxDimensionObject) {
 | |
|     this.#selectionLayer.setSelectionBoxDimensions(boxDimensionObject);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * The screenshots-overlay-container doesn't shrink with the window when the
 | |
|    * window is resized so we have to manually find the width and height of the
 | |
|    * window by looping throught the documentElement's children
 | |
|    * If the children mysteriously have a height or width of 0 then we will
 | |
|    * fallback to the scrollWidth and scrollHeight which can cause the container
 | |
|    * to be larger than the window dimensions
 | |
|    * @param win The window object
 | |
|    */
 | |
|   updateSize(win) {
 | |
|     let { innerWidth, innerHeight, scrollX, scrollY } = win;
 | |
|     this.#selectionLayer.innerWidth = innerWidth;
 | |
|     this.#selectionLayer.innerHeight = innerHeight;
 | |
|     this.#selectionLayer.scrollX = scrollX;
 | |
|     this.#selectionLayer.scrollY = scrollY;
 | |
| 
 | |
|     const doc = win.document.documentElement;
 | |
|     let width = Math.max.apply(
 | |
|       null,
 | |
|       Array.from(doc.children, x => x.scrollWidth)
 | |
|     );
 | |
|     let height = Math.max.apply(
 | |
|       null,
 | |
|       Array.from(doc.children, x => x.scrollHeight)
 | |
|     );
 | |
| 
 | |
|     if (width < 1) {
 | |
|       width = doc.scrollWidth;
 | |
|     } else if (width < innerWidth) {
 | |
|       width = innerWidth;
 | |
|     }
 | |
| 
 | |
|     if (height < 1) {
 | |
|       height = doc.scrollHeight;
 | |
|     } else if (height < innerHeight) {
 | |
|       height = innerHeight;
 | |
|     }
 | |
| 
 | |
|     this.#selectionLayer.scrollWidth = width;
 | |
|     this.#selectionLayer.scrollHeight = height;
 | |
| 
 | |
|     this.#width = width;
 | |
|     this.#height = height;
 | |
| 
 | |
|     this.drawScreenshotsContainer();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Return the dimensions of the screenshots container
 | |
|    * @returns {Object}
 | |
|    *  width: the container width
 | |
|    *  height: the container height
 | |
|    */
 | |
|   getDimension() {
 | |
|     return { width: this.#width, height: this.#height };
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Draw the screenshots container
 | |
|    */
 | |
|   drawScreenshotsContainer() {
 | |
|     this.content.setAttributeForElement(
 | |
|       this.id,
 | |
|       "style",
 | |
|       `top:0;left:0;width:${this.#width}px;height:${this.#height}px;`
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   get hoverElementBoxRect() {
 | |
|     return this.#selectionLayer.hoverElementBoxRect;
 | |
|   }
 | |
| }
 | 
