tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 };