1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * The Screenshots overlay is inserted into the document's
7 * anonymous content container (see dom/webidl/Document.webidl).
9 * This container gets cleared automatically when the document navigates.
11 * To retrieve the AnonymousContent instance, use the `content` getter.
15 * Below are the states of the screenshots overlay
18 * Nothing has happened, and the crosshairs will follow the movement of the mouse
20 * The user has pressed the mouse button, but hasn't moved enough to create a selection
22 * The user has pressed down a mouse button, and is dragging out an area far enough to show a selection
24 * The user has selected an area
26 * The user is resizing the selection
32 getBestRectForElement,
35 } from "chrome://browser/content/screenshots/overlayHelpers.mjs";
37 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
40 CROSSHAIRS: "crosshairs",
41 DRAGGING_READY: "draggingReady",
49 ChromeUtils.defineLazyGetter(lazy, "overlayLocalization", () => {
50 return new Localization(["browser/screenshotsOverlay.ftl"], true);
53 const REGION_CHANGE_THRESHOLD = 5;
54 const SCROLL_BY_EDGE = 20;
56 export class ScreenshotsOverlay {
70 let [cancel, instructions, download, copy] =
71 lazy.overlayLocalization.formatMessagesSync([
72 { id: "screenshots-overlay-cancel-button" },
73 { id: "screenshots-overlay-instructions" },
74 { id: "screenshots-overlay-download-button" },
75 { id: "screenshots-overlay-copy-button" },
80 <link rel="stylesheet" href="chrome://browser/content/screenshots/overlay/overlay.css" />
81 <div id="screenshots-component">
82 <div id="preview-container" hidden>
83 <div class="face-container">
84 <div class="eye left"><div id="left-eye" class="eyeball"></div></div>
85 <div class="eye right"><div id="right-eye" class="eyeball"></div></div>
86 <div class="face"></div>
88 <div class="preview-instructions">${instructions.value}</div>
89 <button class="screenshots-button" id="screenshots-cancel-button">${cancel.value}</button>
91 <div id="hover-highlight" hidden></div>
92 <div id="selection-container" hidden>
93 <div id="top-background" class="bghighlight"></div>
94 <div id="bottom-background" class="bghighlight"></div>
95 <div id="left-background" class="bghighlight"></div>
96 <div id="right-background" class="bghighlight"></div>
97 <div id="highlight" class="highlight" tabindex="0">
98 <div id="mover-topLeft" class="mover-target direction-topLeft" tabindex="0">
99 <div class="mover"></div>
101 <div id="mover-top" class="mover-target direction-top">
102 <div class="mover"></div>
104 <div id="mover-topRight" class="mover-target direction-topRight" tabindex="0">
105 <div class="mover"></div>
107 <div id="mover-left" class="mover-target direction-left">
108 <div class="mover"></div>
110 <div id="mover-right" class="mover-target direction-right">
111 <div class="mover"></div>
113 <div id="mover-bottomLeft" class="mover-target direction-bottomLeft" tabindex="0">
114 <div class="mover"></div>
116 <div id="mover-bottom" class="mover-target direction-bottom">
117 <div class="mover"></div>
119 <div id="mover-bottomRight" class="mover-target direction-bottomRight" tabindex="0">
120 <div class="mover"></div>
122 <div id="selection-size-container">
123 <span id="selection-size"></span>
127 <div id="buttons-container" hidden>
128 <div class="buttons-wrapper">
129 <button id="cancel" class="screenshots-button" title="${cancel.value}" aria-label="${cancel.value}" tabindex="0"><img/></button>
130 <button id="copy" class="screenshots-button" title="${copy.value}" aria-label="${copy.value}" tabindex="0"><img/>${copy.value}</button>
131 <button id="download" class="screenshots-button primary" title="${download.value}" aria-label="${download.value}" tabindex="0"><img/>${download.value}</button>
139 if (!this.overlayTemplate) {
140 let parser = new DOMParser();
141 let doc = parser.parseFromString(this.markup, "text/html");
142 this.overlayTemplate = this.document.importNode(
143 doc.querySelector("template"),
147 let fragment = this.overlayTemplate.content.cloneNode(true);
152 return this.#initialized;
160 return this.#methodsUsed;
163 constructor(contentDocument) {
164 this.document = contentDocument;
165 this.window = contentDocument.ownerGlobal;
167 this.windowDimensions = new WindowDimensions();
168 this.selectionRegion = new Region(this.windowDimensions);
169 this.hoverElementRegion = new Region(this.windowDimensions);
170 this.resetMethodsUsed();
174 if (!this.#content || Cu.isDeadWrapper(this.#content)) {
177 return this.#content;
181 return this.content.root.getElementById(id);
185 if (this.initialized) {
189 this.windowDimensions.reset();
191 this.#content = this.document.insertAnonymousContent();
192 this.#content.root.appendChild(this.fragment);
194 this.initializeElements();
195 this.updateWindowDimensions();
197 this.#setState(STATES.CROSSHAIRS);
199 this.#initialized = true;
203 * Get all the elements that will be used.
205 initializeElements() {
206 this.previewCancelButton = this.getElementById("screenshots-cancel-button");
207 this.cancelButton = this.getElementById("cancel");
208 this.copyButton = this.getElementById("copy");
209 this.downloadButton = this.getElementById("download");
211 this.previewContainer = this.getElementById("preview-container");
212 this.hoverElementContainer = this.getElementById("hover-highlight");
213 this.selectionContainer = this.getElementById("selection-container");
214 this.buttonsContainer = this.getElementById("buttons-container");
215 this.screenshotsContainer = this.getElementById("screenshots-component");
217 this.leftEye = this.getElementById("left-eye");
218 this.rightEye = this.getElementById("right-eye");
220 this.leftBackgroundEl = this.getElementById("left-background");
221 this.topBackgroundEl = this.getElementById("top-background");
222 this.rightBackgroundEl = this.getElementById("right-background");
223 this.bottomBackgroundEl = this.getElementById("bottom-background");
224 this.highlightEl = this.getElementById("highlight");
226 this.topLeftMover = this.getElementById("mover-topLeft");
227 this.topRightMover = this.getElementById("mover-topRight");
228 this.bottomLeftMover = this.getElementById("mover-bottomLeft");
229 this.bottomRightMover = this.getElementById("mover-bottomRight");
231 this.selectionSize = this.getElementById("selection-size");
235 * Removes all event listeners and removes the overlay from the Anonymous Content
237 tearDown(options = {}) {
239 if (!(options.doNotResetMethods === true)) {
240 this.resetMethodsUsed();
243 this.document.removeAnonymousContent(this.#content);
245 // If the current window isn't the one the content was inserted into, this
246 // will fail, but that's fine.
249 this.#initialized = false;
254 this.#methodsUsed = {
263 * Returns the x and y coordinates of the event relative to both the
264 * viewport and the page.
265 * @param {Event} event The event
268 * clientX: The x position relative to the viewport
269 * clientY: The y position relative to the viewport
270 * pageX: The x position relative to the entire page
271 * pageY: The y position relative to the entire page
274 getCoordinatesFromEvent(event) {
275 const { clientX, clientY, pageX, pageY } = event;
277 return { clientX, clientY, pageX, pageY };
281 if (event.button > 0) {
285 switch (event.type) {
287 this.handleClick(event);
290 this.handlePointerDown(event);
293 this.handlePointerMove(event);
296 this.handlePointerUp(event);
299 this.handleKeyDown(event);
302 this.handleKeyUp(event);
308 switch (event.originalTarget.id) {
309 case "screenshots-cancel-button":
311 this.maybeCancelScreenshots();
314 this.#dispatchEvent("Screenshots:Copy", {
315 region: this.selectionRegion.dimensions,
319 this.#dispatchEvent("Screenshots:Download", {
320 region: this.selectionRegion.dimensions,
326 maybeCancelScreenshots() {
327 if (this.#state === STATES.CROSSHAIRS) {
328 this.#dispatchEvent("Screenshots:Close", {
329 reason: "overlay_cancel",
332 this.#setState(STATES.CROSSHAIRS);
337 * Handles the pointerdown event depending on the state.
338 * Early return when a pointer down happens on a button.
339 * @param {Event} event The pointerown event
341 handlePointerDown(event) {
343 event.originalTarget.id === "screenshots-cancel-button" ||
344 event.originalTarget.closest("#buttons-container") ===
345 this.buttonsContainer
347 event.stopPropagation();
351 const { pageX, pageY } = this.getCoordinatesFromEvent(event);
353 switch (this.#state) {
354 case STATES.CROSSHAIRS: {
355 this.crosshairsDragStart(pageX, pageY);
358 case STATES.SELECTED: {
359 this.selectedDragStart(pageX, pageY, event.originalTarget.id);
366 * Handles the pointermove event depending on the state
367 * @param {Event} event The pointermove event
369 handlePointerMove(event) {
370 const { pageX, pageY, clientX, clientY } =
371 this.getCoordinatesFromEvent(event);
373 switch (this.#state) {
374 case STATES.CROSSHAIRS: {
375 this.crosshairsMove(clientX, clientY);
378 case STATES.DRAGGING_READY: {
379 this.draggingReadyDrag(pageX, pageY);
382 case STATES.DRAGGING: {
383 this.draggingDrag(pageX, pageY);
386 case STATES.RESIZING: {
387 this.resizingDrag(pageX, pageY);
394 * Handles the pointerup event depending on the state
395 * @param {Event} event The pointerup event
397 handlePointerUp(event) {
398 const { pageX, pageY, clientX, clientY } =
399 this.getCoordinatesFromEvent(event);
401 switch (this.#state) {
402 case STATES.DRAGGING_READY: {
403 this.draggingReadyDragEnd(pageX - clientX, pageY - clientY);
406 case STATES.DRAGGING: {
407 this.draggingDragEnd(pageX, pageY, event.originalTarget.id);
410 case STATES.RESIZING: {
411 this.resizingDragEnd(pageX, pageY);
418 * Handles when a keydown occurs in the screenshots component.
419 * @param {Event} event The keydown event
421 handleKeyDown(event) {
424 this.handleArrowLeftKeyDown(event);
427 this.handleArrowUpKeyDown(event);
430 this.handleArrowRightKeyDown(event);
433 this.handleArrowDownKeyDown(event);
436 this.maybeLockFocus(event);
439 this.maybeCancelScreenshots();
445 * Gets the accel key depending on the platform.
446 * metaKey for macOS. ctrlKey for Windows and Linux.
447 * @param {Event} event The keydown event
448 * @returns {Boolean} True if the accel key is pressed, false otherwise.
451 if (AppConstants.platform === "macosx") {
452 return event.metaKey;
454 return event.ctrlKey;
458 * Move the region or its left or right side to the left.
459 * Just the arrow key will move the region by 1px.
460 * Arrow key + shift will move the region by 10px.
461 * Arrow key + control/meta will move to the edge of the window.
462 * @param {Event} event The keydown event
464 handleArrowLeftKeyDown(event) {
465 switch (event.originalTarget.id) {
467 if (this.getAccelKey(event)) {
468 let width = this.selectionRegion.width;
469 this.selectionRegion.left = this.windowDimensions.scrollX;
470 this.selectionRegion.right = this.windowDimensions.scrollX + width;
474 this.selectionRegion.right -= 10 ** event.shiftKey;
475 // eslint-disable-next-line no-fallthrough
476 case "mover-topLeft":
477 case "mover-bottomLeft":
478 if (this.getAccelKey(event)) {
479 this.selectionRegion.left = this.windowDimensions.scrollX;
483 this.selectionRegion.left -= 10 ** event.shiftKey;
485 this.selectionRegion.left,
486 this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2
489 case "mover-topRight":
490 case "mover-bottomRight":
491 if (this.getAccelKey(event)) {
492 let left = this.selectionRegion.left;
493 this.selectionRegion.left = this.windowDimensions.scrollX;
494 this.selectionRegion.right = left;
495 if (event.originalTarget.id === "mover-topRight") {
496 this.topLeftMover.focus();
497 } else if (event.originalTarget.id === "mover-bottomRight") {
498 this.bottomLeftMover.focus();
503 this.selectionRegion.right -= 10 ** event.shiftKey;
504 if (this.selectionRegion.x1 >= this.selectionRegion.x2) {
505 this.selectionRegion.sortCoords();
506 if (event.originalTarget.id === "mover-topRight") {
507 this.topLeftMover.focus();
508 } else if (event.originalTarget.id === "mover-bottomRight") {
509 this.bottomLeftMover.focus();
517 if (this.#state !== STATES.RESIZING) {
518 this.#setState(STATES.RESIZING);
521 event.preventDefault();
522 this.drawSelectionContainer();
526 * Move the region or its top or bottom side upward.
527 * Just the arrow key will move the region by 1px.
528 * Arrow key + shift will move the region by 10px.
529 * Arrow key + control/meta will move to the edge of the window.
530 * @param {Event} event The keydown event
532 handleArrowUpKeyDown(event) {
533 switch (event.originalTarget.id) {
535 if (this.getAccelKey(event)) {
536 let height = this.selectionRegion.height;
537 this.selectionRegion.top = this.windowDimensions.scrollY;
538 this.selectionRegion.bottom = this.windowDimensions.scrollY + height;
542 this.selectionRegion.bottom -= 10 ** event.shiftKey;
543 // eslint-disable-next-line no-fallthrough
544 case "mover-topLeft":
545 case "mover-topRight":
546 if (this.getAccelKey(event)) {
547 this.selectionRegion.top = this.windowDimensions.scrollY;
551 this.selectionRegion.top -= 10 ** event.shiftKey;
553 this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2,
554 this.selectionRegion.top
557 case "mover-bottomLeft":
558 case "mover-bottomRight":
559 if (this.getAccelKey(event)) {
560 let top = this.selectionRegion.top;
561 this.selectionRegion.top = this.windowDimensions.scrollY;
562 this.selectionRegion.bottom = top;
563 if (event.originalTarget.id === "mover-bottomLeft") {
564 this.topLeftMover.focus();
565 } else if (event.originalTarget.id === "mover-bottomRight") {
566 this.topRightMover.focus();
571 this.selectionRegion.bottom -= 10 ** event.shiftKey;
572 if (this.selectionRegion.y1 >= this.selectionRegion.y2) {
573 this.selectionRegion.sortCoords();
574 if (event.originalTarget.id === "mover-bottomLeft") {
575 this.topLeftMover.focus();
576 } else if (event.originalTarget.id === "mover-bottomRight") {
577 this.topRightMover.focus();
585 if (this.#state !== STATES.RESIZING) {
586 this.#setState(STATES.RESIZING);
589 event.preventDefault();
590 this.drawSelectionContainer();
594 * Move the region or its left or right side to the right.
595 * Just the arrow key will move the region by 1px.
596 * Arrow key + shift will move the region by 10px.
597 * Arrow key + control/meta will move to the edge of the window.
598 * @param {Event} event The keydown event
600 handleArrowRightKeyDown(event) {
601 switch (event.originalTarget.id) {
603 if (this.getAccelKey(event)) {
604 let width = this.selectionRegion.width;
605 let { scrollX, clientWidth } = this.windowDimensions.dimensions;
606 this.selectionRegion.right = scrollX + clientWidth;
607 this.selectionRegion.left = this.selectionRegion.right - width;
611 this.selectionRegion.left += 10 ** event.shiftKey;
612 // eslint-disable-next-line no-fallthrough
613 case "mover-topRight":
614 case "mover-bottomRight":
615 if (this.getAccelKey(event)) {
616 this.selectionRegion.right =
617 this.windowDimensions.scrollX + this.windowDimensions.clientWidth;
621 this.selectionRegion.right += 10 ** event.shiftKey;
623 this.selectionRegion.right,
624 this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2
627 case "mover-topLeft":
628 case "mover-bottomLeft":
629 if (this.getAccelKey(event)) {
630 let right = this.selectionRegion.right;
631 this.selectionRegion.right =
632 this.windowDimensions.scrollX + this.windowDimensions.clientWidth;
633 this.selectionRegion.left = right;
634 if (event.originalTarget.id === "mover-topLeft") {
635 this.topRightMover.focus();
636 } else if (event.originalTarget.id === "mover-bottomLeft") {
637 this.bottomRightMover.focus();
642 this.selectionRegion.left += 10 ** event.shiftKey;
643 if (this.selectionRegion.x1 >= this.selectionRegion.x2) {
644 this.selectionRegion.sortCoords();
645 if (event.originalTarget.id === "mover-topLeft") {
646 this.topRightMover.focus();
647 } else if (event.originalTarget.id === "mover-bottomLeft") {
648 this.bottomRightMover.focus();
656 if (this.#state !== STATES.RESIZING) {
657 this.#setState(STATES.RESIZING);
660 event.preventDefault();
661 this.drawSelectionContainer();
665 * Move the region or its top or bottom side downward.
666 * Just the arrow key will move the region by 1px.
667 * Arrow key + shift will move the region by 10px.
668 * Arrow key + control/meta will move to the edge of the window.
669 * @param {Event} event The keydown event
671 handleArrowDownKeyDown(event) {
672 switch (event.originalTarget.id) {
674 if (this.getAccelKey(event)) {
675 let height = this.selectionRegion.height;
676 let { scrollY, clientHeight } = this.windowDimensions.dimensions;
677 this.selectionRegion.bottom = scrollY + clientHeight;
678 this.selectionRegion.top = this.selectionRegion.bottom - height;
682 this.selectionRegion.top += 10 ** event.shiftKey;
683 // eslint-disable-next-line no-fallthrough
684 case "mover-bottomLeft":
685 case "mover-bottomRight":
686 if (this.getAccelKey(event)) {
687 this.selectionRegion.bottom =
688 this.windowDimensions.scrollY + this.windowDimensions.clientHeight;
692 this.selectionRegion.bottom += 10 ** event.shiftKey;
694 this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2,
695 this.selectionRegion.bottom
698 case "mover-topLeft":
699 case "mover-topRight":
700 if (this.getAccelKey(event)) {
701 let bottom = this.selectionRegion.bottom;
702 this.selectionRegion.bottom =
703 this.windowDimensions.scrollY + this.windowDimensions.clientHeight;
704 this.selectionRegion.top = bottom;
705 if (event.originalTarget.id === "mover-topLeft") {
706 this.bottomLeftMover.focus();
707 } else if (event.originalTarget.id === "mover-topRight") {
708 this.bottomRightMover.focus();
713 this.selectionRegion.top += 10 ** event.shiftKey;
714 if (this.selectionRegion.y1 >= this.selectionRegion.y2) {
715 this.selectionRegion.sortCoords();
716 if (event.originalTarget.id === "mover-topLeft") {
717 this.bottomLeftMover.focus();
718 } else if (event.originalTarget.id === "mover-topRight") {
719 this.bottomRightMover.focus();
727 if (this.#state !== STATES.RESIZING) {
728 this.#setState(STATES.RESIZING);
731 event.preventDefault();
732 this.drawSelectionContainer();
736 * We lock focus to the overlay when a region is selected.
737 * Can still escape with shift + F6.
738 * @param {Event} event The keydown event
740 maybeLockFocus(event) {
741 if (this.#state !== STATES.SELECTED) {
745 event.preventDefault();
746 if (event.originalTarget.id === "highlight" && event.shiftKey) {
747 this.downloadButton.focus();
748 } else if (event.originalTarget.id === "download" && !event.shiftKey) {
749 this.highlightEl.focus();
751 // The content document can listen for keydown events and prevent moving
752 // focus so we manually move focus to the next element here.
753 let direction = event.shiftKey
754 ? Services.focus.MOVEFOCUS_BACKWARD
755 : Services.focus.MOVEFOCUS_FORWARD;
756 Services.focus.moveFocus(this.window, null, direction, 0);
761 * Handles when a keydown occurs in the screenshots component.
762 * All we need to do on keyup is set the state to selected.
763 * @param {Event} event The keydown event
771 switch (event.originalTarget.id) {
773 case "mover-bottomLeft":
774 case "mover-bottomRight":
775 case "mover-topLeft":
776 case "mover-topRight":
777 event.preventDefault();
778 this.#setState(STATES.SELECTED);
786 * Dispatch a custom event to the ScreenshotsComponentChild actor
787 * @param {String} eventType The name of the event
788 * @param {object} detail Extra details to send to the child actor
790 #dispatchEvent(eventType, detail) {
791 this.window.dispatchEvent(
792 new CustomEvent(eventType, {
800 * Set a new state for the overlay
801 * @param {String} newState
803 #setState(newState) {
804 if (this.#state === STATES.SELECTED && newState === STATES.CROSSHAIRS) {
805 this.#dispatchEvent("Screenshots:RecordEvent", {
806 eventName: "started",
807 reason: "overlay_retry",
810 if (newState !== this.#state) {
811 this.#dispatchEvent("Screenshots:OverlaySelection", {
812 hasSelection: newState == STATES.SELECTED,
815 this.#state = newState;
817 switch (this.#state) {
818 case STATES.CROSSHAIRS: {
819 this.crosshairsStart();
822 case STATES.DRAGGING_READY: {
823 this.draggingReadyStart();
826 case STATES.DRAGGING: {
827 this.draggingStart();
830 case STATES.SELECTED: {
831 this.selectedStart();
834 case STATES.RESIZING: {
835 this.resizingStart();
842 * Hide hover element, selection and buttons containers.
843 * Show the preview container and the panel.
844 * This is the initial state of the overlay.
847 this.hideHoverElementContainer();
848 this.hideSelectionContainer();
849 this.hideButtonsContainer();
850 this.showPreviewContainer();
851 this.#dispatchEvent("Screenshots:ShowPanel");
852 this.#previousDimensions = null;
853 this.#cachedEle = null;
854 this.hoverElementRegion.resetDimensions();
858 * Hide the panel because we have started dragging.
860 draggingReadyStart() {
861 this.#dispatchEvent("Screenshots:HidePanel");
865 * Hide the preview, hover element and buttons containers.
866 * Show the selection container.
869 this.hidePreviewContainer();
870 this.hideButtonsContainer();
871 this.hideHoverElementContainer();
872 this.drawSelectionContainer();
876 * Hide the preview and hover element containers.
877 * Draw the selection and buttons containers.
880 this.hidePreviewContainer();
881 this.hideHoverElementContainer();
882 this.drawSelectionContainer();
883 this.drawButtonsContainer();
887 * Hide the buttons container.
888 * Store the width and height of the current selected region.
889 * The dimensions will be used when moving the region along the edge of the
890 * page and for recording telemetry.
893 this.hideButtonsContainer();
894 let { width, height } = this.selectionRegion.dimensions;
895 this.#previousDimensions = { width, height };
899 * Dragging has started so we set the initial selection region and set the
900 * state to draggingReady.
901 * @param {Number} pageX The x position relative to the page
902 * @param {Number} pageY The y position relative to the page
904 crosshairsDragStart(pageX, pageY) {
905 this.selectionRegion.dimensions = {
912 this.#setState(STATES.DRAGGING_READY);
916 * If the background is clicked we set the state to crosshairs
917 * otherwise set the state to resizing
918 * @param {Number} pageX The x position relative to the page
919 * @param {Number} pageY The y position relative to the page
920 * @param {String} targetId The id of the event target
922 selectedDragStart(pageX, pageY, targetId) {
923 if (targetId === this.screenshotsContainer.id) {
924 this.#setState(STATES.CROSSHAIRS);
927 this.#moverId = targetId;
928 this.#lastPageX = pageX;
929 this.#lastPageY = pageY;
931 this.#setState(STATES.RESIZING);
935 * Draw the eyes in the preview container and find the element currently
937 * @param {Number} clientX The x position relative to the viewport
938 * @param {Number} clientY The y position relative to the viewport
940 crosshairsMove(clientX, clientY) {
941 this.drawPreviewEyes(clientX, clientY);
943 this.handleElementHover(clientX, clientY);
947 * Set the selection region dimensions and if the region is at least 40
948 * pixels diagnally in distance, set the state to dragging.
949 * @param {Number} pageX The x position relative to the page
950 * @param {Number} pageY The y position relative to the page
952 draggingReadyDrag(pageX, pageY) {
953 this.selectionRegion.dimensions = {
958 if (this.selectionRegion.distance > 40) {
959 this.#setState(STATES.DRAGGING);
964 * Scroll if along the edge of the viewport, update the selection region
965 * dimensions and draw the selection container.
966 * @param {Number} pageX The x position relative to the page
967 * @param {Number} pageY The y position relative to the page
969 draggingDrag(pageX, pageY) {
970 this.scrollIfByEdge(pageX, pageY);
971 this.selectionRegion.dimensions = {
976 this.drawSelectionContainer();
980 * Resize the selection region depending on the mover that started the resize.
981 * @param {Number} pageX The x position relative to the page
982 * @param {Number} pageY The y position relative to the page
984 resizingDrag(pageX, pageY) {
985 this.scrollIfByEdge(pageX, pageY);
986 switch (this.#moverId) {
987 case "mover-topLeft": {
988 this.selectionRegion.dimensions = {
995 this.selectionRegion.dimensions = { top: pageY };
998 case "mover-topRight": {
999 this.selectionRegion.dimensions = {
1005 case "mover-right": {
1006 this.selectionRegion.dimensions = {
1011 case "mover-bottomRight": {
1012 this.selectionRegion.dimensions = {
1018 case "mover-bottom": {
1019 this.selectionRegion.dimensions = {
1024 case "mover-bottomLeft": {
1025 this.selectionRegion.dimensions = {
1031 case "mover-left": {
1032 this.selectionRegion.dimensions = { left: pageX };
1036 let diffX = this.#lastPageX - pageX;
1037 let diffY = this.#lastPageY - pageY;
1044 // Unpack dimensions to use here
1052 } = this.selectionRegion.dimensions;
1053 let { scrollWidth, scrollHeight } = this.windowDimensions.dimensions;
1055 // wait until all 4 if elses have completed before setting box dimensions
1056 if (boxWidth <= this.#previousDimensions.width && boxLeft === 0) {
1057 newLeft = boxRight - this.#previousDimensions.width;
1063 boxWidth <= this.#previousDimensions.width &&
1064 boxRight === scrollWidth
1066 newRight = boxLeft + this.#previousDimensions.width;
1068 newRight = boxRight;
1071 if (boxHeight <= this.#previousDimensions.height && boxTop === 0) {
1072 newTop = boxBottom - this.#previousDimensions.height;
1078 boxHeight <= this.#previousDimensions.height &&
1079 boxBottom === scrollHeight
1081 newBottom = boxTop + this.#previousDimensions.height;
1083 newBottom = boxBottom;
1086 this.selectionRegion.dimensions = {
1087 left: newLeft - diffX,
1088 top: newTop - diffY,
1089 right: newRight - diffX,
1090 bottom: newBottom - diffY,
1093 this.#lastPageX = pageX;
1094 this.#lastPageY = pageY;
1098 this.drawSelectionContainer();
1102 * If there is a valid element region, update and draw the selection
1103 * container and set the state to selected.
1104 * Otherwise set the state to crosshairs.
1106 draggingReadyDragEnd() {
1107 if (this.hoverElementRegion.isRegionValid) {
1108 this.selectionRegion.dimensions = this.hoverElementRegion.dimensions;
1109 this.#setState(STATES.SELECTED);
1110 this.downloadButton.focus();
1111 this.#dispatchEvent("Screenshots:RecordEvent", {
1112 eventName: "selected",
1115 this.#methodsUsed.element += 1;
1117 this.#setState(STATES.CROSSHAIRS);
1122 * Update the selection region dimensions and set the state to selected.
1123 * @param {Number} pageX The x position relative to the page
1124 * @param {Number} pageY The y position relative to the page
1126 draggingDragEnd(pageX, pageY) {
1127 this.selectionRegion.dimensions = {
1131 this.selectionRegion.sortCoords();
1132 this.#setState(STATES.SELECTED);
1133 this.maybeRecordRegionSelected();
1134 this.#methodsUsed.region += 1;
1135 this.downloadButton.focus();
1139 * Update the selection region dimensions by calling `resizingDrag` and set
1140 * the state to selected.
1141 * @param {Number} pageX The x position relative to the page
1142 * @param {Number} pageY The y position relative to the page
1144 resizingDragEnd(pageX, pageY) {
1145 this.resizingDrag(pageX, pageY);
1146 this.selectionRegion.sortCoords();
1147 this.#setState(STATES.SELECTED);
1148 this.maybeRecordRegionSelected();
1149 if (this.#moverId === "highlight") {
1150 this.#methodsUsed.move += 1;
1152 this.#methodsUsed.resize += 1;
1156 maybeRecordRegionSelected() {
1157 let { width, height } = this.selectionRegion.dimensions;
1160 !this.#previousDimensions ||
1161 (Math.abs(this.#previousDimensions.width - width) >
1162 REGION_CHANGE_THRESHOLD &&
1163 Math.abs(this.#previousDimensions.height - height) >
1164 REGION_CHANGE_THRESHOLD)
1166 this.#dispatchEvent("Screenshots:RecordEvent", {
1167 eventName: "selected",
1168 reason: "region_selection",
1171 this.#previousDimensions = { width, height };
1175 * Draw the preview eyes pointer towards the mouse.
1176 * @param {Number} clientX The x position relative to the viewport
1177 * @param {Number} clientY The y position relative to the viewport
1179 drawPreviewEyes(clientX, clientY) {
1180 let { clientWidth, clientHeight } = this.windowDimensions.dimensions;
1181 const xpos = Math.floor((10 * (clientX - clientWidth / 2)) / clientWidth);
1182 const ypos = Math.floor((10 * (clientY - clientHeight / 2)) / clientHeight);
1183 const move = `transform:translate(${xpos}px, ${ypos}px);`;
1184 this.leftEye.style = move;
1185 this.rightEye.style = move;
1188 showPreviewContainer() {
1189 this.previewContainer.hidden = false;
1192 hidePreviewContainer() {
1193 this.previewContainer.hidden = true;
1196 updatePreviewContainer() {
1197 let { clientWidth, clientHeight } = this.windowDimensions.dimensions;
1198 this.previewContainer.style.width = `${clientWidth}px`;
1199 this.previewContainer.style.height = `${clientHeight}px`;
1203 * Update the screenshots overlay container based on the window dimensions.
1205 updateScreenshotsOverlayContainer() {
1206 let { scrollWidth, scrollHeight } = this.windowDimensions.dimensions;
1207 this.screenshotsContainer.style = `width:${scrollWidth}px;height:${scrollHeight}px;`;
1210 showScreenshotsOverlayContainer() {
1211 this.screenshotsContainer.hidden = false;
1214 hideScreenshotsOverlayContainer() {
1215 this.screenshotsContainer.hidden = true;
1219 * Draw the hover element container based on the hover element region.
1221 drawHoverElementRegion() {
1222 this.showHoverElementContainer();
1224 let { top, left, width, height } = this.hoverElementRegion.dimensions;
1226 this.hoverElementContainer.style = `top:${top}px;left:${left}px;width:${width}px;height:${height}px;`;
1229 showHoverElementContainer() {
1230 this.hoverElementContainer.hidden = false;
1233 hideHoverElementContainer() {
1234 this.hoverElementContainer.hidden = true;
1238 * Draw each background element and the highlight element base on the
1241 drawSelectionContainer() {
1242 this.showSelectionContainer();
1244 let { top, left, right, bottom, width, height } =
1245 this.selectionRegion.dimensions;
1247 this.highlightEl.style = `top:${top}px;left:${left}px;width:${width}px;height:${height}px;`;
1249 this.leftBackgroundEl.style = `top:${top}px;width:${left}px;height:${height}px;`;
1250 this.topBackgroundEl.style.height = `${top}px`;
1251 this.rightBackgroundEl.style = `top:${top}px;left:${right}px;width:calc(100% - ${right}px);height:${height}px;`;
1252 this.bottomBackgroundEl.style = `top:${bottom}px;height:calc(100% - ${bottom}px);`;
1254 this.updateSelectionSizeText();
1257 updateSelectionSizeText() {
1258 let dpr = this.windowDimensions.devicePixelRatio;
1259 let { width, height } = this.selectionRegion.dimensions;
1261 let [selectionSizeTranslation] =
1262 lazy.overlayLocalization.formatMessagesSync([
1264 id: "screenshots-overlay-selection-region-size",
1266 width: Math.floor(width * dpr),
1267 height: Math.floor(height * dpr),
1271 this.selectionSize.textContent = selectionSizeTranslation.value;
1274 showSelectionContainer() {
1275 this.selectionContainer.hidden = false;
1278 hideSelectionContainer() {
1279 this.selectionContainer.hidden = true;
1283 * Draw the buttons container in the bottom right corner of the selection
1284 * container if possible.
1285 * The buttons will be visible in the viewport if the selection container
1286 * is within the viewport, otherwise skip drawing the buttons.
1288 drawButtonsContainer() {
1289 this.showButtonsContainer();
1296 } = this.selectionRegion.dimensions;
1297 let { clientWidth, clientHeight, scrollX, scrollY } =
1298 this.windowDimensions.dimensions;
1301 boxTop > scrollY + clientHeight ||
1302 boxBottom < scrollY ||
1303 boxLeft > scrollX + clientWidth ||
1306 // The box is offscreen so need to draw the buttons
1310 let top = boxBottom;
1312 if (scrollY + clientHeight - boxBottom < 70) {
1313 if (boxBottom < scrollY + clientHeight) {
1314 top = boxBottom - 60;
1315 } else if (scrollY + clientHeight - boxTop < 70) {
1318 top = scrollY + clientHeight - 60;
1322 if (boxRight < 300) {
1323 this.buttonsContainer.style.left = `${boxLeft}px`;
1324 this.buttonsContainer.style.right = "";
1326 this.buttonsContainer.style.right = `calc(100% - ${boxRight}px)`;
1327 this.buttonsContainer.style.left = "";
1330 this.buttonsContainer.style.top = `${top}px`;
1333 showButtonsContainer() {
1334 this.buttonsContainer.hidden = false;
1337 hideButtonsContainer() {
1338 this.buttonsContainer.hidden = true;
1342 * Set the pointer events to none on the screenshots elements so
1343 * elementFromPoint can find the real element at the given point.
1345 setPointerEventsNone() {
1346 this.screenshotsContainer.style.pointerEvents = "none";
1349 resetPointerEvents() {
1350 this.screenshotsContainer.style.pointerEvents = "";
1354 * Try to find a reasonable element for a given point.
1355 * If a reasonable element is found, draw the hover element container for
1356 * that element region.
1357 * @param {Number} clientX The x position relative to the viewport
1358 * @param {Number} clientY The y position relative to the viewport
1360 handleElementHover(clientX, clientY) {
1361 this.setPointerEventsNone();
1362 let ele = this.document.elementFromPoint(clientX, clientY);
1363 this.resetPointerEvents();
1365 if (this.#cachedEle && this.#cachedEle === ele) {
1366 // Still hovering over the same element
1369 this.#cachedEle = ele;
1371 let rect = getBestRectForElement(ele, this.document);
1373 let { scrollX, scrollY } = this.windowDimensions.dimensions;
1374 let { left, top, right, bottom } = rect;
1376 left: left + scrollX,
1378 right: right + scrollX,
1379 bottom: bottom + scrollY,
1381 this.hoverElementRegion.dimensions = newRect;
1382 this.drawHoverElementRegion();
1384 this.hoverElementRegion.resetDimensions();
1385 this.hideHoverElementContainer();
1390 * Scroll the viewport if near one or both of the edges.
1391 * @param {Number} pageX The x position relative to the page
1392 * @param {Number} pageY The y position relative to the page
1394 scrollIfByEdge(pageX, pageY) {
1395 let { scrollX, scrollY, clientWidth, clientHeight } =
1396 this.windowDimensions.dimensions;
1398 if (pageY - scrollY < SCROLL_BY_EDGE) {
1400 this.scrollWindow(0, -(SCROLL_BY_EDGE - (pageY - scrollY)));
1401 } else if (scrollY + clientHeight - pageY < SCROLL_BY_EDGE) {
1403 this.scrollWindow(0, SCROLL_BY_EDGE - (scrollY + clientHeight - pageY));
1406 if (pageX - scrollX <= SCROLL_BY_EDGE) {
1408 this.scrollWindow(-(SCROLL_BY_EDGE - (pageX - scrollX)), 0);
1409 } else if (scrollX + clientWidth - pageX <= SCROLL_BY_EDGE) {
1411 this.scrollWindow(SCROLL_BY_EDGE - (scrollX + clientWidth - pageX), 0);
1416 * Scroll the window by the given amount.
1417 * @param {Number} x The x amount to scroll
1418 * @param {Number} y The y amount to scroll
1420 scrollWindow(x, y) {
1421 this.window.scrollBy(x, y);
1422 this.updateScreenshotsOverlayDimensions("scroll");
1426 * The page was resized or scrolled. We need to update the screenshots
1427 * container size so we don't draw outside the page bounds.
1428 * @param {String} eventType will be "scroll" or "resize"
1430 updateScreenshotsOverlayDimensions(eventType) {
1431 this.updateWindowDimensions();
1433 if (this.#state === STATES.CROSSHAIRS) {
1434 if (eventType === "resize") {
1435 this.hideHoverElementContainer();
1436 this.updatePreviewContainer();
1437 } else if (eventType === "scroll") {
1438 if (this.#lastClientX && this.#lastClientY) {
1439 this.#cachedEle = null;
1440 this.handleElementHover(this.#lastClientX, this.#lastClientY);
1443 } else if (this.#state === STATES.SELECTED) {
1444 this.drawButtonsContainer();
1445 this.updateSelectionSizeText();
1450 * Returns the window's dimensions for the current window.
1452 * @return {Object} An object containing window dimensions
1454 * clientWidth: The width of the viewport
1455 * clientHeight: The height of the viewport
1456 * scrollWidth: The width of the enitre page
1457 * scrollHeight: The height of the entire page
1458 * scrollX: The X scroll offset of the viewport
1459 * scrollY: The Y scroll offest of the viewport
1460 * scrollMinX: The X mininmun the viewport can scroll to
1461 * scrollMinY: The Y mininmun the viewport can scroll to
1464 getDimensionsFromWindow() {
1476 let scrollWidth = innerWidth + scrollMaxX - scrollMinX;
1477 let scrollHeight = innerHeight + scrollMaxY - scrollMinY;
1478 let clientHeight = innerHeight;
1479 let clientWidth = innerWidth;
1481 const scrollbarHeight = {};
1482 const scrollbarWidth = {};
1483 this.window.windowUtils.getScrollbarSize(
1488 scrollWidth -= scrollbarWidth.value;
1489 scrollHeight -= scrollbarHeight.value;
1490 clientWidth -= scrollbarWidth.value;
1491 clientHeight -= scrollbarHeight.value;
1506 * Update the screenshots container because the window has changed size of
1507 * scrolled. The screenshots-overlay-container doesn't shrink with the page
1508 * when the window is resized so we have to manually find the width and
1509 * height of the page and make sure we aren't drawing outside the actual page
1512 updateWindowDimensions() {
1522 } = this.getDimensionsFromWindow();
1524 let shouldUpdate = true;
1527 clientHeight < this.windowDimensions.clientHeight ||
1528 clientWidth < this.windowDimensions.clientWidth
1530 let widthDiff = this.windowDimensions.clientWidth - clientWidth;
1531 let heightDiff = this.windowDimensions.clientHeight - clientHeight;
1533 this.windowDimensions.dimensions = {
1534 scrollWidth: scrollWidth - Math.max(widthDiff, 0),
1535 scrollHeight: scrollHeight - Math.max(heightDiff, 0),
1540 if (this.#state === STATES.SELECTED) {
1541 let didShift = this.selectionRegion.shift();
1543 this.drawSelectionContainer();
1544 this.drawButtonsContainer();
1546 } else if (this.#state === STATES.CROSSHAIRS) {
1547 this.updatePreviewContainer();
1549 this.updateScreenshotsOverlayContainer();
1550 // We just updated the screenshots container so we check if the window
1551 // dimensions are still accurate
1552 let { scrollWidth: updatedWidth, scrollHeight: updatedHeight } =
1553 this.getDimensionsFromWindow();
1555 // If the width and height are the same then we don't need to draw the overlay again
1556 if (updatedWidth === scrollWidth && updatedHeight === scrollHeight) {
1557 shouldUpdate = false;
1560 scrollWidth = updatedWidth;
1561 scrollHeight = updatedHeight;
1564 setMaxDetectHeight(Math.max(clientHeight + 100, 700));
1565 setMaxDetectWidth(Math.max(clientWidth + 100, 1000));
1567 this.windowDimensions.dimensions = {
1576 devicePixelRatio: this.window.devicePixelRatio,
1580 this.updatePreviewContainer();
1581 this.updateScreenshotsOverlayContainer();