diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /browser/base/content/browser-gestureSupport.js | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/base/content/browser-gestureSupport.js')
-rw-r--r-- | browser/base/content/browser-gestureSupport.js | 944 |
1 files changed, 944 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..fce6b2716a --- /dev/null +++ b/browser/base/content/browser-gestureSupport.js @@ -0,0 +1,944 @@ +/* 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 + ) + ) { + this.active = true; + } + }, + + /** + * Uninitializes the support for history swipe animations. + */ + uninit: function HSA_uninit() { + this.active = false; + this.isLTR = false; + 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 prevOpacity = window + .getComputedStyle(this._prevBox) + .getPropertyValue("opacity"); + let nextOpacity = window + .getComputedStyle(this._nextBox) + .getPropertyValue("opacity"); + let box = null; + if (prevOpacity > 0) { + box = this._prevBox; + } else if (nextOpacity > 0) { + box = this._nextBox; + } + if (box != null) { + this._isStoppingAnimation = true; + box.style.transition = "opacity 0.35s cubic-bezier(.25,.1,0.25,1)"; + box.addEventListener("transitionend", this, true); + box.style.opacity = 0; + } else { + this._isStoppingAnimation = false; + this._removeBoxes(); + } + }, + + /** + * Updates the animation between two pages in history. + * + * @param aVal + * A floating point value that represents the progress of the + * swipe gesture. + */ + updateAnimation: function HSA_updateAnimation(aVal) { + if (!this.isAnimationRunning() || this._isStoppingAnimation) { + return; + } + + const translateDistance = Services.prefs.getIntPref( + "browser.swipe.navigation-icon-move-distance", + 0 + ); + // We use the following value to set the opacity (and translate on Windows) + // of the swipe arrows. Absolute values of 0.25 (or greater) trigger + // history navigation, hence the multiplier 4 to set the arrows to full + // opacity at 0.25 or greater. + let opacity = Math.abs(aVal) * 4; + + // If we move the icon along with the opacity change, we make the opacity + // change 2x faster so that it will avoid appearing the icon suddenly away + // from the browser edge. + if (translateDistance > 0) { + opacity *= 2; + } + + // The icon moves from -20% of |translateDistance| so that it will be + // partially out of the viewport. + let translate = + Math.abs(aVal) * 4 * translateDistance - 0.2 * translateDistance; + + // Clamp inside [-20% of translateDistance, 80% of translateDistance]. + translate = Math.min( + Math.max(-translateDistance * 0.2, translate), + translateDistance * 0.8 + ); + + if (!this.isLTR) { + translate = -translate; + } + + if ((aVal >= 0 && this.isLTR) || (aVal <= 0 && !this.isLTR)) { + // The intention is to go back. + if (this._canGoBack) { + this._prevBox.collapsed = false; + this._nextBox.collapsed = true; + this._prevBox.style.opacity = opacity > 1 ? 1 : opacity; + this._prevBox.style.translate = `${translate}px 0px`; + + if (translateDistance > 0) { + if (Math.abs(aVal) >= 0.25) { + // Add a glow the same color as the circle surrounding the arrow. + // If the page background is too close to this color it won't be + // distinguishable, but hopefully that's not too common. + this._prevBox.style.fill = "rgb(56, 56, 61)"; + } else { + // Hide the glow. + this._prevBox.style.fill = "rgba(0,0,0,0)"; + } + } + } + } else if (this._canGoForward) { + // The intention is to go forward. + this._nextBox.collapsed = false; + this._prevBox.collapsed = true; + this._nextBox.style.opacity = opacity > 1 ? 1 : opacity; + this._nextBox.style.translate = `${-translate}px 0px`; + + if (translateDistance > 0) { + if (Math.abs(aVal) >= 0.25) { + // Add a glow the same color as the circle surrounding the arrow. + // If the page background is too close to this color it won't be + // distinguishable, but hopefully that's not too common. + this._nextBox.style.fill = "rgb(56, 56, 61)"; + } else { + // Hide the glow. + this._nextBox.style.fill = "rgba(0,0,0,0)"; + } + } + } + }, + + /** + * 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._prevBox.style.opacity = 0; + this._container.appendChild(this._prevBox); + + this._nextBox = this._createElement( + "historySwipeAnimationNextArrow", + "box" + ); + this._nextBox.collapsed = true; + this._nextBox.style.opacity = 0; + this._container.appendChild(this._nextBox); + }, + + /** + * 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; + }, +}; |