ScreenshotsOverlayChild.sys.mjs (61771B)
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/. */ 4 5 /** 6 * The Screenshots overlay is inserted into the document's 7 * anonymous content container (see dom/webidl/Document.webidl). 8 * 9 * This container gets cleared automatically when the document navigates. 10 * 11 * To retrieve the AnonymousContent instance, use the `content` getter. 12 */ 13 14 /* 15 * Below are the states of the screenshots overlay 16 * States: 17 * "crosshairs": 18 * Nothing has happened, and the crosshairs will follow the movement of the mouse 19 * "draggingReady": 20 * The user has pressed the mouse button, but hasn't moved enough to create a selection 21 * "dragging": 22 * The user has pressed down a mouse button, and is dragging out an area far enough to show a selection 23 * "selected": 24 * The user has selected an area 25 * "resizing": 26 * The user is resizing the selection 27 */ 28 29 import { 30 setMaxDetectHeight, 31 setMaxDetectWidth, 32 getBestRectForElement, 33 getElementFromPoint, 34 Region, 35 WindowDimensions, 36 } from "chrome://browser/content/screenshots/overlayHelpers.mjs"; 37 38 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 39 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 40 import { ShortcutUtils } from "resource://gre/modules/ShortcutUtils.sys.mjs"; 41 42 const STATES = { 43 CROSSHAIRS: "crosshairs", 44 DRAGGING_READY: "draggingReady", 45 DRAGGING: "dragging", 46 SELECTED: "selected", 47 RESIZING: "resizing", 48 }; 49 50 const lazy = {}; 51 52 ChromeUtils.defineLazyGetter(lazy, "overlayLocalization", () => { 53 return new Localization(["browser/screenshots.ftl"], true); 54 }); 55 56 const SCREENSHOTS_LAST_SAVED_METHOD_PREF = 57 "screenshots.browser.component.last-saved-method"; 58 59 XPCOMUtils.defineLazyPreferenceGetter( 60 lazy, 61 "SCREENSHOTS_LAST_SAVED_METHOD", 62 SCREENSHOTS_LAST_SAVED_METHOD_PREF, 63 "download" 64 ); 65 66 const REGION_CHANGE_THRESHOLD = 5; 67 const SCROLL_BY_EDGE = 20; 68 69 export class ScreenshotsOverlay { 70 #content; 71 #initialized = false; 72 #state = ""; 73 #moverId; 74 #cachedEle; 75 #lastPageX; 76 #lastPageY; 77 #lastClientX; 78 #lastClientY; 79 #previousDimensions; 80 #methodsUsed; 81 82 get markup() { 83 let accelString = ShortcutUtils.getModifierString("accel"); 84 let copyShorcut = accelString + this.copyKey; 85 let downloadShortcut = accelString + this.downloadKey; 86 87 let [ 88 cancelLabel, 89 cancelAttributes, 90 instructions, 91 downloadAttributes, 92 copyAttributes, 93 previewFaceAriaLabel, 94 ] = lazy.overlayLocalization.formatMessagesSync([ 95 { id: "screenshots-cancel-button" }, 96 { id: "screenshots-component-cancel-button" }, 97 { id: "screenshots-instructions" }, 98 { 99 id: "screenshots-component-download-button-2", 100 args: { shortcut: downloadShortcut }, 101 }, 102 { 103 id: "screenshots-component-copy-button-2", 104 args: { shortcut: copyShorcut }, 105 }, 106 { id: "screenshots-overlay-preview-face-label" }, 107 ]); 108 109 return ` 110 <template> 111 <link rel="stylesheet" href="chrome://browser/content/screenshots/overlay/overlay.css" /> 112 <div id="screenshots-component"> 113 <div id="preview-container" hidden> 114 <div id="face-container" tabindex="0" role="button" aria-label="${previewFaceAriaLabel.attributes[0].value}"> 115 <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"> 116 <g> 117 <path d="M11.4.9v2.9h-6c-.9 0-1.5.8-1.5 1.5v6H.8V3.8C.8 2.1 2.2.7 3.9.7h7.6v.2z" class="face-line-color"/> 118 <path d="M63.2 11.4h-3.1v-6c0-.8-.6-1.5-1.5-1.5h-6v-3h7.6c1.7 0 3.1 1.4 3.1 3.1z" class="face-line-color"/> 119 <path d="M52.6 63.2v-3.1h6c.9 0 1.5-.6 1.5-1.5v-6h3.1v7.6c0 1.7-1.4 3.1-3.1 3.1z" class="face-line-color"/> 120 <path d="M.8 52.7h3.1v6c0 .9.6 1.5 1.5 1.5h6v3.1H3.8c-1.7 0-3.1-1.4-3.1-3.1z" class="face-line-color"/> 121 <path d="M33.3 49.2H33c-4.6-.1-7.8-3.6-7.9-3.8-.6-.8-.6-2 .1-2.7.8-.8 1.9-.6 2.6.1 0 0 2.3 2.6 5.2 2.6 1.8 0 3.6-.9 5.2-2.6.8-.8 1.9-.8 2.7 0 .8.8.8 1.9 0 2.7-2.2 2.4-4.9 3.7-7.6 3.7z" class="face-line-color" style="display:inline"/> 122 <ellipse id="leftEye" cx="23" cy="26" class="face-line-color" rx="5" ry="7"/> 123 <ellipse id="rightEye" cx="43" cy="26" class="face-line-color" rx="5" ry="7"/> 124 <ellipse id="leftPupil" cx="25" cy="30" class="face-pupil-color" rx="3" ry="3"/> 125 <ellipse id="rightPupil" cx="45" cy="30" class="face-pupil-color" rx="3" ry="3"/> 126 </g> 127 </svg> 128 129 </div> 130 <div class="preview-instructions">${instructions.value}</div> 131 <button class="screenshots-button ghost-button" id="screenshots-cancel-button" title="${cancelAttributes.attributes[0].value}" aria-label="${cancelAttributes.attributes[1].value}">${cancelLabel.value}</button> 132 </div> 133 <div id="hover-highlight" hidden></div> 134 <div id="selection-container" hidden> 135 <div id="top-background" class="bghighlight"></div> 136 <div id="bottom-background" class="bghighlight"></div> 137 <div id="left-background" class="bghighlight"></div> 138 <div id="right-background" class="bghighlight"></div> 139 <div id="highlight" class="highlight" tabindex="0"> 140 <div id="mover-topLeft" class="mover-target direction-topLeft" tabindex="0"> 141 <div class="mover"></div> 142 </div> 143 <div id="mover-top" class="mover-target direction-top"> 144 <div class="mover"></div> 145 </div> 146 <div id="mover-topRight" class="mover-target direction-topRight" tabindex="0"> 147 <div class="mover"></div> 148 </div> 149 <div id="mover-right" class="mover-target direction-right"> 150 <div class="mover"></div> 151 </div> 152 <div id="mover-bottomRight" class="mover-target direction-bottomRight" tabindex="0"> 153 <div class="mover"></div> 154 </div> 155 <div id="mover-bottom" class="mover-target direction-bottom"> 156 <div class="mover"></div> 157 </div> 158 <div id="mover-bottomLeft" class="mover-target direction-bottomLeft" tabindex="0"> 159 <div class="mover"></div> 160 </div> 161 <div id="mover-left" class="mover-target direction-left"> 162 <div class="mover"></div> 163 </div> 164 <div id="selection-size-container"> 165 <span id="selection-size" dir="ltr"></span> 166 </div> 167 </div> 168 </div> 169 <div id="buttons-container" hidden> 170 <div class="buttons-wrapper"> 171 <button id="cancel" class="screenshots-button" title="${cancelAttributes.attributes[0].value}" aria-label="${cancelAttributes.attributes[1].value}"><img/></button> 172 <button id="copy" class="screenshots-button" title="${copyAttributes.attributes[0].value}" aria-label="${copyAttributes.attributes[1].value}"><img/><label>${copyAttributes.value}</label></button> 173 <button id="download" class="screenshots-button primary" title="${downloadAttributes.attributes[0].value}" aria-label="${downloadAttributes.attributes[1].value}"><img/><label>${downloadAttributes.value}</label></button> 174 </div> 175 </div> 176 </div> 177 </template>`; 178 } 179 180 get fragment() { 181 if (!this.overlayTemplate) { 182 let parser = new DOMParser(); 183 let doc = parser.parseFromString(this.markup, "text/html"); 184 this.overlayTemplate = this.document.importNode( 185 doc.querySelector("template"), 186 true 187 ); 188 } 189 let fragment = this.overlayTemplate.content.cloneNode(true); 190 return fragment; 191 } 192 193 get initialized() { 194 return this.#initialized; 195 } 196 197 get state() { 198 return this.#state; 199 } 200 201 get methodsUsed() { 202 return this.#methodsUsed; 203 } 204 205 constructor(contentDocument) { 206 this.document = contentDocument; 207 this.window = contentDocument.ownerGlobal; 208 209 this.windowDimensions = new WindowDimensions(); 210 this.selectionRegion = new Region(this.windowDimensions); 211 this.hoverElementRegion = new Region(this.windowDimensions); 212 this.resetMethodsUsed(); 213 214 let [downloadKey, copyKey] = lazy.overlayLocalization.formatMessagesSync([ 215 { id: "screenshots-component-download-key" }, 216 { id: "screenshots-component-copy-key" }, 217 ]); 218 219 this.downloadKey = downloadKey.value; 220 this.copyKey = copyKey.value; 221 } 222 223 get content() { 224 if (!this.#content || Cu.isDeadWrapper(this.#content)) { 225 return null; 226 } 227 return this.#content; 228 } 229 230 getElementById(id) { 231 return this.content.root.getElementById(id); 232 } 233 234 async initialize() { 235 if (this.initialized) { 236 return; 237 } 238 239 this.windowDimensions.reset(); 240 241 this.#content = this.document.insertAnonymousContent(); 242 this.#content.root.appendChild(this.fragment); 243 244 this.initializeElements(); 245 this.screenshotsContainer.dir = Services.locale.isAppLocaleRTL 246 ? "rtl" 247 : "ltr"; 248 await this.updateWindowDimensions(); 249 250 this.#setState(STATES.CROSSHAIRS); 251 252 this.selection = this.window.getSelection(); 253 this.ranges = []; 254 for (let i = 0; i < this.selection.rangeCount; i++) { 255 this.ranges.push(this.selection.getRangeAt(i)); 256 } 257 258 this.#initialized = true; 259 } 260 261 /** 262 * Get all the elements that will be used. 263 */ 264 initializeElements() { 265 this.previewCancelButton = this.getElementById("screenshots-cancel-button"); 266 this.cancelButton = this.getElementById("cancel"); 267 this.copyButton = this.getElementById("copy"); 268 this.downloadButton = this.getElementById("download"); 269 270 this.previewContainer = this.getElementById("preview-container"); 271 this.previewFace = this.getElementById("face-container"); 272 this.hoverElementContainer = this.getElementById("hover-highlight"); 273 this.selectionContainer = this.getElementById("selection-container"); 274 this.buttonsContainer = this.getElementById("buttons-container"); 275 this.screenshotsContainer = this.getElementById("screenshots-component"); 276 277 this.leftEye = this.getElementById("leftPupil"); 278 this.rightEye = this.getElementById("rightPupil"); 279 280 this.leftBackgroundEl = this.getElementById("left-background"); 281 this.topBackgroundEl = this.getElementById("top-background"); 282 this.rightBackgroundEl = this.getElementById("right-background"); 283 this.bottomBackgroundEl = this.getElementById("bottom-background"); 284 this.highlightEl = this.getElementById("highlight"); 285 286 this.topLeftMover = this.getElementById("mover-topLeft"); 287 this.topRightMover = this.getElementById("mover-topRight"); 288 this.bottomLeftMover = this.getElementById("mover-bottomLeft"); 289 this.bottomRightMover = this.getElementById("mover-bottomRight"); 290 291 this.selectionSize = this.getElementById("selection-size"); 292 } 293 294 /** 295 * Removes all event listeners and removes the overlay from the Anonymous Content 296 */ 297 tearDown(options = {}) { 298 if (this.#content) { 299 if (!(options.doNotResetMethods === true)) { 300 this.resetMethodsUsed(); 301 } 302 try { 303 this.document.removeAnonymousContent(this.#content); 304 } catch (e) { 305 // If the current window isn't the one the content was inserted into, this 306 // will fail, but that's fine. 307 } 308 } 309 this.#initialized = false; 310 this.#setState(""); 311 } 312 313 resetMethodsUsed() { 314 this.#methodsUsed = { 315 element: 0, 316 region: 0, 317 move: 0, 318 resize: 0, 319 }; 320 } 321 322 focus(direction) { 323 if (direction === "backward") { 324 this.previewCancelButton.focus({ focusVisible: true }); 325 } else { 326 this.previewFace.focus({ focusVisible: true }); 327 } 328 } 329 330 /** 331 * Returns the x and y coordinates of the event relative to both the 332 * viewport and the page. 333 * 334 * @param {Event} event The event 335 * @returns 336 * { 337 * clientX: The x position relative to the viewport 338 * clientY: The y position relative to the viewport 339 * pageX: The x position relative to the entire page 340 * pageY: The y position relative to the entire page 341 * } 342 */ 343 getCoordinatesFromEvent(event) { 344 let { clientX, clientY, pageX, pageY } = event; 345 pageX -= this.windowDimensions.scrollMinX; 346 pageY -= this.windowDimensions.scrollMinY; 347 348 return { clientX, clientY, pageX, pageY }; 349 } 350 351 handleEvent(event) { 352 switch (event.type) { 353 case "click": 354 this.handleClick(event); 355 break; 356 case "pointerdown": 357 this.handlePointerDown(event); 358 break; 359 case "pointermove": 360 this.handlePointerMove(event); 361 break; 362 case "pointerup": 363 this.handlePointerUp(event); 364 break; 365 case "keydown": 366 this.handleKeyDown(event); 367 break; 368 case "keyup": 369 this.handleKeyUp(event); 370 break; 371 case "selectionchange": 372 this.handleSelectionChange(); 373 break; 374 } 375 } 376 377 /** 378 * If the event came from the primary button, return false as we should not 379 * early return in the event handler function. 380 * If the event had another button, set to the crosshairs or selected state 381 * and return true to early return from the event handler function. 382 * 383 * @param {PointerEvent} event 384 * @returns true if the event button(s) was the non primary button 385 * false otherwise 386 */ 387 preEventHandler(event) { 388 if (event.button > 0 || event.buttons > 1) { 389 switch (this.#state) { 390 case STATES.DRAGGING_READY: 391 this.#setState(STATES.CROSSHAIRS); 392 break; 393 case STATES.DRAGGING: 394 case STATES.RESIZING: 395 this.#setState(STATES.SELECTED); 396 break; 397 } 398 return true; 399 } 400 return false; 401 } 402 403 handleClick(event) { 404 if (this.preEventHandler(event)) { 405 return; 406 } 407 408 switch (event.originalTarget.id) { 409 case "screenshots-cancel-button": 410 case "cancel": 411 this.maybeCancelScreenshots(); 412 break; 413 case "copy": 414 this.copySelectedRegion(); 415 break; 416 case "download": 417 this.downloadSelectedRegion(); 418 break; 419 } 420 } 421 422 maybeCancelScreenshots() { 423 if (this.#state === STATES.CROSSHAIRS) { 424 this.#dispatchEvent("Screenshots:Close", { 425 reason: "OverlayCancel", 426 }); 427 } else { 428 this.#setState(STATES.CROSSHAIRS); 429 } 430 } 431 432 /** 433 * Handles the pointerdown event depending on the state. 434 * Early return when a pointer down happens on a button. 435 * 436 * @param {Event} event The pointerown event 437 */ 438 handlePointerDown(event) { 439 // Early return if the event target is not within the screenshots component 440 // element. 441 if (!event.originalTarget.closest("#screenshots-component")) { 442 return; 443 } 444 445 if (this.preEventHandler(event)) { 446 return; 447 } 448 449 if ( 450 event.originalTarget.id === "screenshots-cancel-button" || 451 event.originalTarget.closest("#buttons-container") === 452 this.buttonsContainer 453 ) { 454 event.stopPropagation(); 455 return; 456 } 457 458 const { pageX, pageY } = this.getCoordinatesFromEvent(event); 459 460 switch (this.#state) { 461 case STATES.CROSSHAIRS: { 462 this.crosshairsDragStart(pageX, pageY); 463 break; 464 } 465 case STATES.SELECTED: { 466 this.selectedDragStart(pageX, pageY, event.originalTarget.id); 467 break; 468 } 469 } 470 } 471 472 /** 473 * Handles the pointermove event depending on the state 474 * 475 * @param {Event} event The pointermove event 476 */ 477 handlePointerMove(event) { 478 if (this.preEventHandler(event)) { 479 return; 480 } 481 482 const { pageX, pageY, clientX, clientY } = 483 this.getCoordinatesFromEvent(event); 484 485 switch (this.#state) { 486 case STATES.CROSSHAIRS: { 487 this.crosshairsMove(clientX, clientY); 488 break; 489 } 490 case STATES.DRAGGING_READY: { 491 this.draggingReadyDrag(pageX, pageY); 492 break; 493 } 494 case STATES.DRAGGING: { 495 this.draggingDrag(pageX, pageY); 496 break; 497 } 498 case STATES.RESIZING: { 499 this.resizingDrag(pageX, pageY); 500 break; 501 } 502 } 503 } 504 505 /** 506 * Handles the pointerup event depending on the state 507 * 508 * @param {Event} event The pointerup event 509 */ 510 handlePointerUp(event) { 511 const { pageX, pageY, clientX, clientY } = 512 this.getCoordinatesFromEvent(event); 513 514 switch (this.#state) { 515 case STATES.DRAGGING_READY: { 516 this.draggingReadyDragEnd(pageX - clientX, pageY - clientY); 517 break; 518 } 519 case STATES.DRAGGING: { 520 this.draggingDragEnd(pageX, pageY, event.originalTarget.id); 521 break; 522 } 523 case STATES.RESIZING: { 524 this.resizingDragEnd(pageX, pageY); 525 break; 526 } 527 } 528 } 529 530 /** 531 * Handles when a keydown occurs in the screenshots component. 532 * 533 * @param {Event} event The keydown event 534 */ 535 handleKeyDown(event) { 536 if (event.key === "Escape") { 537 this.maybeCancelScreenshots(); 538 return; 539 } 540 541 switch (this.#state) { 542 case STATES.CROSSHAIRS: 543 this.crosshairsKeyDown(event); 544 break; 545 case STATES.DRAGGING: 546 this.draggingKeyDown(event); 547 break; 548 case STATES.RESIZING: 549 this.resizingKeyDown(event); 550 break; 551 case STATES.SELECTED: 552 this.selectedKeyDown(event); 553 break; 554 } 555 } 556 557 /** 558 * Handles when a keyup occurs in the screenshots component. 559 * All we need to do on keyup is set the state to selected. 560 * 561 * @param {Event} event The keydown event 562 */ 563 handleKeyUp(event) { 564 switch (this.#state) { 565 case STATES.RESIZING: 566 switch (event.key) { 567 case "ArrowLeft": 568 case "ArrowUp": 569 case "ArrowRight": 570 case "ArrowDown": 571 switch (event.originalTarget.id) { 572 case "highlight": 573 case "mover-bottomLeft": 574 case "mover-bottomRight": 575 case "mover-topLeft": 576 case "mover-topRight": 577 this.#setState(STATES.SELECTED, { doNotMoveFocus: true }); 578 break; 579 } 580 break; 581 } 582 break; 583 } 584 } 585 586 /** 587 * Gets the accel key depending on the platform. 588 * metaKey for macOS. ctrlKey for Windows and Linux. 589 * 590 * @param {Event} event The keydown event 591 * @returns {boolean} True if the accel key is pressed, false otherwise. 592 */ 593 getAccelKey(event) { 594 if (AppConstants.platform === "macosx") { 595 return event.metaKey; 596 } 597 return event.ctrlKey; 598 } 599 600 crosshairsKeyDown(event) { 601 switch (event.key) { 602 case "ArrowLeft": 603 case "ArrowUp": 604 case "ArrowRight": 605 case "ArrowDown": 606 // Do nothing so we can prevent default below 607 break; 608 case "Tab": 609 this.maybeLockFocus(event); 610 return; 611 case "Enter": 612 if (this.handleKeyDownOnButton(event)) { 613 return; 614 } 615 616 if (this.hoverElementRegion.isRegionValid) { 617 this.draggingReadyStart(); 618 this.draggingReadyDragEnd(); 619 return; 620 } 621 // eslint-disable-next-line no-fallthrough 622 case " ": { 623 if (this.handleKeyDownOnButton(event)) { 624 return; 625 } 626 627 if (Services.appinfo.isWayland) { 628 return; 629 } 630 631 // If the preview face is focused, create a region from the preview 632 // face and move focus to the bottom right mover for adjustments 633 if (Services.focus.focusedElement === this.previewFace) { 634 let rect = this.previewFace.getBoundingClientRect(); 635 this.hoverElementRegion.dimensions = rect; 636 this.draggingReadyStart(); 637 this.draggingReadyDragEnd({ doNotMoveFocus: true }); 638 this.bottomRightMover.focus({ focusVisible: true }); 639 return; 640 } 641 642 if (Services.appinfo.isWayland) { 643 return; 644 } 645 646 // The left and top coordinates from cursorRegion are relative to 647 // the client window so we need to add the scroll offset of the page to 648 // get the correct coordinates. 649 let x = {}; 650 let y = {}; 651 this.window.windowUtils.getLastOverWindowPointerLocationInCSSPixels( 652 x, 653 y 654 ); 655 this.crosshairsDragStart( 656 x.value + this.windowDimensions.scrollX, 657 y.value + this.windowDimensions.scrollY 658 ); 659 this.#setState(STATES.DRAGGING); 660 break; 661 } 662 } 663 } 664 665 /** 666 * Handles a keydown event for the dragging state. 667 * 668 * @param {Event} event The keydown event 669 */ 670 draggingKeyDown(event) { 671 switch (event.key) { 672 case "ArrowLeft": 673 this.handleArrowLeftKeyDown(event); 674 break; 675 case "ArrowUp": 676 this.handleArrowUpKeyDown(event); 677 break; 678 case "ArrowRight": 679 this.handleArrowRightKeyDown(event); 680 break; 681 case "ArrowDown": 682 this.handleArrowDownKeyDown(event); 683 break; 684 case "Enter": 685 case " ": 686 this.#setState(STATES.SELECTED); 687 return; 688 default: 689 return; 690 } 691 692 this.drawSelectionContainer(); 693 } 694 695 /** 696 * Handles a keydown event for the resizing state. 697 * 698 * @param {Event} event The keydown event 699 */ 700 resizingKeyDown(event) { 701 switch (event.key) { 702 case "ArrowLeft": 703 this.resizingArrowLeftKeyDown(event); 704 break; 705 case "ArrowUp": 706 this.resizingArrowUpKeyDown(event); 707 break; 708 case "ArrowRight": 709 this.resizingArrowRightKeyDown(event); 710 break; 711 case "ArrowDown": 712 this.resizingArrowDownKeyDown(event); 713 break; 714 } 715 } 716 717 selectedKeyDown(event) { 718 let isSelectionElement = event.originalTarget.closest( 719 "#selection-container" 720 ); 721 switch (event.key) { 722 case "ArrowLeft": 723 if (isSelectionElement) { 724 this.resizingArrowLeftKeyDown(event); 725 } 726 break; 727 case "ArrowUp": 728 if (isSelectionElement) { 729 this.resizingArrowUpKeyDown(event); 730 } 731 break; 732 case "ArrowRight": 733 if (isSelectionElement) { 734 this.resizingArrowRightKeyDown(event); 735 } 736 break; 737 case "ArrowDown": 738 if (isSelectionElement) { 739 this.resizingArrowDownKeyDown(event); 740 } 741 break; 742 case "Tab": 743 this.maybeLockFocus(event); 744 break; 745 case "Enter": 746 case " ": 747 this.handleKeyDownOnButton(event); 748 break; 749 case this.copyKey.toLowerCase(): 750 if (this.state === "selected" && this.getAccelKey(event)) { 751 this.copySelectedRegion(); 752 } 753 break; 754 case this.downloadKey.toLowerCase(): 755 if (this.state === "selected" && this.getAccelKey(event)) { 756 this.downloadSelectedRegion(); 757 } 758 break; 759 } 760 } 761 762 /** 763 * Move the region or its left or right side to the left. 764 * Just the arrow key will move the region by 1px. 765 * Arrow key + shift will move the region by 10px. 766 * Arrow key + control/meta will move to the edge of the window. 767 * 768 * @param {Event} event The keydown event 769 */ 770 resizingArrowLeftKeyDown(event) { 771 this.handleArrowLeftKeyDown(event); 772 773 if (this.#state !== STATES.RESIZING) { 774 this.#setState(STATES.RESIZING); 775 } 776 777 this.drawSelectionContainer(); 778 } 779 780 /** 781 * Move the region or its left or right side to the left. 782 * Just the arrow key will move the region by 1px. 783 * Arrow key + shift will move the region by 10px. 784 * Arrow key + control/meta will move to the edge of the window. 785 * 786 * @param {Event} event The keydown event 787 */ 788 handleArrowLeftKeyDown(event) { 789 let exponent = event.shiftKey ? 1 : 0; 790 switch (event.originalTarget.id) { 791 case "highlight": 792 if (this.getAccelKey(event)) { 793 let width = this.selectionRegion.width; 794 this.selectionRegion.left = this.windowDimensions.scrollX; 795 this.selectionRegion.right = this.windowDimensions.scrollX + width; 796 break; 797 } 798 799 this.selectionRegion.right -= 10 ** exponent; 800 // eslint-disable-next-line no-fallthrough 801 case "mover-topLeft": 802 case "mover-bottomLeft": 803 if (this.getAccelKey(event)) { 804 this.selectionRegion.left = this.windowDimensions.scrollX; 805 break; 806 } 807 808 this.selectionRegion.left -= 10 ** exponent; 809 this.scrollIfByEdge( 810 this.selectionRegion.left, 811 this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2 812 ); 813 break; 814 case "mover-topRight": 815 case "mover-bottomRight": 816 if (this.getAccelKey(event)) { 817 let left = this.selectionRegion.left; 818 this.selectionRegion.left = this.windowDimensions.scrollX; 819 this.selectionRegion.right = left; 820 if (event.originalTarget.id === "mover-topRight") { 821 this.topLeftMover.focus({ focusVisible: true }); 822 } else if (event.originalTarget.id === "mover-bottomRight") { 823 this.bottomLeftMover.focus({ focusVisible: true }); 824 } 825 break; 826 } 827 828 this.selectionRegion.right -= 10 ** exponent; 829 if (this.selectionRegion.x1 >= this.selectionRegion.x2) { 830 this.selectionRegion.sortCoords(); 831 if (event.originalTarget.id === "mover-topRight") { 832 this.topLeftMover.focus({ focusVisible: true }); 833 } else if (event.originalTarget.id === "mover-bottomRight") { 834 this.bottomLeftMover.focus({ focusVisible: true }); 835 } 836 } 837 break; 838 } 839 } 840 841 /** 842 * Move the region or its top or bottom side upward. 843 * Just the arrow key will move the region by 1px. 844 * Arrow key + shift will move the region by 10px. 845 * Arrow key + control/meta will move to the edge of the window. 846 * 847 * @param {Event} event The keydown event 848 */ 849 resizingArrowUpKeyDown(event) { 850 this.handleArrowUpKeyDown(event); 851 852 if (this.#state !== STATES.RESIZING) { 853 this.#setState(STATES.RESIZING); 854 } 855 856 this.drawSelectionContainer(); 857 } 858 859 /** 860 * Move the region or its top or bottom side upward. 861 * Just the arrow key will move the region by 1px. 862 * Arrow key + shift will move the region by 10px. 863 * Arrow key + control/meta will move to the edge of the window. 864 * 865 * @param {Event} event The keydown event 866 */ 867 handleArrowUpKeyDown(event) { 868 let exponent = event.shiftKey ? 1 : 0; 869 switch (event.originalTarget.id) { 870 case "highlight": 871 if (this.getAccelKey(event)) { 872 let height = this.selectionRegion.height; 873 this.selectionRegion.top = this.windowDimensions.scrollY; 874 this.selectionRegion.bottom = this.windowDimensions.scrollY + height; 875 break; 876 } 877 878 this.selectionRegion.bottom -= 10 ** exponent; 879 // eslint-disable-next-line no-fallthrough 880 case "mover-topLeft": 881 case "mover-topRight": 882 if (this.getAccelKey(event)) { 883 this.selectionRegion.top = this.windowDimensions.scrollY; 884 break; 885 } 886 887 this.selectionRegion.top -= 10 ** exponent; 888 this.scrollIfByEdge( 889 this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2, 890 this.selectionRegion.top 891 ); 892 break; 893 case "mover-bottomLeft": 894 case "mover-bottomRight": 895 if (this.getAccelKey(event)) { 896 let top = this.selectionRegion.top; 897 this.selectionRegion.top = this.windowDimensions.scrollY; 898 this.selectionRegion.bottom = top; 899 if (event.originalTarget.id === "mover-bottomLeft") { 900 this.topLeftMover.focus({ focusVisible: true }); 901 } else if (event.originalTarget.id === "mover-bottomRight") { 902 this.topRightMover.focus({ focusVisible: true }); 903 } 904 break; 905 } 906 907 this.selectionRegion.bottom -= 10 ** exponent; 908 if (this.selectionRegion.y1 >= this.selectionRegion.y2) { 909 this.selectionRegion.sortCoords(); 910 if (event.originalTarget.id === "mover-bottomLeft") { 911 this.topLeftMover.focus({ focusVisible: true }); 912 } else if (event.originalTarget.id === "mover-bottomRight") { 913 this.topRightMover.focus({ focusVisible: true }); 914 } 915 } 916 break; 917 } 918 } 919 920 /** 921 * Move the region or its left or right side to the right. 922 * Just the arrow key will move the region by 1px. 923 * Arrow key + shift will move the region by 10px. 924 * Arrow key + control/meta will move to the edge of the window. 925 * 926 * @param {Event} event The keydown event 927 */ 928 resizingArrowRightKeyDown(event) { 929 this.handleArrowRightKeyDown(event); 930 931 if (this.#state !== STATES.RESIZING) { 932 this.#setState(STATES.RESIZING); 933 } 934 935 this.drawSelectionContainer(); 936 } 937 938 /** 939 * Move the region or its left or right side to the right. 940 * Just the arrow key will move the region by 1px. 941 * Arrow key + shift will move the region by 10px. 942 * Arrow key + control/meta will move to the edge of the window. 943 * 944 * @param {Event} event The keydown event 945 */ 946 handleArrowRightKeyDown(event) { 947 let exponent = event.shiftKey ? 1 : 0; 948 switch (event.originalTarget.id) { 949 case "highlight": 950 if (this.getAccelKey(event)) { 951 let width = this.selectionRegion.width; 952 let { scrollX, clientWidth } = this.windowDimensions.dimensions; 953 this.selectionRegion.right = scrollX + clientWidth; 954 this.selectionRegion.left = this.selectionRegion.right - width; 955 break; 956 } 957 958 this.selectionRegion.left += 10 ** exponent; 959 // eslint-disable-next-line no-fallthrough 960 case "mover-topRight": 961 case "mover-bottomRight": 962 if (this.getAccelKey(event)) { 963 this.selectionRegion.right = 964 this.windowDimensions.scrollX + this.windowDimensions.clientWidth; 965 break; 966 } 967 968 this.selectionRegion.right += 10 ** exponent; 969 this.scrollIfByEdge( 970 this.selectionRegion.right, 971 this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2 972 ); 973 break; 974 case "mover-topLeft": 975 case "mover-bottomLeft": 976 if (this.getAccelKey(event)) { 977 let right = this.selectionRegion.right; 978 this.selectionRegion.right = 979 this.windowDimensions.scrollX + this.windowDimensions.clientWidth; 980 this.selectionRegion.left = right; 981 if (event.originalTarget.id === "mover-topLeft") { 982 this.topRightMover.focus({ focusVisible: true }); 983 } else if (event.originalTarget.id === "mover-bottomLeft") { 984 this.bottomRightMover.focus({ focusVisible: true }); 985 } 986 break; 987 } 988 989 this.selectionRegion.left += 10 ** exponent; 990 if (this.selectionRegion.x1 >= this.selectionRegion.x2) { 991 this.selectionRegion.sortCoords(); 992 if (event.originalTarget.id === "mover-topLeft") { 993 this.topRightMover.focus({ focusVisible: true }); 994 } else if (event.originalTarget.id === "mover-bottomLeft") { 995 this.bottomRightMover.focus({ focusVisible: true }); 996 } 997 } 998 break; 999 } 1000 } 1001 1002 /** 1003 * Move the region or its top or bottom side downward. 1004 * Just the arrow key will move the region by 1px. 1005 * Arrow key + shift will move the region by 10px. 1006 * Arrow key + control/meta will move to the edge of the window. 1007 * 1008 * @param {Event} event The keydown event 1009 */ 1010 resizingArrowDownKeyDown(event) { 1011 this.handleArrowDownKeyDown(event); 1012 1013 if (this.#state !== STATES.RESIZING) { 1014 this.#setState(STATES.RESIZING); 1015 } 1016 1017 this.drawSelectionContainer(); 1018 } 1019 1020 handleArrowDownKeyDown(event) { 1021 let exponent = event.shiftKey ? 1 : 0; 1022 switch (event.originalTarget.id) { 1023 case "highlight": 1024 if (this.getAccelKey(event)) { 1025 let height = this.selectionRegion.height; 1026 let { scrollY, clientHeight } = this.windowDimensions.dimensions; 1027 this.selectionRegion.bottom = scrollY + clientHeight; 1028 this.selectionRegion.top = this.selectionRegion.bottom - height; 1029 break; 1030 } 1031 1032 this.selectionRegion.top += 10 ** exponent; 1033 // eslint-disable-next-line no-fallthrough 1034 case "mover-bottomLeft": 1035 case "mover-bottomRight": 1036 if (this.getAccelKey(event)) { 1037 this.selectionRegion.bottom = 1038 this.windowDimensions.scrollY + this.windowDimensions.clientHeight; 1039 break; 1040 } 1041 1042 this.selectionRegion.bottom += 10 ** exponent; 1043 this.scrollIfByEdge( 1044 this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2, 1045 this.selectionRegion.bottom 1046 ); 1047 break; 1048 case "mover-topLeft": 1049 case "mover-topRight": 1050 if (this.getAccelKey(event)) { 1051 let bottom = this.selectionRegion.bottom; 1052 this.selectionRegion.bottom = 1053 this.windowDimensions.scrollY + this.windowDimensions.clientHeight; 1054 this.selectionRegion.top = bottom; 1055 if (event.originalTarget.id === "mover-topLeft") { 1056 this.bottomLeftMover.focus({ focusVisible: true }); 1057 } else if (event.originalTarget.id === "mover-topRight") { 1058 this.bottomRightMover.focus({ focusVisible: true }); 1059 } 1060 break; 1061 } 1062 1063 this.selectionRegion.top += 10 ** exponent; 1064 if (this.selectionRegion.y1 >= this.selectionRegion.y2) { 1065 this.selectionRegion.sortCoords(); 1066 if (event.originalTarget.id === "mover-topLeft") { 1067 this.bottomLeftMover.focus({ focusVisible: true }); 1068 } else if (event.originalTarget.id === "mover-topRight") { 1069 this.bottomRightMover.focus({ focusVisible: true }); 1070 } 1071 } 1072 break; 1073 } 1074 } 1075 1076 /** 1077 * We lock focus to the overlay when a region is selected. 1078 * Can still escape with shift + F6. 1079 * 1080 * @param {Event} event The keydown event 1081 */ 1082 maybeLockFocus(event) { 1083 switch (this.#state) { 1084 case STATES.CROSSHAIRS: 1085 if (event.originalTarget === this.previewCancelButton) { 1086 if (event.shiftKey) { 1087 this.previewFace.focus({ focusVisible: true }); 1088 } else { 1089 this.#dispatchEvent("Screenshots:FocusPanel", { 1090 direction: "forward", 1091 }); 1092 } 1093 } else if (event.originalTarget === this.previewFace) { 1094 if (event.shiftKey) { 1095 this.#dispatchEvent("Screenshots:FocusPanel", { 1096 direction: "backward", 1097 }); 1098 } else { 1099 this.previewCancelButton.focus({ focusVisible: true }); 1100 } 1101 } 1102 break; 1103 case STATES.SELECTED: 1104 if (event.originalTarget.id === "highlight" && event.shiftKey) { 1105 this.downloadButton.focus({ focusVisible: true }); 1106 } else if (event.originalTarget.id === "download" && !event.shiftKey) { 1107 this.highlightEl.focus({ focusVisible: true }); 1108 } else { 1109 // The content document can listen for keydown events and prevent moving 1110 // focus so we manually move focus to the next element here. 1111 let direction = event.shiftKey 1112 ? Services.focus.MOVEFOCUS_BACKWARD 1113 : Services.focus.MOVEFOCUS_FORWARD; 1114 Services.focus.moveFocus( 1115 this.window, 1116 null, 1117 direction, 1118 Services.focus.FLAG_BYKEY 1119 ); 1120 } 1121 break; 1122 } 1123 } 1124 1125 /** 1126 * Set the focus to the most recent saved method. 1127 * This will default to the download button. 1128 */ 1129 setFocusToActionButton() { 1130 if (lazy.SCREENSHOTS_LAST_SAVED_METHOD === "copy") { 1131 this.copyButton.focus({ focusVisible: true, preventScroll: true }); 1132 } else { 1133 this.downloadButton.focus({ focusVisible: true, preventScroll: true }); 1134 } 1135 } 1136 1137 /** 1138 * We prevent all events in ScreenshotsComponentChild so we need to 1139 * explicitly handle keydown events on buttons here. 1140 * 1141 * @param {KeyEvent} event The keydown event 1142 * 1143 * @returns {boolean} True if the event was handled here, otherwise false. 1144 */ 1145 handleKeyDownOnButton(event) { 1146 switch (event.originalTarget) { 1147 case this.cancelButton: 1148 case this.previewCancelButton: 1149 this.maybeCancelScreenshots(); 1150 break; 1151 case this.copyButton: 1152 this.copySelectedRegion(); 1153 break; 1154 case this.downloadButton: 1155 this.downloadSelectedRegion(); 1156 break; 1157 default: 1158 return false; 1159 } 1160 return true; 1161 } 1162 1163 /** 1164 * All of the selection ranges were recorded at initialization. The ranges 1165 * are removed when focus is set to the buttons so we add the selection 1166 * ranges back so a selected region can be captured. 1167 */ 1168 handleSelectionChange() { 1169 if (this.ranges.length) { 1170 for (let range of this.ranges) { 1171 this.selection.addRange(range); 1172 } 1173 } 1174 } 1175 1176 /** 1177 * Dispatch a custom event to the ScreenshotsComponentChild actor 1178 * 1179 * @param {string} eventType The name of the event 1180 * @param {object} detail Extra details to send to the child actor 1181 */ 1182 #dispatchEvent(eventType, detail) { 1183 this.window.windowUtils.dispatchEventToChromeOnly( 1184 this.window, 1185 new CustomEvent(eventType, { 1186 bubbles: true, 1187 detail, 1188 }) 1189 ); 1190 } 1191 1192 /** 1193 * Set a new state for the overlay 1194 * 1195 * @param {string} newState 1196 * @param {object} options (optional) Options for calling start of state method 1197 */ 1198 #setState(newState, options = {}) { 1199 if (this.#state === STATES.SELECTED && newState === STATES.CROSSHAIRS) { 1200 this.#dispatchEvent("Screenshots:RecordEvent", { 1201 eventName: "startedOverlayRetry", 1202 }); 1203 } 1204 if (newState !== this.#state) { 1205 this.#dispatchEvent("Screenshots:OverlaySelection", { 1206 hasSelection: [ 1207 STATES.DRAGGING_READY, 1208 STATES.DRAGGING, 1209 STATES.RESIZING, 1210 STATES.SELECTED, 1211 ].includes(newState), 1212 overlayState: newState, 1213 }); 1214 } 1215 this.#state = newState; 1216 1217 switch (this.#state) { 1218 case STATES.CROSSHAIRS: { 1219 this.crosshairsStart(); 1220 break; 1221 } 1222 case STATES.DRAGGING_READY: { 1223 this.draggingReadyStart(); 1224 break; 1225 } 1226 case STATES.DRAGGING: { 1227 this.draggingStart(); 1228 break; 1229 } 1230 case STATES.SELECTED: { 1231 this.selectedStart(options); 1232 break; 1233 } 1234 case STATES.RESIZING: { 1235 this.resizingStart(); 1236 break; 1237 } 1238 } 1239 } 1240 1241 copySelectedRegion() { 1242 this.#dispatchEvent("Screenshots:Copy", { 1243 region: this.selectionRegion.dimensions, 1244 }); 1245 } 1246 1247 downloadSelectedRegion() { 1248 this.#dispatchEvent("Screenshots:Download", { 1249 region: this.selectionRegion.dimensions, 1250 }); 1251 } 1252 1253 /** 1254 * Hide hover element, selection and buttons containers. 1255 * Show the preview container and the panel. 1256 * This is the initial state of the overlay. 1257 */ 1258 crosshairsStart() { 1259 this.hideHoverElementContainer(); 1260 this.hideSelectionContainer(); 1261 this.hideButtonsContainer(); 1262 this.showPreviewContainer(); 1263 this.#dispatchEvent("Screenshots:ShowPanel"); 1264 this.#previousDimensions = null; 1265 this.#cachedEle = null; 1266 this.hoverElementRegion.resetDimensions(); 1267 } 1268 1269 /** 1270 * Hide the panel because we have started dragging. 1271 */ 1272 draggingReadyStart() { 1273 this.#dispatchEvent("Screenshots:HidePanel"); 1274 } 1275 1276 /** 1277 * Hide the preview, hover element and buttons containers. 1278 * Show the selection container. 1279 */ 1280 draggingStart() { 1281 this.hidePreviewContainer(); 1282 this.hideButtonsContainer(); 1283 this.hideHoverElementContainer(); 1284 this.drawSelectionContainer(); 1285 } 1286 1287 /** 1288 * Hide the preview and hover element containers. 1289 * Draw the selection and buttons containers. 1290 * 1291 * @param {object} [options={}] (optional) 1292 * @param {boolean} doNotMoveFocus True if focus should not be moved to an action button 1293 */ 1294 selectedStart(options = {}) { 1295 this.selectionRegion.sortCoords(); 1296 this.hidePreviewContainer(); 1297 this.hideHoverElementContainer(); 1298 this.drawSelectionContainer(); 1299 this.drawButtonsContainer(); 1300 1301 if (!options.doNotMoveFocus) { 1302 this.setFocusToActionButton(); 1303 } 1304 } 1305 1306 /** 1307 * Hide the buttons container. 1308 * Store the width and height of the current selected region. 1309 * The dimensions will be used when moving the region along the edge of the 1310 * page and for recording telemetry. 1311 */ 1312 resizingStart() { 1313 this.hideButtonsContainer(); 1314 let { width, height } = this.selectionRegion.dimensions; 1315 this.#previousDimensions = { width, height }; 1316 } 1317 1318 /** 1319 * Dragging has started so we set the initial selection region and set the 1320 * state to draggingReady. 1321 * 1322 * @param {number} pageX The x position relative to the page 1323 * @param {number} pageY The y position relative to the page 1324 */ 1325 crosshairsDragStart(pageX, pageY) { 1326 this.selectionRegion.dimensions = { 1327 left: pageX, 1328 top: pageY, 1329 right: pageX, 1330 bottom: pageY, 1331 }; 1332 1333 this.#setState(STATES.DRAGGING_READY); 1334 } 1335 1336 /** 1337 * If the background is clicked we set the state to crosshairs 1338 * otherwise set the state to resizing 1339 * 1340 * @param {number} pageX The x position relative to the page 1341 * @param {number} pageY The y position relative to the page 1342 * @param {string} targetId The id of the event target 1343 */ 1344 selectedDragStart(pageX, pageY, targetId) { 1345 if (targetId === this.screenshotsContainer.id) { 1346 this.#setState(STATES.CROSSHAIRS); 1347 return; 1348 } 1349 this.#moverId = targetId; 1350 this.#lastPageX = pageX; 1351 this.#lastPageY = pageY; 1352 1353 this.#setState(STATES.RESIZING); 1354 } 1355 1356 /** 1357 * Draw the eyes in the preview container and find the element currently 1358 * being hovered. 1359 * 1360 * @param {number} clientX The x position relative to the viewport 1361 * @param {number} clientY The y position relative to the viewport 1362 */ 1363 crosshairsMove(clientX, clientY) { 1364 this.drawPreviewEyes(clientX, clientY); 1365 1366 this.handleElementHover(clientX, clientY); 1367 } 1368 1369 /** 1370 * Set the selection region dimensions and if the region is at least 40 1371 * pixels diagnally in distance, set the state to dragging. 1372 * 1373 * @param {number} pageX The x position relative to the page 1374 * @param {number} pageY The y position relative to the page 1375 */ 1376 draggingReadyDrag(pageX, pageY) { 1377 this.selectionRegion.dimensions = { 1378 right: pageX, 1379 bottom: pageY, 1380 }; 1381 1382 if (this.selectionRegion.distance > 40) { 1383 this.#setState(STATES.DRAGGING); 1384 } 1385 } 1386 1387 /** 1388 * Scroll if along the edge of the viewport, update the selection region 1389 * dimensions and draw the selection container. 1390 * 1391 * @param {number} pageX The x position relative to the page 1392 * @param {number} pageY The y position relative to the page 1393 */ 1394 draggingDrag(pageX, pageY) { 1395 this.scrollIfByEdge(pageX, pageY); 1396 this.selectionRegion.dimensions = { 1397 right: pageX, 1398 bottom: pageY, 1399 }; 1400 1401 this.drawSelectionContainer(); 1402 } 1403 1404 /** 1405 * Resize the selection region depending on the mover that started the resize. 1406 * 1407 * @param {number} pageX The x position relative to the page 1408 * @param {number} pageY The y position relative to the page 1409 */ 1410 resizingDrag(pageX, pageY) { 1411 this.scrollIfByEdge(pageX, pageY); 1412 switch (this.#moverId) { 1413 case "mover-topLeft": { 1414 this.selectionRegion.dimensions = { 1415 left: pageX, 1416 top: pageY, 1417 }; 1418 break; 1419 } 1420 case "mover-top": { 1421 this.selectionRegion.dimensions = { top: pageY }; 1422 break; 1423 } 1424 case "mover-topRight": { 1425 this.selectionRegion.dimensions = { 1426 top: pageY, 1427 right: pageX, 1428 }; 1429 break; 1430 } 1431 case "mover-right": { 1432 this.selectionRegion.dimensions = { 1433 right: pageX, 1434 }; 1435 break; 1436 } 1437 case "mover-bottomRight": { 1438 this.selectionRegion.dimensions = { 1439 right: pageX, 1440 bottom: pageY, 1441 }; 1442 break; 1443 } 1444 case "mover-bottom": { 1445 this.selectionRegion.dimensions = { 1446 bottom: pageY, 1447 }; 1448 break; 1449 } 1450 case "mover-bottomLeft": { 1451 this.selectionRegion.dimensions = { 1452 left: pageX, 1453 bottom: pageY, 1454 }; 1455 break; 1456 } 1457 case "mover-left": { 1458 this.selectionRegion.dimensions = { left: pageX }; 1459 break; 1460 } 1461 case "highlight": { 1462 let diffX = this.#lastPageX - pageX; 1463 let diffY = this.#lastPageY - pageY; 1464 1465 let newLeft; 1466 let newRight; 1467 let newTop; 1468 let newBottom; 1469 1470 // Unpack dimensions to use here 1471 let { 1472 left: boxLeft, 1473 top: boxTop, 1474 right: boxRight, 1475 bottom: boxBottom, 1476 width: boxWidth, 1477 height: boxHeight, 1478 } = this.selectionRegion.dimensions; 1479 let { scrollWidth, scrollHeight } = this.windowDimensions.dimensions; 1480 1481 // wait until all 4 if elses have completed before setting box dimensions 1482 if (boxWidth <= this.#previousDimensions.width && boxLeft === 0) { 1483 newLeft = boxRight - this.#previousDimensions.width; 1484 } else { 1485 newLeft = boxLeft; 1486 } 1487 1488 if ( 1489 boxWidth <= this.#previousDimensions.width && 1490 boxRight === scrollWidth 1491 ) { 1492 newRight = boxLeft + this.#previousDimensions.width; 1493 } else { 1494 newRight = boxRight; 1495 } 1496 1497 if (boxHeight <= this.#previousDimensions.height && boxTop === 0) { 1498 newTop = boxBottom - this.#previousDimensions.height; 1499 } else { 1500 newTop = boxTop; 1501 } 1502 1503 if ( 1504 boxHeight <= this.#previousDimensions.height && 1505 boxBottom === scrollHeight 1506 ) { 1507 newBottom = boxTop + this.#previousDimensions.height; 1508 } else { 1509 newBottom = boxBottom; 1510 } 1511 1512 this.selectionRegion.dimensions = { 1513 left: newLeft - diffX, 1514 top: newTop - diffY, 1515 right: newRight - diffX, 1516 bottom: newBottom - diffY, 1517 }; 1518 1519 this.#lastPageX = pageX; 1520 this.#lastPageY = pageY; 1521 break; 1522 } 1523 } 1524 this.drawSelectionContainer(); 1525 } 1526 1527 /** 1528 * If there is a valid element region, update and draw the selection 1529 * container and set the state to selected. 1530 * Otherwise set the state to crosshairs. 1531 * 1532 * @param {object} options (optional) Options for passing to setState method 1533 */ 1534 draggingReadyDragEnd(options = {}) { 1535 if (this.hoverElementRegion.isRegionValid) { 1536 this.selectionRegion.dimensions = this.hoverElementRegion.dimensions; 1537 this.#setState(STATES.SELECTED, options); 1538 this.#dispatchEvent("Screenshots:RecordEvent", { 1539 eventName: "selectedElement", 1540 }); 1541 this.#methodsUsed.element += 1; 1542 } else { 1543 this.#setState(STATES.CROSSHAIRS); 1544 } 1545 } 1546 1547 /** 1548 * Update the selection region dimensions and set the state to selected. 1549 * 1550 * @param {number} pageX The x position relative to the page 1551 * @param {number} pageY The y position relative to the page 1552 */ 1553 draggingDragEnd(pageX, pageY) { 1554 this.selectionRegion.dimensions = { 1555 right: pageX, 1556 bottom: pageY, 1557 }; 1558 this.#setState(STATES.SELECTED); 1559 this.maybeRecordRegionSelected(); 1560 this.#methodsUsed.region += 1; 1561 } 1562 1563 /** 1564 * Update the selection region dimensions by calling `resizingDrag` and set 1565 * the state to selected. 1566 * 1567 * @param {number} pageX The x position relative to the page 1568 * @param {number} pageY The y position relative to the page 1569 */ 1570 resizingDragEnd(pageX, pageY) { 1571 this.resizingDrag(pageX, pageY); 1572 this.#setState(STATES.SELECTED); 1573 this.maybeRecordRegionSelected(); 1574 if (this.#moverId === "highlight") { 1575 this.#methodsUsed.move += 1; 1576 } else { 1577 this.#methodsUsed.resize += 1; 1578 } 1579 } 1580 1581 maybeRecordRegionSelected() { 1582 let { width, height } = this.selectionRegion.dimensions; 1583 1584 if ( 1585 !this.#previousDimensions || 1586 (Math.abs(this.#previousDimensions.width - width) > 1587 REGION_CHANGE_THRESHOLD && 1588 Math.abs(this.#previousDimensions.height - height) > 1589 REGION_CHANGE_THRESHOLD) 1590 ) { 1591 this.#dispatchEvent("Screenshots:RecordEvent", { 1592 eventName: "selectedRegionSelection", 1593 }); 1594 } 1595 this.#previousDimensions = { width, height }; 1596 } 1597 1598 /** 1599 * Draw the preview eyes pointer towards the mouse. 1600 * 1601 * @param {number} clientX The x position relative to the viewport 1602 * @param {number} clientY The y position relative to the viewport 1603 */ 1604 drawPreviewEyes(clientX, clientY) { 1605 let { clientWidth, clientHeight } = this.windowDimensions.dimensions; 1606 const xpos = Math.floor((10 * (clientX - clientWidth / 2)) / clientWidth); 1607 const ypos = Math.floor((10 * (clientY - clientHeight / 2)) / clientHeight); 1608 const move = `transform:translate(${xpos}px, ${ypos}px);`; 1609 this.leftEye.style = move; 1610 this.rightEye.style = move; 1611 } 1612 1613 showPreviewContainer() { 1614 this.previewContainer.hidden = false; 1615 } 1616 1617 hidePreviewContainer() { 1618 this.previewContainer.hidden = true; 1619 } 1620 1621 updatePreviewContainer() { 1622 let { clientWidth, clientHeight } = this.windowDimensions.dimensions; 1623 this.previewContainer.style.width = `${clientWidth}px`; 1624 this.previewContainer.style.height = `${clientHeight}px`; 1625 } 1626 1627 /** 1628 * Update the screenshots overlay container based on the window dimensions. 1629 */ 1630 updateScreenshotsOverlayContainer() { 1631 let { scrollWidth, scrollHeight, scrollMinX } = 1632 this.windowDimensions.dimensions; 1633 this.screenshotsContainer.style = `left:${scrollMinX};width:${scrollWidth}px;height:${scrollHeight}px;`; 1634 } 1635 1636 showScreenshotsOverlayContainer() { 1637 this.screenshotsContainer.hidden = false; 1638 } 1639 1640 hideScreenshotsOverlayContainer() { 1641 this.screenshotsContainer.hidden = true; 1642 } 1643 1644 /** 1645 * Draw the hover element container based on the hover element region. 1646 */ 1647 drawHoverElementRegion() { 1648 this.showHoverElementContainer(); 1649 1650 let { top, left, width, height } = this.hoverElementRegion.dimensions; 1651 1652 this.hoverElementContainer.style = `top:${top}px;left:${left}px;width:${width}px;height:${height}px;`; 1653 } 1654 1655 showHoverElementContainer() { 1656 this.hoverElementContainer.hidden = false; 1657 } 1658 1659 hideHoverElementContainer() { 1660 this.hoverElementContainer.hidden = true; 1661 } 1662 1663 /** 1664 * Draw each background element and the highlight element base on the 1665 * selection region. 1666 */ 1667 drawSelectionContainer() { 1668 this.showSelectionContainer(); 1669 1670 let { top, left, right, bottom, width, height } = 1671 this.selectionRegion.dimensions; 1672 1673 this.highlightEl.style = `top:${top}px;left:${left}px;width:${width}px;height:${height}px;`; 1674 1675 this.leftBackgroundEl.style = `top:${top}px;width:${left}px;height:${height}px;`; 1676 this.topBackgroundEl.style.height = `${top}px`; 1677 this.rightBackgroundEl.style = `top:${top}px;left:${right}px;width:calc(100% - ${right}px);height:${height}px;`; 1678 this.bottomBackgroundEl.style = `top:${bottom}px;height:calc(100% - ${bottom}px);`; 1679 1680 this.updateSelectionSizeText(); 1681 } 1682 1683 /** 1684 * Update the size of the selected region. Use the zoom to correctly display 1685 * the region dimensions. 1686 */ 1687 updateSelectionSizeText() { 1688 let { width, height } = this.selectionRegion.dimensions; 1689 let zoom = Math.round(this.window.browsingContext.fullZoom * 100) / 100; 1690 1691 let [selectionSizeTranslation] = 1692 lazy.overlayLocalization.formatMessagesSync([ 1693 { 1694 id: "screenshots-overlay-selection-region-size-3", 1695 args: { 1696 width: Math.floor(width * zoom), 1697 height: Math.floor(height * zoom), 1698 }, 1699 }, 1700 ]); 1701 this.selectionSize.textContent = selectionSizeTranslation.value; 1702 } 1703 1704 showSelectionContainer() { 1705 this.selectionContainer.hidden = false; 1706 } 1707 1708 hideSelectionContainer() { 1709 this.selectionContainer.hidden = true; 1710 } 1711 1712 /** 1713 * Draw the buttons container in the bottom right corner of the selection 1714 * container if possible. 1715 * The buttons will be visible in the viewport if the selection container 1716 * is within the viewport, otherwise skip drawing the buttons. 1717 */ 1718 drawButtonsContainer() { 1719 this.showButtonsContainer(); 1720 1721 let { 1722 left: boxLeft, 1723 top: boxTop, 1724 right: boxRight, 1725 bottom: boxBottom, 1726 } = this.selectionRegion.dimensions; 1727 1728 let { clientWidth, clientHeight, scrollX, scrollY, scrollWidth } = 1729 this.windowDimensions.dimensions; 1730 1731 if (!this.windowDimensions.isInViewport(this.selectionRegion.dimensions)) { 1732 // The box is entirely offscreen so need to draw the buttons 1733 1734 return; 1735 } 1736 1737 let top = boxBottom; 1738 1739 if (scrollY + clientHeight - boxBottom < 70) { 1740 if (boxBottom < scrollY + clientHeight) { 1741 top = boxBottom - 60; 1742 } else if (scrollY + clientHeight - boxTop < 70) { 1743 top = boxTop - 60; 1744 } else { 1745 top = scrollY + clientHeight - 60; 1746 } 1747 } 1748 1749 if (!this.buttonsContainerRect) { 1750 this.buttonsContainerRect = this.buttonsContainer.getBoundingClientRect(); 1751 } 1752 1753 let viewportLeft = scrollX; 1754 let viewportRight = scrollX + clientWidth; 1755 1756 let left, right; 1757 let isLTR = !Services.locale.isAppLocaleRTL; 1758 if (isLTR) { 1759 left = Math.max( 1760 Math.min(viewportRight, boxRight), 1761 viewportLeft + Math.ceil(this.buttonsContainerRect.width) 1762 ); 1763 right = scrollWidth - left; 1764 1765 this.buttonsContainer.style.right = `${right}px`; 1766 this.buttonsContainer.style.left = ""; 1767 } else { 1768 left = Math.min( 1769 Math.max(viewportLeft, boxLeft), 1770 viewportRight - Math.ceil(this.buttonsContainerRect.width) 1771 ); 1772 1773 this.buttonsContainer.style.left = `${left}px`; 1774 this.buttonsContainer.style.right = ""; 1775 } 1776 1777 this.buttonsContainer.style.top = `${top}px`; 1778 } 1779 1780 showButtonsContainer() { 1781 this.buttonsContainer.hidden = false; 1782 } 1783 1784 hideButtonsContainer() { 1785 this.buttonsContainer.hidden = true; 1786 } 1787 1788 updateCursorRegion(left, top) { 1789 this.cursorRegion = { left, top, right: left, bottom: top }; 1790 } 1791 1792 /** 1793 * Set the pointer events to none on the screenshots elements so 1794 * elementFromPoint can find the real element at the given point. 1795 */ 1796 setPointerEventsNone() { 1797 this.screenshotsContainer.style.pointerEvents = "none"; 1798 } 1799 1800 resetPointerEvents() { 1801 this.screenshotsContainer.style.pointerEvents = ""; 1802 } 1803 1804 /** 1805 * Try to find a reasonable element for a given point. 1806 * If a reasonable element is found, draw the hover element container for 1807 * that element region. 1808 * 1809 * @param {number} clientX The x position relative to the viewport 1810 * @param {number} clientY The y position relative to the viewport 1811 */ 1812 async handleElementHover(clientX, clientY) { 1813 this.setPointerEventsNone(); 1814 let promise = getElementFromPoint(clientX, clientY, this.document); 1815 this.resetPointerEvents(); 1816 let { ele, rect } = await promise; 1817 1818 if ( 1819 this.#cachedEle && 1820 !this.window.HTMLIFrameElement.isInstance(this.#cachedEle) && 1821 this.#cachedEle === ele 1822 ) { 1823 // Still hovering over the same element 1824 return; 1825 } 1826 this.#cachedEle = ele; 1827 1828 if (!rect) { 1829 // this means we found an element that wasn't an iframe 1830 rect = getBestRectForElement(ele, this.document); 1831 } 1832 1833 if (rect) { 1834 let { scrollX, scrollY } = this.windowDimensions.dimensions; 1835 let { left, top, right, bottom } = rect; 1836 let newRect = { 1837 left: left + scrollX, 1838 top: top + scrollY, 1839 right: right + scrollX, 1840 bottom: bottom + scrollY, 1841 }; 1842 this.hoverElementRegion.dimensions = newRect; 1843 this.drawHoverElementRegion(); 1844 } else { 1845 this.hoverElementRegion.resetDimensions(); 1846 this.hideHoverElementContainer(); 1847 } 1848 } 1849 1850 /** 1851 * Scroll the viewport if near one or both of the edges. 1852 * 1853 * @param {number} pageX The x position relative to the page 1854 * @param {number} pageY The y position relative to the page 1855 */ 1856 scrollIfByEdge(pageX, pageY) { 1857 let { scrollX, scrollY, clientWidth, clientHeight } = 1858 this.windowDimensions.dimensions; 1859 1860 if (pageY - scrollY < SCROLL_BY_EDGE) { 1861 // Scroll up 1862 this.scrollWindow(0, -(SCROLL_BY_EDGE - (pageY - scrollY))); 1863 } else if (scrollY + clientHeight - pageY < SCROLL_BY_EDGE) { 1864 // Scroll down 1865 this.scrollWindow(0, SCROLL_BY_EDGE - (scrollY + clientHeight - pageY)); 1866 } 1867 1868 if (pageX - scrollX <= SCROLL_BY_EDGE) { 1869 // Scroll left 1870 this.scrollWindow(-(SCROLL_BY_EDGE - (pageX - scrollX)), 0); 1871 } else if (scrollX + clientWidth - pageX <= SCROLL_BY_EDGE) { 1872 // Scroll right 1873 this.scrollWindow(SCROLL_BY_EDGE - (scrollX + clientWidth - pageX), 0); 1874 } 1875 } 1876 1877 /** 1878 * Scroll the window by the given amount. 1879 * 1880 * @param {number} x The x amount to scroll 1881 * @param {number} y The y amount to scroll 1882 */ 1883 scrollWindow(x, y) { 1884 this.window.scrollBy(x, y); 1885 this.updateScreenshotsOverlayDimensions("scroll"); 1886 } 1887 1888 /** 1889 * The page was resized or scrolled. We need to update the screenshots 1890 * container size so we don't draw outside the page bounds. 1891 * 1892 * @param {string} eventType will be "scroll" or "resize" 1893 */ 1894 async updateScreenshotsOverlayDimensions(eventType) { 1895 let updateWindowDimensionsPromise = this.updateWindowDimensions(); 1896 1897 if (this.#state === STATES.CROSSHAIRS) { 1898 if (eventType === "resize") { 1899 this.hideHoverElementContainer(); 1900 this.#cachedEle = null; 1901 } else if (eventType === "scroll") { 1902 if (this.#lastClientX && this.#lastClientY) { 1903 this.#cachedEle = null; 1904 this.handleElementHover(this.#lastClientX, this.#lastClientY); 1905 } 1906 } 1907 } else if (this.#state === STATES.SELECTED) { 1908 await updateWindowDimensionsPromise; 1909 this.selectionRegion.shift(); 1910 this.drawSelectionContainer(); 1911 this.drawButtonsContainer(); 1912 this.updateSelectionSizeText(); 1913 } 1914 } 1915 1916 /** 1917 * Returns the window's dimensions for the current window. 1918 * 1919 * @return {object} An object containing window dimensions 1920 * { 1921 * clientWidth: The width of the viewport 1922 * clientHeight: The height of the viewport 1923 * scrollWidth: The width of the enitre page 1924 * scrollHeight: The height of the entire page 1925 * scrollX: The X scroll offset of the viewport 1926 * scrollY: The Y scroll offest of the viewport 1927 * scrollMinX: The X minimum the viewport can scroll to 1928 * scrollMinY: The Y minimum the viewport can scroll to 1929 * scrollMaxX: The X maximum the viewport can scroll to 1930 * scrollMaxY: The Y maximum the viewport can scroll to 1931 * } 1932 */ 1933 getDimensionsFromWindow() { 1934 let { 1935 innerHeight, 1936 innerWidth, 1937 scrollMaxY, 1938 scrollMaxX, 1939 scrollMinY, 1940 scrollMinX, 1941 scrollY, 1942 scrollX, 1943 } = this.window; 1944 1945 let scrollWidth = innerWidth + scrollMaxX - scrollMinX; 1946 let scrollHeight = innerHeight + scrollMaxY - scrollMinY; 1947 let clientHeight = innerHeight; 1948 let clientWidth = innerWidth; 1949 1950 const scrollbarHeight = {}; 1951 const scrollbarWidth = {}; 1952 this.window.windowUtils.getScrollbarSize( 1953 false, 1954 scrollbarWidth, 1955 scrollbarHeight 1956 ); 1957 scrollWidth -= scrollbarWidth.value; 1958 scrollHeight -= scrollbarHeight.value; 1959 clientWidth -= scrollbarWidth.value; 1960 clientHeight -= scrollbarHeight.value; 1961 1962 return { 1963 clientWidth, 1964 clientHeight, 1965 scrollWidth, 1966 scrollHeight, 1967 scrollX, 1968 scrollY, 1969 scrollMinX, 1970 scrollMinY, 1971 scrollMaxX, 1972 scrollMaxY, 1973 }; 1974 } 1975 1976 /** 1977 * We have to be careful not to draw the overlay larger than the document 1978 * because the overlay is absolutely position and within the document so we 1979 * can cause the document to overflow when it shouldn't. To mitigate this, 1980 * we will temporarily position the overlay to position fixed with width and 1981 * height 100% so the overlay is within the document bounds. Then we will get 1982 * the dimensions of the document to correctly draw the overlay. 1983 */ 1984 async updateWindowDimensions() { 1985 // Setting the screenshots container attribute "resizing" will make the 1986 // overlay fixed position with width and height of 100% percent so it 1987 // does not draw outside the actual document. 1988 this.screenshotsContainer.toggleAttribute("resizing", true); 1989 1990 await new Promise(r => this.window.requestAnimationFrame(r)); 1991 1992 let { 1993 clientWidth, 1994 clientHeight, 1995 scrollWidth, 1996 scrollHeight, 1997 scrollX, 1998 scrollY, 1999 scrollMinX, 2000 scrollMinY, 2001 scrollMaxX, 2002 scrollMaxY, 2003 } = this.getDimensionsFromWindow(); 2004 this.screenshotsContainer.toggleAttribute("resizing", false); 2005 2006 this.windowDimensions.dimensions = { 2007 clientWidth, 2008 clientHeight, 2009 scrollWidth, 2010 scrollHeight, 2011 scrollX, 2012 scrollY, 2013 scrollMinX, 2014 scrollMinY, 2015 scrollMaxX, 2016 scrollMaxY, 2017 devicePixelRatio: this.window.devicePixelRatio, 2018 }; 2019 2020 this.updatePreviewContainer(); 2021 this.updateScreenshotsOverlayContainer(); 2022 2023 setMaxDetectHeight(Math.max(clientHeight + 100, 700)); 2024 setMaxDetectWidth(Math.max(clientWidth + 100, 1000)); 2025 } 2026 }