browser-gestureSupport.js (29997B)
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 // Simple gestures support 6 // 7 // As per bug #412486, web content must not be allowed to receive any 8 // simple gesture events. Multi-touch gesture APIs are in their 9 // infancy and we do NOT want to be forced into supporting an API that 10 // will probably have to change in the future. (The current Mac OS X 11 // API is undocumented and was reverse-engineered.) Until support is 12 // implemented in the event dispatcher to keep these events as 13 // chrome-only, we must listen for the simple gesture events during 14 // the capturing phase and call stopPropagation on every event. 15 16 var gGestureSupport = { 17 _currentRotation: 0, 18 _lastRotateDelta: 0, 19 _rotateMomentumThreshold: 0.75, 20 21 /** 22 * Add or remove mouse gesture event listeners 23 * 24 * @param aAddListener 25 * True to add/init listeners and false to remove/uninit 26 */ 27 init: function GS_init(aAddListener) { 28 const gestureEvents = [ 29 "SwipeGestureMayStart", 30 "SwipeGestureStart", 31 "SwipeGestureUpdate", 32 "SwipeGestureEnd", 33 "SwipeGesture", 34 "MagnifyGestureStart", 35 "MagnifyGestureUpdate", 36 "MagnifyGesture", 37 "RotateGestureStart", 38 "RotateGestureUpdate", 39 "RotateGesture", 40 "TapGesture", 41 "PressTapGesture", 42 ]; 43 44 for (let event of gestureEvents) { 45 if (aAddListener) { 46 gBrowser.tabbox.addEventListener("Moz" + event, this, true); 47 } else { 48 gBrowser.tabbox.removeEventListener("Moz" + event, this, true); 49 } 50 } 51 }, 52 53 /** 54 * Dispatch events based on the type of mouse gesture event. For now, make 55 * sure to stop propagation of every gesture event so that web content cannot 56 * receive gesture events. 57 * 58 * @param aEvent 59 * The gesture event to handle 60 */ 61 handleEvent: function GS_handleEvent(aEvent) { 62 if ( 63 !Services.prefs.getBoolPref( 64 "dom.debug.propagate_gesture_events_through_content" 65 ) 66 ) { 67 aEvent.stopPropagation(); 68 } 69 70 // Create a preference object with some defaults 71 let def = (aThreshold, aLatched) => ({ 72 threshold: aThreshold, 73 latched: !!aLatched, 74 }); 75 76 switch (aEvent.type) { 77 case "MozSwipeGestureMayStart": 78 if (this._shouldDoSwipeGesture(aEvent)) { 79 aEvent.preventDefault(); 80 } 81 break; 82 case "MozSwipeGestureStart": 83 aEvent.preventDefault(); 84 this._setupSwipeGesture(); 85 break; 86 case "MozSwipeGestureUpdate": 87 aEvent.preventDefault(); 88 this._doUpdate(aEvent); 89 break; 90 case "MozSwipeGestureEnd": 91 aEvent.preventDefault(); 92 this._doEnd(aEvent); 93 break; 94 case "MozSwipeGesture": 95 aEvent.preventDefault(); 96 this.onSwipe(aEvent); 97 break; 98 case "MozMagnifyGestureStart": 99 aEvent.preventDefault(); 100 this._setupGesture(aEvent, "pinch", def(25, 0), "out", "in"); 101 break; 102 case "MozRotateGestureStart": 103 aEvent.preventDefault(); 104 this._setupGesture(aEvent, "twist", def(25, 0), "right", "left"); 105 break; 106 case "MozMagnifyGestureUpdate": 107 case "MozRotateGestureUpdate": 108 aEvent.preventDefault(); 109 this._doUpdate(aEvent); 110 break; 111 case "MozTapGesture": 112 aEvent.preventDefault(); 113 this._doAction(aEvent, ["tap"]); 114 break; 115 case "MozRotateGesture": 116 aEvent.preventDefault(); 117 this._doAction(aEvent, ["twist", "end"]); 118 break; 119 /* case "MozPressTapGesture": 120 break; */ 121 } 122 }, 123 124 /** 125 * Called at the start of "pinch" and "twist" gestures to setup all of the 126 * information needed to process the gesture 127 * 128 * @param aEvent 129 * The continual motion start event to handle 130 * @param aGesture 131 * Name of the gesture to handle 132 * @param aPref 133 * Preference object with the names of preferences and defaults 134 * @param aInc 135 * Command to trigger for increasing motion (without gesture name) 136 * @param aDec 137 * Command to trigger for decreasing motion (without gesture name) 138 */ 139 _setupGesture: function GS__setupGesture( 140 aEvent, 141 aGesture, 142 aPref, 143 aInc, 144 aDec 145 ) { 146 // Try to load user-set values from preferences 147 for (let [pref, def] of Object.entries(aPref)) { 148 aPref[pref] = this._getPref(aGesture + "." + pref, def); 149 } 150 151 // Keep track of the total deltas and latching behavior 152 let offset = 0; 153 let latchDir = aEvent.delta > 0 ? 1 : -1; 154 let isLatched = false; 155 156 // Create the update function here to capture closure state 157 this._doUpdate = function GS__doUpdate(updateEvent) { 158 // Update the offset with new event data 159 offset += updateEvent.delta; 160 161 // Check if the cumulative deltas exceed the threshold 162 if (Math.abs(offset) > aPref.threshold) { 163 // Trigger the action if we don't care about latching; otherwise, make 164 // sure either we're not latched and going the same direction of the 165 // initial motion; or we're latched and going the opposite way 166 let sameDir = (latchDir ^ offset) >= 0; 167 if (!aPref.latched || isLatched ^ sameDir) { 168 this._doAction(updateEvent, [aGesture, offset > 0 ? aInc : aDec]); 169 170 // We must be getting latched or leaving it, so just toggle 171 isLatched = !isLatched; 172 } 173 174 // Reset motion counter to prepare for more of the same gesture 175 offset = 0; 176 } 177 }; 178 179 // The start event also contains deltas, so handle an update right away 180 this._doUpdate(aEvent); 181 }, 182 183 /** 184 * Checks whether a swipe gesture event can navigate the browser history or 185 * not. 186 * 187 * @param aEvent 188 * The swipe gesture event. 189 * @return true if the swipe event may navigate the history, false othwerwise. 190 */ 191 _swipeNavigatesHistory: function GS__swipeNavigatesHistory(aEvent) { 192 return ( 193 this._getCommand(aEvent, ["swipe", "left"]) == 194 "Browser:BackOrBackDuplicate" && 195 this._getCommand(aEvent, ["swipe", "right"]) == 196 "Browser:ForwardOrForwardDuplicate" 197 ); 198 }, 199 200 /** 201 * Checks whether we want to start a swipe for aEvent and sets 202 * aEvent.allowedDirections to the right values. 203 * 204 * @param aEvent 205 * The swipe gesture "MayStart" event. 206 * @return true if we're willing to start a swipe for this event, false 207 * otherwise. 208 */ 209 _shouldDoSwipeGesture: function GS__shouldDoSwipeGesture(aEvent) { 210 if (!this._swipeNavigatesHistory(aEvent)) { 211 return false; 212 } 213 214 let isVerticalSwipe = false; 215 if (aEvent.direction == aEvent.DIRECTION_UP) { 216 if (gMultiProcessBrowser || window.content.pageYOffset > 0) { 217 return false; 218 } 219 isVerticalSwipe = true; 220 } else if (aEvent.direction == aEvent.DIRECTION_DOWN) { 221 if ( 222 gMultiProcessBrowser || 223 window.content.pageYOffset < window.content.scrollMaxY 224 ) { 225 return false; 226 } 227 isVerticalSwipe = true; 228 } 229 if (isVerticalSwipe) { 230 // Vertical overscroll has been temporarily disabled until bug 939480 is 231 // fixed. 232 return false; 233 } 234 235 let canGoBack = gHistorySwipeAnimation.canGoBack(); 236 let canGoForward = gHistorySwipeAnimation.canGoForward(); 237 let isLTR = gHistorySwipeAnimation.isLTR; 238 239 if (canGoBack) { 240 aEvent.allowedDirections |= isLTR 241 ? aEvent.DIRECTION_LEFT 242 : aEvent.DIRECTION_RIGHT; 243 } 244 if (canGoForward) { 245 aEvent.allowedDirections |= isLTR 246 ? aEvent.DIRECTION_RIGHT 247 : aEvent.DIRECTION_LEFT; 248 } 249 250 return canGoBack || canGoForward; 251 }, 252 253 /** 254 * Sets up swipe gestures. This includes setting up swipe animations for the 255 * gesture, if enabled. 256 * 257 * @param aEvent 258 * The swipe gesture start event. 259 * @return true if swipe gestures could successfully be set up, false 260 * othwerwise. 261 */ 262 _setupSwipeGesture: function GS__setupSwipeGesture() { 263 gHistorySwipeAnimation.startAnimation(); 264 265 this._doUpdate = function GS__doUpdate(aEvent) { 266 gHistorySwipeAnimation.updateAnimation(aEvent.delta); 267 }; 268 269 this._doEnd = function GS__doEnd() { 270 gHistorySwipeAnimation.swipeEndEventReceived(); 271 272 this._doUpdate = function () {}; 273 this._doEnd = function () {}; 274 }; 275 }, 276 277 /** 278 * Generator producing the powerset of the input array where the first result 279 * is the complete set and the last result (before StopIteration) is empty. 280 * 281 * @param aArray 282 * Source array containing any number of elements 283 * @yield Array that is a subset of the input array from full set to empty 284 */ 285 _power: function* GS__power(aArray) { 286 // Create a bitmask based on the length of the array 287 let num = 1 << aArray.length; 288 while (--num >= 0) { 289 // Only select array elements where the current bit is set 290 yield aArray.reduce(function (aPrev, aCurr, aIndex) { 291 if (num & (1 << aIndex)) { 292 aPrev.push(aCurr); 293 } 294 return aPrev; 295 }, []); 296 } 297 }, 298 299 /** 300 * Determine what action to do for the gesture based on which keys are 301 * pressed and which commands are set, and execute the command. 302 * 303 * @param aEvent 304 * The original gesture event to convert into a fake click event 305 * @param aGesture 306 * Array of gesture name parts (to be joined by periods) 307 * @return Name of the executed command. Returns null if no command is 308 * found. 309 */ 310 _doAction: function GS__doAction(aEvent, aGesture) { 311 let command = this._getCommand(aEvent, aGesture); 312 return command && this._doCommand(aEvent, command); 313 }, 314 315 /** 316 * Determine what action to do for the gesture based on which keys are 317 * pressed and which commands are set 318 * 319 * @param aEvent 320 * The original gesture event to convert into a fake click event 321 * @param aGesture 322 * Array of gesture name parts (to be joined by periods) 323 */ 324 _getCommand: function GS__getCommand(aEvent, aGesture) { 325 // Create an array of pressed keys in a fixed order so that a command for 326 // "meta" is preferred over "ctrl" when both buttons are pressed (and a 327 // command for both don't exist) 328 let keyCombos = []; 329 for (let key of ["shift", "alt", "ctrl", "meta"]) { 330 if (aEvent[key + "Key"]) { 331 keyCombos.push(key); 332 } 333 } 334 335 // Try each combination of key presses in decreasing order for commands 336 for (let subCombo of this._power(keyCombos)) { 337 // Convert a gesture and pressed keys into the corresponding command 338 // action where the preference has the gesture before "shift" before 339 // "alt" before "ctrl" before "meta" all separated by periods 340 let command; 341 try { 342 command = this._getPref(aGesture.concat(subCombo).join(".")); 343 } catch (e) {} 344 345 if (command) { 346 return command; 347 } 348 } 349 return null; 350 }, 351 352 /** 353 * Execute the specified command. 354 * 355 * @param aEvent 356 * The original gesture event to convert into a fake click event 357 * @param aCommand 358 * Name of the command found for the event's keys and gesture. 359 */ 360 _doCommand: function GS__doCommand(aEvent, aCommand) { 361 let node = document.getElementById(aCommand); 362 if (node) { 363 if (node.getAttribute("disabled") != "true") { 364 let cmdEvent = document.createEvent("xulcommandevent"); 365 cmdEvent.initCommandEvent( 366 "command", 367 true, 368 true, 369 window, 370 0, 371 aEvent.ctrlKey, 372 aEvent.altKey, 373 aEvent.shiftKey, 374 aEvent.metaKey, 375 0, 376 aEvent, 377 aEvent.inputSource 378 ); 379 node.dispatchEvent(cmdEvent); 380 } 381 } else { 382 goDoCommand(aCommand); 383 } 384 }, 385 386 /** 387 * Handle continual motion events. This function will be set by 388 * _setupGesture or _setupSwipe. 389 * 390 * @param aEvent 391 * The continual motion update event to handle 392 */ 393 _doUpdate() {}, 394 395 /** 396 * Handle gesture end events. This function will be set by _setupSwipe. 397 * 398 * @param aEvent 399 * The gesture end event to handle 400 */ 401 _doEnd() {}, 402 403 /** 404 * Convert the swipe gesture into a browser action based on the direction. 405 * 406 * @param aEvent 407 * The swipe event to handle 408 */ 409 onSwipe: function GS_onSwipe(aEvent) { 410 // Figure out which one (and only one) direction was triggered 411 for (let dir of ["UP", "RIGHT", "DOWN", "LEFT"]) { 412 if (aEvent.direction == aEvent["DIRECTION_" + dir]) { 413 this._coordinateSwipeEventWithAnimation(aEvent, dir); 414 break; 415 } 416 } 417 }, 418 419 /** 420 * Process a swipe event based on the given direction. 421 * 422 * @param aEvent 423 * The swipe event to handle 424 * @param aDir 425 * The direction for the swipe event 426 */ 427 processSwipeEvent: function GS_processSwipeEvent(aEvent, aDir) { 428 let dir = aDir.toLowerCase(); 429 // This is a bit of a hack. Ideally we would like our pref names to not 430 // associate a direction (eg left) with a history action (eg back), and 431 // instead name them something like HistoryLeft/Right and then intercept 432 // that in this file and turn it into the back or forward command, but 433 // that involves sending whether we are in LTR or not into _doAction and 434 // _getCommand and then having them recognize that these command needs to 435 // be interpreted differently for rtl/ltr (but not other commands), which 436 // seems more brittle (have to keep all the places in sync) and more code. 437 // So we'll just live with presenting the wrong semantics in the prefs. 438 if (!gHistorySwipeAnimation.isLTR) { 439 if (dir == "right") { 440 dir = "left"; 441 } else if (dir == "left") { 442 dir = "right"; 443 } 444 } 445 this._doAction(aEvent, ["swipe", dir]); 446 }, 447 448 /** 449 * Coordinates the swipe event with the swipe animation, if any. 450 * If an animation is currently running, the swipe event will be 451 * processed once the animation stops. This will guarantee a fluid 452 * motion of the animation. 453 * 454 * @param aEvent 455 * The swipe event to handle 456 * @param aDir 457 * The direction for the swipe event 458 */ 459 _coordinateSwipeEventWithAnimation: 460 function GS__coordinateSwipeEventWithAnimation(aEvent, aDir) { 461 gHistorySwipeAnimation.stopAnimation(); 462 this.processSwipeEvent(aEvent, aDir); 463 }, 464 465 /** 466 * Get a gesture preference or use a default if it doesn't exist 467 * 468 * @param aPref 469 * Name of the preference to load under the gesture branch 470 * @param aDef 471 * Default value if the preference doesn't exist 472 */ 473 _getPref: function GS__getPref(aPref, aDef) { 474 // Preferences branch under which all gestures preferences are stored 475 const branch = "browser.gesture."; 476 477 try { 478 // Determine what type of data to load based on default value's type 479 let type = typeof aDef; 480 let getFunc = "Char"; 481 if (type == "boolean") { 482 getFunc = "Bool"; 483 } else if (type == "number") { 484 getFunc = "Int"; 485 } 486 return Services.prefs["get" + getFunc + "Pref"](branch + aPref); 487 } catch (e) { 488 return aDef; 489 } 490 }, 491 492 /** 493 * Perform rotation for ImageDocuments 494 * 495 * @param aEvent 496 * The MozRotateGestureUpdate event triggering this call 497 */ 498 rotate(aEvent) { 499 if (!ImageDocument.isInstance(window.content.document)) { 500 return; 501 } 502 503 let contentElement = window.content.document.body.firstElementChild; 504 if (!contentElement) { 505 return; 506 } 507 // If we're currently snapping, cancel that snap 508 if (contentElement.classList.contains("completeRotation")) { 509 this._clearCompleteRotation(); 510 } 511 512 this.rotation = Math.round(this.rotation + aEvent.delta); 513 contentElement.style.transform = "rotate(" + this.rotation + "deg)"; 514 this._lastRotateDelta = aEvent.delta; 515 }, 516 517 /** 518 * Perform a rotation end for ImageDocuments 519 */ 520 rotateEnd() { 521 if (!ImageDocument.isInstance(window.content.document)) { 522 return; 523 } 524 525 let contentElement = window.content.document.body.firstElementChild; 526 if (!contentElement) { 527 return; 528 } 529 530 let transitionRotation = 0; 531 532 // The reason that 360 is allowed here is because when rotating between 533 // 315 and 360, setting rotate(0deg) will cause it to rotate the wrong 534 // direction around--spinning wildly. 535 if (this.rotation <= 45) { 536 transitionRotation = 0; 537 } else if (this.rotation > 45 && this.rotation <= 135) { 538 transitionRotation = 90; 539 } else if (this.rotation > 135 && this.rotation <= 225) { 540 transitionRotation = 180; 541 } else if (this.rotation > 225 && this.rotation <= 315) { 542 transitionRotation = 270; 543 } else { 544 transitionRotation = 360; 545 } 546 547 // If we're going fast enough, and we didn't already snap ahead of rotation, 548 // then snap ahead of rotation to simulate momentum 549 if ( 550 this._lastRotateDelta > this._rotateMomentumThreshold && 551 this.rotation > transitionRotation 552 ) { 553 transitionRotation += 90; 554 } else if ( 555 this._lastRotateDelta < -1 * this._rotateMomentumThreshold && 556 this.rotation < transitionRotation 557 ) { 558 transitionRotation -= 90; 559 } 560 561 // Only add the completeRotation class if it is is necessary 562 if (transitionRotation != this.rotation) { 563 contentElement.classList.add("completeRotation"); 564 contentElement.addEventListener( 565 "transitionend", 566 this._clearCompleteRotation 567 ); 568 } 569 570 contentElement.style.transform = "rotate(" + transitionRotation + "deg)"; 571 this.rotation = transitionRotation; 572 }, 573 574 /** 575 * Gets the current rotation for the ImageDocument 576 */ 577 get rotation() { 578 return this._currentRotation; 579 }, 580 581 /** 582 * Sets the current rotation for the ImageDocument 583 * 584 * @param aVal 585 * The new value to take. Can be any value, but it will be bounded to 586 * 0 inclusive to 360 exclusive. 587 */ 588 set rotation(aVal) { 589 this._currentRotation = aVal % 360; 590 if (this._currentRotation < 0) { 591 this._currentRotation += 360; 592 } 593 }, 594 595 /** 596 * When the location/tab changes, need to reload the current rotation for the 597 * image 598 */ 599 restoreRotationState() { 600 // Bug 1108553 - Cannot rotate images in stand-alone image documents with e10s 601 if (gMultiProcessBrowser) { 602 return; 603 } 604 605 if (!ImageDocument.isInstance(window.content.document)) { 606 return; 607 } 608 609 let contentElement = window.content.document.body.firstElementChild; 610 let transformValue = 611 window.content.window.getComputedStyle(contentElement).transform; 612 613 if (transformValue == "none") { 614 this.rotation = 0; 615 return; 616 } 617 618 // transformValue is a rotation matrix--split it and do mathemagic to 619 // obtain the real rotation value 620 transformValue = transformValue.split("(")[1].split(")")[0].split(","); 621 this.rotation = Math.round( 622 Math.atan2(transformValue[1], transformValue[0]) * (180 / Math.PI) 623 ); 624 }, 625 626 /** 627 * Removes the transition rule by removing the completeRotation class 628 */ 629 _clearCompleteRotation() { 630 let contentElement = 631 window.content.document && 632 ImageDocument.isInstance(window.content.document) && 633 window.content.document.body && 634 window.content.document.body.firstElementChild; 635 if (!contentElement) { 636 return; 637 } 638 contentElement.classList.remove("completeRotation"); 639 contentElement.removeEventListener( 640 "transitionend", 641 this._clearCompleteRotation 642 ); 643 }, 644 }; 645 646 // History Swipe Animation Support (bug 678392) 647 var gHistorySwipeAnimation = { 648 active: false, 649 isLTR: false, 650 651 /** 652 * Initializes the support for history swipe animations, if it is supported 653 * by the platform/configuration. 654 */ 655 init: function HSA_init() { 656 this.isLTR = document.documentElement.matches(":-moz-locale-dir(ltr)"); 657 this._isStoppingAnimation = false; 658 659 if (!this._isSupported()) { 660 return; 661 } 662 663 if ( 664 Services.prefs.getBoolPref( 665 "browser.history_swipe_animation.disabled", 666 false 667 ) 668 ) { 669 return; 670 } 671 672 this._icon = document.getElementById("swipe-nav-icon"); 673 this._initPrefValues(); 674 this._addPrefObserver(); 675 this.active = true; 676 }, 677 678 /** 679 * Uninitializes the support for history swipe animations. 680 */ 681 uninit: function HSA_uninit() { 682 this._removePrefObserver(); 683 this.active = false; 684 this.isLTR = false; 685 this._icon = null; 686 this._removeBoxes(); 687 }, 688 689 /** 690 * Starts the swipe animation. 691 * 692 * @param aIsVerticalSwipe 693 * Whether we're dealing with a vertical swipe or not. 694 */ 695 startAnimation: function HSA_startAnimation() { 696 // old boxes can still be around (if completing fade out for example), we 697 // always want to remove them and recreate them because they can be 698 // attached to an old browser stack that's no longer in use. 699 this._removeBoxes(); 700 this._isStoppingAnimation = false; 701 this._canGoBack = this.canGoBack(); 702 this._canGoForward = this.canGoForward(); 703 if (this.active) { 704 this._addBoxes(); 705 } 706 this.updateAnimation(0); 707 }, 708 709 /** 710 * Stops the swipe animation. 711 */ 712 stopAnimation: function HSA_stopAnimation() { 713 if (!this.isAnimationRunning() || this._isStoppingAnimation) { 714 return; 715 } 716 717 let box = null; 718 if (!this._prevBox.collapsed) { 719 box = this._prevBox; 720 } else if (!this._nextBox.collapsed) { 721 box = this._nextBox; 722 } 723 if (box != null) { 724 this._isStoppingAnimation = true; 725 box.style.transition = "opacity 0.35s 0.35s cubic-bezier(.25,.1,0.25,1)"; 726 box.addEventListener("transitionend", this, true); 727 box.style.opacity = 0; 728 window.getComputedStyle(box).opacity; 729 } else { 730 this._isStoppingAnimation = false; 731 this._removeBoxes(); 732 } 733 }, 734 735 _willGoBack: function HSA_willGoBack(aVal) { 736 return ( 737 ((aVal > 0 && this.isLTR) || (aVal < 0 && !this.isLTR)) && this._canGoBack 738 ); 739 }, 740 741 _willGoForward: function HSA_willGoForward(aVal) { 742 return ( 743 ((aVal > 0 && !this.isLTR) || (aVal < 0 && this.isLTR)) && 744 this._canGoForward 745 ); 746 }, 747 748 /** 749 * Updates the animation between two pages in history. 750 * 751 * @param aVal 752 * A floating point value that represents the progress of the 753 * swipe gesture. History navigation will be triggered if the absolute 754 * value of this `aVal` is greater than or equal to 0.25. 755 */ 756 updateAnimation: function HSA_updateAnimation(aVal) { 757 if (!this.isAnimationRunning() || this._isStoppingAnimation) { 758 return; 759 } 760 761 // Convert `aVal` into [0, 1] range. 762 // Note that absolute values of 0.25 (or greater) trigger history 763 // navigation, hence we multiply the value by 4 here. 764 const progress = Math.min(Math.abs(aVal) * 4, 1.0); 765 766 // Compute the icon position based on preferences. 767 let translate = 768 this.translateStartPosition + 769 progress * (this.translateEndPosition - this.translateStartPosition); 770 if (!this.isLTR) { 771 translate = -translate; 772 } 773 774 // Compute the icon radius based on preferences. 775 const radius = 776 this.minRadius + progress * (this.maxRadius - this.minRadius); 777 if (this._willGoBack(aVal)) { 778 this._prevBox.collapsed = false; 779 this._nextBox.collapsed = true; 780 this._prevBox.style.translate = `${translate}px 0px`; 781 if (radius >= 0) { 782 this._prevBox 783 .querySelectorAll("circle")[1] 784 .setAttribute("r", `${radius}`); 785 } 786 787 if (Math.abs(aVal) >= 0.25) { 788 // If `aVal` goes above 0.25, it means history navigation will be 789 // triggered once after the user lifts their fingers, it's time to 790 // trigger __indicator__ animations by adding `will-navigate` class. 791 this._prevBox.querySelector("svg").classList.add("will-navigate"); 792 } else { 793 this._prevBox.querySelector("svg").classList.remove("will-navigate"); 794 } 795 } else if (this._willGoForward(aVal)) { 796 // The intention is to go forward. 797 this._nextBox.collapsed = false; 798 this._prevBox.collapsed = true; 799 this._nextBox.style.translate = `${-translate}px 0px`; 800 if (radius >= 0) { 801 this._nextBox 802 .querySelectorAll("circle")[1] 803 .setAttribute("r", `${radius}`); 804 } 805 806 if (Math.abs(aVal) >= 0.25) { 807 // Same as above "go back" case. 808 this._nextBox.querySelector("svg").classList.add("will-navigate"); 809 } else { 810 this._nextBox.querySelector("svg").classList.remove("will-navigate"); 811 } 812 } else { 813 this._prevBox.collapsed = true; 814 this._nextBox.collapsed = true; 815 this._prevBox.style.translate = "none"; 816 this._nextBox.style.translate = "none"; 817 } 818 }, 819 820 /** 821 * Checks whether the history swipe animation is currently running or not. 822 * 823 * @return true if the animation is currently running, false otherwise. 824 */ 825 isAnimationRunning: function HSA_isAnimationRunning() { 826 return !!this._container; 827 }, 828 829 /** 830 * Checks if there is a page in the browser history to go back to. 831 * 832 * @return true if there is a previous page in history, false otherwise. 833 */ 834 canGoBack: function HSA_canGoBack() { 835 return gBrowser.webNavigation.canGoBack; 836 }, 837 838 /** 839 * Checks if there is a page in the browser history to go forward to. 840 * 841 * @return true if there is a next page in history, false otherwise. 842 */ 843 canGoForward: function HSA_canGoForward() { 844 return gBrowser.webNavigation.canGoForward; 845 }, 846 847 /** 848 * Used to notify the history swipe animation that the OS sent a swipe end 849 * event and that we should navigate to the page that the user swiped to, if 850 * any. This will also result in the animation overlay to be torn down. 851 */ 852 swipeEndEventReceived: function HSA_swipeEndEventReceived() { 853 this.stopAnimation(); 854 }, 855 856 /** 857 * Checks to see if history swipe animations are supported by this 858 * platform/configuration. 859 * 860 * return true if supported, false otherwise. 861 */ 862 _isSupported: function HSA__isSupported() { 863 return window.matchMedia("(-moz-swipe-animation-enabled)").matches; 864 }, 865 866 handleEvent: function HSA_handleEvent(aEvent) { 867 switch (aEvent.type) { 868 case "transitionend": 869 this._completeFadeOut(); 870 break; 871 } 872 }, 873 874 _completeFadeOut: function HSA__completeFadeOut() { 875 if (!this._isStoppingAnimation) { 876 // The animation was restarted in the middle of our stopping fade out 877 // tranistion, so don't do anything. 878 return; 879 } 880 this._isStoppingAnimation = false; 881 gHistorySwipeAnimation._removeBoxes(); 882 }, 883 884 /** 885 * Adds the boxes that contain the arrows used during the swipe animation. 886 */ 887 _addBoxes: function HSA__addBoxes() { 888 let browserStack = gBrowser.getPanel().querySelector(".browserStack"); 889 this._container = this._createElement( 890 "historySwipeAnimationContainer", 891 "stack" 892 ); 893 browserStack.appendChild(this._container); 894 895 this._prevBox = this._createElement( 896 "historySwipeAnimationPreviousArrow", 897 "box" 898 ); 899 this._prevBox.collapsed = true; 900 this._container.appendChild(this._prevBox); 901 let icon = this._icon.cloneNode(true); 902 icon.classList.add("swipe-nav-icon"); 903 this._prevBox.appendChild(icon); 904 905 this._nextBox = this._createElement( 906 "historySwipeAnimationNextArrow", 907 "box" 908 ); 909 this._nextBox.collapsed = true; 910 this._container.appendChild(this._nextBox); 911 icon = this._icon.cloneNode(true); 912 icon.classList.add("swipe-nav-icon"); 913 this._nextBox.appendChild(icon); 914 }, 915 916 /** 917 * Removes the boxes. 918 */ 919 _removeBoxes: function HSA__removeBoxes() { 920 this._prevBox = null; 921 this._nextBox = null; 922 if (this._container) { 923 this._container.remove(); 924 } 925 this._container = null; 926 }, 927 928 /** 929 * Creates an element with a given identifier and tag name. 930 * 931 * @param aID 932 * An identifier to create the element with. 933 * @param aTagName 934 * The name of the tag to create the element for. 935 * @return the newly created element. 936 */ 937 _createElement: function HSA__createElement(aID, aTagName) { 938 let element = document.createXULElement(aTagName); 939 element.id = aID; 940 return element; 941 }, 942 943 observe(subj, topic) { 944 switch (topic) { 945 case "nsPref:changed": 946 this._initPrefValues(); 947 } 948 }, 949 950 _initPrefValues: function HSA__initPrefValues() { 951 this.translateStartPosition = Services.prefs.getIntPref( 952 "browser.swipe.navigation-icon-start-position", 953 0 954 ); 955 this.translateEndPosition = Services.prefs.getIntPref( 956 "browser.swipe.navigation-icon-end-position", 957 0 958 ); 959 this.minRadius = Services.prefs.getIntPref( 960 "browser.swipe.navigation-icon-min-radius", 961 -1 962 ); 963 this.maxRadius = Services.prefs.getIntPref( 964 "browser.swipe.navigation-icon-max-radius", 965 -1 966 ); 967 }, 968 969 _addPrefObserver: function HSA__addPrefObserver() { 970 [ 971 "browser.swipe.navigation-icon-start-position", 972 "browser.swipe.navigation-icon-end-position", 973 "browser.swipe.navigation-icon-min-radius", 974 "browser.swipe.navigation-icon-max-radius", 975 ].forEach(pref => { 976 Services.prefs.addObserver(pref, this); 977 }); 978 }, 979 980 _removePrefObserver: function HSA__removePrefObserver() { 981 [ 982 "browser.swipe.navigation-icon-start-position", 983 "browser.swipe.navigation-icon-end-position", 984 "browser.swipe.navigation-icon-min-radius", 985 "browser.swipe.navigation-icon-max-radius", 986 ].forEach(pref => { 987 Services.prefs.removeObserver(pref, this); 988 }); 989 }, 990 };