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