зеркало из https://github.com/mozilla/gecko-dev.git
847 строки
24 KiB
JavaScript
847 строки
24 KiB
JavaScript
/* 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",
|
|
];
|
|
|
|
let addRemove = aAddListener
|
|
? window.addEventListener
|
|
: window.removeEventListener;
|
|
|
|
for (let event of gestureEvents) {
|
|
addRemove("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 true;
|
|
},
|
|
|
|
/**
|
|
* 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,
|
|
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) {
|
|
this._doAction(aEvent, ["swipe", aDir.toLowerCase()]);
|
|
},
|
|
|
|
/**
|
|
* 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 (!(window.content.document instanceof ImageDocument)) {
|
|
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 (!(window.content.document instanceof ImageDocument)) {
|
|
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;
|
|
}
|
|
return this._currentRotation;
|
|
},
|
|
|
|
/**
|
|
* 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 (!(window.content.document instanceof ImageDocument)) {
|
|
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 &&
|
|
window.content.document instanceof ImageDocument &&
|
|
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() {
|
|
if (!this._isSupported()) {
|
|
return;
|
|
}
|
|
|
|
this.isLTR = document.documentElement.matches(":-moz-locale-dir(ltr)");
|
|
this._isStoppingAnimation = false;
|
|
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() {
|
|
if (this.isAnimationRunning()) {
|
|
return;
|
|
}
|
|
|
|
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()) {
|
|
return;
|
|
}
|
|
this._isStoppingAnimation = true;
|
|
let box = this._prevBox.style.opacity > 0 ? this._prevBox : this._nextBox;
|
|
if (box.style.opacity > 0) {
|
|
box.style.transition = "opacity 0.2s cubic-bezier(.07,.95,0,1)";
|
|
box.addEventListener("transitionend", this._completeFadeOut);
|
|
box.style.opacity = 0;
|
|
} else {
|
|
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;
|
|
}
|
|
|
|
// We use the following value to set the opacity of the swipe arrows. It was
|
|
// determined experimentally that 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 ((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;
|
|
}
|
|
} 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;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
},
|
|
|
|
_completeFadeOut: function HSA__completeFadeOut(aEvent) {
|
|
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;
|
|
},
|
|
};
|