summaryrefslogtreecommitdiffstats
path: root/browser/base/content/browser-gestureSupport.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/browser-gestureSupport.js')
-rw-r--r--browser/base/content/browser-gestureSupport.js993
1 files changed, 993 insertions, 0 deletions
diff --git a/browser/base/content/browser-gestureSupport.js b/browser/base/content/browser-gestureSupport.js
new file mode 100644
index 0000000000..f9a094c499
--- /dev/null
+++ b/browser/base/content/browser-gestureSupport.js
@@ -0,0 +1,993 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is loaded into the browser window scope.
+/* eslint-env mozilla/browser-window */
+
+// Simple gestures support
+//
+// As per bug #412486, web content must not be allowed to receive any
+// simple gesture events. Multi-touch gesture APIs are in their
+// infancy and we do NOT want to be forced into supporting an API that
+// will probably have to change in the future. (The current Mac OS X
+// API is undocumented and was reverse-engineered.) Until support is
+// implemented in the event dispatcher to keep these events as
+// chrome-only, we must listen for the simple gesture events during
+// the capturing phase and call stopPropagation on every event.
+
+var gGestureSupport = {
+ _currentRotation: 0,
+ _lastRotateDelta: 0,
+ _rotateMomentumThreshold: 0.75,
+
+ /**
+ * Add or remove mouse gesture event listeners
+ *
+ * @param aAddListener
+ * True to add/init listeners and false to remove/uninit
+ */
+ init: function GS_init(aAddListener) {
+ const gestureEvents = [
+ "SwipeGestureMayStart",
+ "SwipeGestureStart",
+ "SwipeGestureUpdate",
+ "SwipeGestureEnd",
+ "SwipeGesture",
+ "MagnifyGestureStart",
+ "MagnifyGestureUpdate",
+ "MagnifyGesture",
+ "RotateGestureStart",
+ "RotateGestureUpdate",
+ "RotateGesture",
+ "TapGesture",
+ "PressTapGesture",
+ ];
+
+ for (let event of gestureEvents) {
+ if (aAddListener) {
+ gBrowser.tabbox.addEventListener("Moz" + event, this, true);
+ } else {
+ gBrowser.tabbox.removeEventListener("Moz" + event, this, true);
+ }
+ }
+ },
+
+ /**
+ * Dispatch events based on the type of mouse gesture event. For now, make
+ * sure to stop propagation of every gesture event so that web content cannot
+ * receive gesture events.
+ *
+ * @param aEvent
+ * The gesture event to handle
+ */
+ handleEvent: function GS_handleEvent(aEvent) {
+ if (
+ !Services.prefs.getBoolPref(
+ "dom.debug.propagate_gesture_events_through_content"
+ )
+ ) {
+ aEvent.stopPropagation();
+ }
+
+ // Create a preference object with some defaults
+ let def = (aThreshold, aLatched) => ({
+ threshold: aThreshold,
+ latched: !!aLatched,
+ });
+
+ switch (aEvent.type) {
+ case "MozSwipeGestureMayStart":
+ if (this._shouldDoSwipeGesture(aEvent)) {
+ aEvent.preventDefault();
+ }
+ break;
+ case "MozSwipeGestureStart":
+ aEvent.preventDefault();
+ this._setupSwipeGesture();
+ break;
+ case "MozSwipeGestureUpdate":
+ aEvent.preventDefault();
+ this._doUpdate(aEvent);
+ break;
+ case "MozSwipeGestureEnd":
+ aEvent.preventDefault();
+ this._doEnd(aEvent);
+ break;
+ case "MozSwipeGesture":
+ aEvent.preventDefault();
+ this.onSwipe(aEvent);
+ break;
+ case "MozMagnifyGestureStart":
+ aEvent.preventDefault();
+ this._setupGesture(aEvent, "pinch", def(25, 0), "out", "in");
+ break;
+ case "MozRotateGestureStart":
+ aEvent.preventDefault();
+ this._setupGesture(aEvent, "twist", def(25, 0), "right", "left");
+ break;
+ case "MozMagnifyGestureUpdate":
+ case "MozRotateGestureUpdate":
+ aEvent.preventDefault();
+ this._doUpdate(aEvent);
+ break;
+ case "MozTapGesture":
+ aEvent.preventDefault();
+ this._doAction(aEvent, ["tap"]);
+ break;
+ case "MozRotateGesture":
+ aEvent.preventDefault();
+ this._doAction(aEvent, ["twist", "end"]);
+ break;
+ /* case "MozPressTapGesture":
+ break; */
+ }
+ },
+
+ /**
+ * Called at the start of "pinch" and "twist" gestures to setup all of the
+ * information needed to process the gesture
+ *
+ * @param aEvent
+ * The continual motion start event to handle
+ * @param aGesture
+ * Name of the gesture to handle
+ * @param aPref
+ * Preference object with the names of preferences and defaults
+ * @param aInc
+ * Command to trigger for increasing motion (without gesture name)
+ * @param aDec
+ * Command to trigger for decreasing motion (without gesture name)
+ */
+ _setupGesture: function GS__setupGesture(
+ aEvent,
+ aGesture,
+ aPref,
+ aInc,
+ aDec
+ ) {
+ // Try to load user-set values from preferences
+ for (let [pref, def] of Object.entries(aPref)) {
+ aPref[pref] = this._getPref(aGesture + "." + pref, def);
+ }
+
+ // Keep track of the total deltas and latching behavior
+ let offset = 0;
+ let latchDir = aEvent.delta > 0 ? 1 : -1;
+ let isLatched = false;
+
+ // Create the update function here to capture closure state
+ this._doUpdate = function GS__doUpdate(updateEvent) {
+ // Update the offset with new event data
+ offset += updateEvent.delta;
+
+ // Check if the cumulative deltas exceed the threshold
+ if (Math.abs(offset) > aPref.threshold) {
+ // Trigger the action if we don't care about latching; otherwise, make
+ // sure either we're not latched and going the same direction of the
+ // initial motion; or we're latched and going the opposite way
+ let sameDir = (latchDir ^ offset) >= 0;
+ if (!aPref.latched || isLatched ^ sameDir) {
+ this._doAction(updateEvent, [aGesture, offset > 0 ? aInc : aDec]);
+
+ // We must be getting latched or leaving it, so just toggle
+ isLatched = !isLatched;
+ }
+
+ // Reset motion counter to prepare for more of the same gesture
+ offset = 0;
+ }
+ };
+
+ // The start event also contains deltas, so handle an update right away
+ this._doUpdate(aEvent);
+ },
+
+ /**
+ * Checks whether a swipe gesture event can navigate the browser history or
+ * not.
+ *
+ * @param aEvent
+ * The swipe gesture event.
+ * @return true if the swipe event may navigate the history, false othwerwise.
+ */
+ _swipeNavigatesHistory: function GS__swipeNavigatesHistory(aEvent) {
+ return (
+ this._getCommand(aEvent, ["swipe", "left"]) ==
+ "Browser:BackOrBackDuplicate" &&
+ this._getCommand(aEvent, ["swipe", "right"]) ==
+ "Browser:ForwardOrForwardDuplicate"
+ );
+ },
+
+ /**
+ * Checks whether we want to start a swipe for aEvent and sets
+ * aEvent.allowedDirections to the right values.
+ *
+ * @param aEvent
+ * The swipe gesture "MayStart" event.
+ * @return true if we're willing to start a swipe for this event, false
+ * otherwise.
+ */
+ _shouldDoSwipeGesture: function GS__shouldDoSwipeGesture(aEvent) {
+ if (!this._swipeNavigatesHistory(aEvent)) {
+ return false;
+ }
+
+ let isVerticalSwipe = false;
+ if (aEvent.direction == aEvent.DIRECTION_UP) {
+ if (gMultiProcessBrowser || window.content.pageYOffset > 0) {
+ return false;
+ }
+ isVerticalSwipe = true;
+ } else if (aEvent.direction == aEvent.DIRECTION_DOWN) {
+ if (
+ gMultiProcessBrowser ||
+ window.content.pageYOffset < window.content.scrollMaxY
+ ) {
+ return false;
+ }
+ isVerticalSwipe = true;
+ }
+ if (isVerticalSwipe) {
+ // Vertical overscroll has been temporarily disabled until bug 939480 is
+ // fixed.
+ return false;
+ }
+
+ let canGoBack = gHistorySwipeAnimation.canGoBack();
+ let canGoForward = gHistorySwipeAnimation.canGoForward();
+ let isLTR = gHistorySwipeAnimation.isLTR;
+
+ if (canGoBack) {
+ aEvent.allowedDirections |= isLTR
+ ? aEvent.DIRECTION_LEFT
+ : aEvent.DIRECTION_RIGHT;
+ }
+ if (canGoForward) {
+ aEvent.allowedDirections |= isLTR
+ ? aEvent.DIRECTION_RIGHT
+ : aEvent.DIRECTION_LEFT;
+ }
+
+ return canGoBack || canGoForward;
+ },
+
+ /**
+ * Sets up swipe gestures. This includes setting up swipe animations for the
+ * gesture, if enabled.
+ *
+ * @param aEvent
+ * The swipe gesture start event.
+ * @return true if swipe gestures could successfully be set up, false
+ * othwerwise.
+ */
+ _setupSwipeGesture: function GS__setupSwipeGesture() {
+ gHistorySwipeAnimation.startAnimation();
+
+ this._doUpdate = function GS__doUpdate(aEvent) {
+ gHistorySwipeAnimation.updateAnimation(aEvent.delta);
+ };
+
+ this._doEnd = function GS__doEnd(aEvent) {
+ gHistorySwipeAnimation.swipeEndEventReceived();
+
+ this._doUpdate = function () {};
+ this._doEnd = function () {};
+ };
+ },
+
+ /**
+ * Generator producing the powerset of the input array where the first result
+ * is the complete set and the last result (before StopIteration) is empty.
+ *
+ * @param aArray
+ * Source array containing any number of elements
+ * @yield Array that is a subset of the input array from full set to empty
+ */
+ _power: function* GS__power(aArray) {
+ // Create a bitmask based on the length of the array
+ let num = 1 << aArray.length;
+ while (--num >= 0) {
+ // Only select array elements where the current bit is set
+ yield aArray.reduce(function (aPrev, aCurr, aIndex) {
+ if (num & (1 << aIndex)) {
+ aPrev.push(aCurr);
+ }
+ return aPrev;
+ }, []);
+ }
+ },
+
+ /**
+ * Determine what action to do for the gesture based on which keys are
+ * pressed and which commands are set, and execute the command.
+ *
+ * @param aEvent
+ * The original gesture event to convert into a fake click event
+ * @param aGesture
+ * Array of gesture name parts (to be joined by periods)
+ * @return Name of the executed command. Returns null if no command is
+ * found.
+ */
+ _doAction: function GS__doAction(aEvent, aGesture) {
+ let command = this._getCommand(aEvent, aGesture);
+ return command && this._doCommand(aEvent, command);
+ },
+
+ /**
+ * Determine what action to do for the gesture based on which keys are
+ * pressed and which commands are set
+ *
+ * @param aEvent
+ * The original gesture event to convert into a fake click event
+ * @param aGesture
+ * Array of gesture name parts (to be joined by periods)
+ */
+ _getCommand: function GS__getCommand(aEvent, aGesture) {
+ // Create an array of pressed keys in a fixed order so that a command for
+ // "meta" is preferred over "ctrl" when both buttons are pressed (and a
+ // command for both don't exist)
+ let keyCombos = [];
+ for (let key of ["shift", "alt", "ctrl", "meta"]) {
+ if (aEvent[key + "Key"]) {
+ keyCombos.push(key);
+ }
+ }
+
+ // Try each combination of key presses in decreasing order for commands
+ for (let subCombo of this._power(keyCombos)) {
+ // Convert a gesture and pressed keys into the corresponding command
+ // action where the preference has the gesture before "shift" before
+ // "alt" before "ctrl" before "meta" all separated by periods
+ let command;
+ try {
+ command = this._getPref(aGesture.concat(subCombo).join("."));
+ } catch (e) {}
+
+ if (command) {
+ return command;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Execute the specified command.
+ *
+ * @param aEvent
+ * The original gesture event to convert into a fake click event
+ * @param aCommand
+ * Name of the command found for the event's keys and gesture.
+ */
+ _doCommand: function GS__doCommand(aEvent, aCommand) {
+ let node = document.getElementById(aCommand);
+ if (node) {
+ if (node.getAttribute("disabled") != "true") {
+ let cmdEvent = document.createEvent("xulcommandevent");
+ cmdEvent.initCommandEvent(
+ "command",
+ true,
+ true,
+ window,
+ 0,
+ aEvent.ctrlKey,
+ aEvent.altKey,
+ aEvent.shiftKey,
+ aEvent.metaKey,
+ 0,
+ aEvent,
+ aEvent.mozInputSource
+ );
+ node.dispatchEvent(cmdEvent);
+ }
+ } else {
+ goDoCommand(aCommand);
+ }
+ },
+
+ /**
+ * Handle continual motion events. This function will be set by
+ * _setupGesture or _setupSwipe.
+ *
+ * @param aEvent
+ * The continual motion update event to handle
+ */
+ _doUpdate(aEvent) {},
+
+ /**
+ * Handle gesture end events. This function will be set by _setupSwipe.
+ *
+ * @param aEvent
+ * The gesture end event to handle
+ */
+ _doEnd(aEvent) {},
+
+ /**
+ * Convert the swipe gesture into a browser action based on the direction.
+ *
+ * @param aEvent
+ * The swipe event to handle
+ */
+ onSwipe: function GS_onSwipe(aEvent) {
+ // Figure out which one (and only one) direction was triggered
+ for (let dir of ["UP", "RIGHT", "DOWN", "LEFT"]) {
+ if (aEvent.direction == aEvent["DIRECTION_" + dir]) {
+ this._coordinateSwipeEventWithAnimation(aEvent, dir);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Process a swipe event based on the given direction.
+ *
+ * @param aEvent
+ * The swipe event to handle
+ * @param aDir
+ * The direction for the swipe event
+ */
+ processSwipeEvent: function GS_processSwipeEvent(aEvent, aDir) {
+ let dir = aDir.toLowerCase();
+ // This is a bit of a hack. Ideally we would like our pref names to not
+ // associate a direction (eg left) with a history action (eg back), and
+ // instead name them something like HistoryLeft/Right and then intercept
+ // that in this file and turn it into the back or forward command, but
+ // that involves sending whether we are in LTR or not into _doAction and
+ // _getCommand and then having them recognize that these command needs to
+ // be interpreted differently for rtl/ltr (but not other commands), which
+ // seems more brittle (have to keep all the places in sync) and more code.
+ // So we'll just live with presenting the wrong semantics in the prefs.
+ if (!gHistorySwipeAnimation.isLTR) {
+ if (dir == "right") {
+ dir = "left";
+ } else if (dir == "left") {
+ dir = "right";
+ }
+ }
+ this._doAction(aEvent, ["swipe", dir]);
+ },
+
+ /**
+ * Coordinates the swipe event with the swipe animation, if any.
+ * If an animation is currently running, the swipe event will be
+ * processed once the animation stops. This will guarantee a fluid
+ * motion of the animation.
+ *
+ * @param aEvent
+ * The swipe event to handle
+ * @param aDir
+ * The direction for the swipe event
+ */
+ _coordinateSwipeEventWithAnimation:
+ function GS__coordinateSwipeEventWithAnimation(aEvent, aDir) {
+ gHistorySwipeAnimation.stopAnimation();
+ this.processSwipeEvent(aEvent, aDir);
+ },
+
+ /**
+ * Get a gesture preference or use a default if it doesn't exist
+ *
+ * @param aPref
+ * Name of the preference to load under the gesture branch
+ * @param aDef
+ * Default value if the preference doesn't exist
+ */
+ _getPref: function GS__getPref(aPref, aDef) {
+ // Preferences branch under which all gestures preferences are stored
+ const branch = "browser.gesture.";
+
+ try {
+ // Determine what type of data to load based on default value's type
+ let type = typeof aDef;
+ let getFunc = "Char";
+ if (type == "boolean") {
+ getFunc = "Bool";
+ } else if (type == "number") {
+ getFunc = "Int";
+ }
+ return Services.prefs["get" + getFunc + "Pref"](branch + aPref);
+ } catch (e) {
+ return aDef;
+ }
+ },
+
+ /**
+ * Perform rotation for ImageDocuments
+ *
+ * @param aEvent
+ * The MozRotateGestureUpdate event triggering this call
+ */
+ rotate(aEvent) {
+ if (!ImageDocument.isInstance(window.content.document)) {
+ return;
+ }
+
+ let contentElement = window.content.document.body.firstElementChild;
+ if (!contentElement) {
+ return;
+ }
+ // If we're currently snapping, cancel that snap
+ if (contentElement.classList.contains("completeRotation")) {
+ this._clearCompleteRotation();
+ }
+
+ this.rotation = Math.round(this.rotation + aEvent.delta);
+ contentElement.style.transform = "rotate(" + this.rotation + "deg)";
+ this._lastRotateDelta = aEvent.delta;
+ },
+
+ /**
+ * Perform a rotation end for ImageDocuments
+ */
+ rotateEnd() {
+ if (!ImageDocument.isInstance(window.content.document)) {
+ return;
+ }
+
+ let contentElement = window.content.document.body.firstElementChild;
+ if (!contentElement) {
+ return;
+ }
+
+ let transitionRotation = 0;
+
+ // The reason that 360 is allowed here is because when rotating between
+ // 315 and 360, setting rotate(0deg) will cause it to rotate the wrong
+ // direction around--spinning wildly.
+ if (this.rotation <= 45) {
+ transitionRotation = 0;
+ } else if (this.rotation > 45 && this.rotation <= 135) {
+ transitionRotation = 90;
+ } else if (this.rotation > 135 && this.rotation <= 225) {
+ transitionRotation = 180;
+ } else if (this.rotation > 225 && this.rotation <= 315) {
+ transitionRotation = 270;
+ } else {
+ transitionRotation = 360;
+ }
+
+ // If we're going fast enough, and we didn't already snap ahead of rotation,
+ // then snap ahead of rotation to simulate momentum
+ if (
+ this._lastRotateDelta > this._rotateMomentumThreshold &&
+ this.rotation > transitionRotation
+ ) {
+ transitionRotation += 90;
+ } else if (
+ this._lastRotateDelta < -1 * this._rotateMomentumThreshold &&
+ this.rotation < transitionRotation
+ ) {
+ transitionRotation -= 90;
+ }
+
+ // Only add the completeRotation class if it is is necessary
+ if (transitionRotation != this.rotation) {
+ contentElement.classList.add("completeRotation");
+ contentElement.addEventListener(
+ "transitionend",
+ this._clearCompleteRotation
+ );
+ }
+
+ contentElement.style.transform = "rotate(" + transitionRotation + "deg)";
+ this.rotation = transitionRotation;
+ },
+
+ /**
+ * Gets the current rotation for the ImageDocument
+ */
+ get rotation() {
+ return this._currentRotation;
+ },
+
+ /**
+ * Sets the current rotation for the ImageDocument
+ *
+ * @param aVal
+ * The new value to take. Can be any value, but it will be bounded to
+ * 0 inclusive to 360 exclusive.
+ */
+ set rotation(aVal) {
+ this._currentRotation = aVal % 360;
+ if (this._currentRotation < 0) {
+ this._currentRotation += 360;
+ }
+ },
+
+ /**
+ * When the location/tab changes, need to reload the current rotation for the
+ * image
+ */
+ restoreRotationState() {
+ // Bug 1108553 - Cannot rotate images in stand-alone image documents with e10s
+ if (gMultiProcessBrowser) {
+ return;
+ }
+
+ if (!ImageDocument.isInstance(window.content.document)) {
+ return;
+ }
+
+ let contentElement = window.content.document.body.firstElementChild;
+ let transformValue =
+ window.content.window.getComputedStyle(contentElement).transform;
+
+ if (transformValue == "none") {
+ this.rotation = 0;
+ return;
+ }
+
+ // transformValue is a rotation matrix--split it and do mathemagic to
+ // obtain the real rotation value
+ transformValue = transformValue.split("(")[1].split(")")[0].split(",");
+ this.rotation = Math.round(
+ Math.atan2(transformValue[1], transformValue[0]) * (180 / Math.PI)
+ );
+ },
+
+ /**
+ * Removes the transition rule by removing the completeRotation class
+ */
+ _clearCompleteRotation() {
+ let contentElement =
+ window.content.document &&
+ ImageDocument.isInstance(window.content.document) &&
+ window.content.document.body &&
+ window.content.document.body.firstElementChild;
+ if (!contentElement) {
+ return;
+ }
+ contentElement.classList.remove("completeRotation");
+ contentElement.removeEventListener(
+ "transitionend",
+ this._clearCompleteRotation
+ );
+ },
+};
+
+// History Swipe Animation Support (bug 678392)
+var gHistorySwipeAnimation = {
+ active: false,
+ isLTR: false,
+
+ /**
+ * Initializes the support for history swipe animations, if it is supported
+ * by the platform/configuration.
+ */
+ init: function HSA_init() {
+ this.isLTR = document.documentElement.matches(":-moz-locale-dir(ltr)");
+ this._isStoppingAnimation = false;
+
+ if (!this._isSupported()) {
+ return;
+ }
+
+ if (
+ Services.prefs.getBoolPref(
+ "browser.history_swipe_animation.disabled",
+ false
+ )
+ ) {
+ return;
+ }
+
+ this._icon = document.getElementById("swipe-nav-icon");
+ this._initPrefValues();
+ this._addPrefObserver();
+ this.active = true;
+ },
+
+ /**
+ * Uninitializes the support for history swipe animations.
+ */
+ uninit: function HSA_uninit() {
+ this._removePrefObserver();
+ this.active = false;
+ this.isLTR = false;
+ this._icon = null;
+ this._removeBoxes();
+ },
+
+ /**
+ * Starts the swipe animation.
+ *
+ * @param aIsVerticalSwipe
+ * Whether we're dealing with a vertical swipe or not.
+ */
+ startAnimation: function HSA_startAnimation() {
+ // old boxes can still be around (if completing fade out for example), we
+ // always want to remove them and recreate them because they can be
+ // attached to an old browser stack that's no longer in use.
+ this._removeBoxes();
+ this._isStoppingAnimation = false;
+ this._canGoBack = this.canGoBack();
+ this._canGoForward = this.canGoForward();
+ if (this.active) {
+ this._addBoxes();
+ }
+ this.updateAnimation(0);
+ },
+
+ /**
+ * Stops the swipe animation.
+ */
+ stopAnimation: function HSA_stopAnimation() {
+ if (!this.isAnimationRunning() || this._isStoppingAnimation) {
+ return;
+ }
+
+ let box = null;
+ if (!this._prevBox.collapsed) {
+ box = this._prevBox;
+ } else if (!this._nextBox.collapsed) {
+ box = this._nextBox;
+ }
+ if (box != null) {
+ this._isStoppingAnimation = true;
+ box.style.transition = "opacity 0.35s 0.35s cubic-bezier(.25,.1,0.25,1)";
+ box.addEventListener("transitionend", this, true);
+ box.style.opacity = 0;
+ window.getComputedStyle(box).opacity;
+ } else {
+ this._isStoppingAnimation = false;
+ this._removeBoxes();
+ }
+ },
+
+ _willGoBack: function HSA_willGoBack(aVal) {
+ return (
+ ((aVal > 0 && this.isLTR) || (aVal < 0 && !this.isLTR)) && this._canGoBack
+ );
+ },
+
+ _willGoForward: function HSA_willGoForward(aVal) {
+ return (
+ ((aVal > 0 && !this.isLTR) || (aVal < 0 && this.isLTR)) &&
+ this._canGoForward
+ );
+ },
+
+ /**
+ * Updates the animation between two pages in history.
+ *
+ * @param aVal
+ * A floating point value that represents the progress of the
+ * swipe gesture. History navigation will be triggered if the absolute
+ * value of this `aVal` is greater than or equal to 0.25.
+ */
+ updateAnimation: function HSA_updateAnimation(aVal) {
+ if (!this.isAnimationRunning() || this._isStoppingAnimation) {
+ return;
+ }
+
+ // Convert `aVal` into [0, 1] range.
+ // Note that absolute values of 0.25 (or greater) trigger history
+ // navigation, hence we multiply the value by 4 here.
+ const progress = Math.min(Math.abs(aVal) * 4, 1.0);
+
+ // Compute the icon position based on preferences.
+ let translate =
+ this.translateStartPosition +
+ progress * (this.translateEndPosition - this.translateStartPosition);
+ if (!this.isLTR) {
+ translate = -translate;
+ }
+
+ // Compute the icon radius based on preferences.
+ const radius =
+ this.minRadius + progress * (this.maxRadius - this.minRadius);
+ if (this._willGoBack(aVal)) {
+ this._prevBox.collapsed = false;
+ this._nextBox.collapsed = true;
+ this._prevBox.style.translate = `${translate}px 0px`;
+ if (radius >= 0) {
+ this._prevBox
+ .querySelectorAll("circle")[1]
+ .setAttribute("r", `${radius}`);
+ }
+
+ if (Math.abs(aVal) >= 0.25) {
+ // If `aVal` goes above 0.25, it means history navigation will be
+ // triggered once after the user lifts their fingers, it's time to
+ // trigger __indicator__ animations by adding `will-navigate` class.
+ this._prevBox.querySelector("svg").classList.add("will-navigate");
+ } else {
+ this._prevBox.querySelector("svg").classList.remove("will-navigate");
+ }
+ } else if (this._willGoForward(aVal)) {
+ // The intention is to go forward.
+ this._nextBox.collapsed = false;
+ this._prevBox.collapsed = true;
+ this._nextBox.style.translate = `${-translate}px 0px`;
+ if (radius >= 0) {
+ this._nextBox
+ .querySelectorAll("circle")[1]
+ .setAttribute("r", `${radius}`);
+ }
+
+ if (Math.abs(aVal) >= 0.25) {
+ // Same as above "go back" case.
+ this._nextBox.querySelector("svg").classList.add("will-navigate");
+ } else {
+ this._nextBox.querySelector("svg").classList.remove("will-navigate");
+ }
+ } else {
+ this._prevBox.collapsed = true;
+ this._nextBox.collapsed = true;
+ this._prevBox.style.translate = "none";
+ this._nextBox.style.translate = "none";
+ }
+ },
+
+ /**
+ * Checks whether the history swipe animation is currently running or not.
+ *
+ * @return true if the animation is currently running, false otherwise.
+ */
+ isAnimationRunning: function HSA_isAnimationRunning() {
+ return !!this._container;
+ },
+
+ /**
+ * Checks if there is a page in the browser history to go back to.
+ *
+ * @return true if there is a previous page in history, false otherwise.
+ */
+ canGoBack: function HSA_canGoBack() {
+ return gBrowser.webNavigation.canGoBack;
+ },
+
+ /**
+ * Checks if there is a page in the browser history to go forward to.
+ *
+ * @return true if there is a next page in history, false otherwise.
+ */
+ canGoForward: function HSA_canGoForward() {
+ return gBrowser.webNavigation.canGoForward;
+ },
+
+ /**
+ * Used to notify the history swipe animation that the OS sent a swipe end
+ * event and that we should navigate to the page that the user swiped to, if
+ * any. This will also result in the animation overlay to be torn down.
+ */
+ swipeEndEventReceived: function HSA_swipeEndEventReceived() {
+ this.stopAnimation();
+ },
+
+ /**
+ * Checks to see if history swipe animations are supported by this
+ * platform/configuration.
+ *
+ * return true if supported, false otherwise.
+ */
+ _isSupported: function HSA__isSupported() {
+ return window.matchMedia("(-moz-swipe-animation-enabled)").matches;
+ },
+
+ handleEvent: function HSA_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "transitionend":
+ this._completeFadeOut();
+ break;
+ }
+ },
+
+ _completeFadeOut: function HSA__completeFadeOut(aEvent) {
+ if (!this._isStoppingAnimation) {
+ // The animation was restarted in the middle of our stopping fade out
+ // tranistion, so don't do anything.
+ return;
+ }
+ this._isStoppingAnimation = false;
+ gHistorySwipeAnimation._removeBoxes();
+ },
+
+ /**
+ * Adds the boxes that contain the arrows used during the swipe animation.
+ */
+ _addBoxes: function HSA__addBoxes() {
+ let browserStack = gBrowser.getPanel().querySelector(".browserStack");
+ this._container = this._createElement(
+ "historySwipeAnimationContainer",
+ "stack"
+ );
+ browserStack.appendChild(this._container);
+
+ this._prevBox = this._createElement(
+ "historySwipeAnimationPreviousArrow",
+ "box"
+ );
+ this._prevBox.collapsed = true;
+ this._container.appendChild(this._prevBox);
+ let icon = this._icon.cloneNode(true);
+ icon.classList.add("swipe-nav-icon");
+ this._prevBox.appendChild(icon);
+
+ this._nextBox = this._createElement(
+ "historySwipeAnimationNextArrow",
+ "box"
+ );
+ this._nextBox.collapsed = true;
+ this._container.appendChild(this._nextBox);
+ icon = this._icon.cloneNode(true);
+ icon.classList.add("swipe-nav-icon");
+ this._nextBox.appendChild(icon);
+ },
+
+ /**
+ * Removes the boxes.
+ */
+ _removeBoxes: function HSA__removeBoxes() {
+ this._prevBox = null;
+ this._nextBox = null;
+ if (this._container) {
+ this._container.remove();
+ }
+ this._container = null;
+ },
+
+ /**
+ * Creates an element with a given identifier and tag name.
+ *
+ * @param aID
+ * An identifier to create the element with.
+ * @param aTagName
+ * The name of the tag to create the element for.
+ * @return the newly created element.
+ */
+ _createElement: function HSA__createElement(aID, aTagName) {
+ let element = document.createXULElement(aTagName);
+ element.id = aID;
+ return element;
+ },
+
+ observe(subj, topic, data) {
+ switch (topic) {
+ case "nsPref:changed":
+ this._initPrefValues();
+ }
+ },
+
+ _initPrefValues: function HSA__initPrefValues() {
+ this.translateStartPosition = Services.prefs.getIntPref(
+ "browser.swipe.navigation-icon-start-position",
+ 0
+ );
+ this.translateEndPosition = Services.prefs.getIntPref(
+ "browser.swipe.navigation-icon-end-position",
+ 0
+ );
+ this.minRadius = Services.prefs.getIntPref(
+ "browser.swipe.navigation-icon-min-radius",
+ -1
+ );
+ this.maxRadius = Services.prefs.getIntPref(
+ "browser.swipe.navigation-icon-max-radius",
+ -1
+ );
+ },
+
+ _addPrefObserver: function HSA__addPrefObserver() {
+ [
+ "browser.swipe.navigation-icon-start-position",
+ "browser.swipe.navigation-icon-end-position",
+ "browser.swipe.navigation-icon-min-radius",
+ "browser.swipe.navigation-icon-max-radius",
+ ].forEach(pref => {
+ Services.prefs.addObserver(pref, this);
+ });
+ },
+
+ _removePrefObserver: function HSA__removePrefObserver() {
+ [
+ "browser.swipe.navigation-icon-start-position",
+ "browser.swipe.navigation-icon-end-position",
+ "browser.swipe.navigation-icon-min-radius",
+ "browser.swipe.navigation-icon-max-radius",
+ ].forEach(pref => {
+ Services.prefs.removeObserver(pref, this);
+ });
+ },
+};