/*
This file contains proprietary software owned by Motorola Mobility, Inc.
No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.
(c) Copyright 2011 Motorola Mobility, Inc. All Rights Reserved.
*/
/**
@module montage/ui/composer/translate-composer
@requires montage
@requires montage/ui/composer/composer
*/
var Montage = require("montage").Montage,
Composer = require("ui/composer/composer").Composer,
defaultEventManager = require("core/event/event-manager").defaultEventManager;
/**
@class module:montage/ui/composer/translate-composer.TranslateComposer
@extends module:montage/ui/composer/composer.Composer
*/
var TranslateComposer = exports.TranslateComposer = Montage.create(Composer,/** @lends module:montage/ui/event/composer/translate-composer.TranslateComposer# */ {
/**
These elements perform some native action when clicked/touched and so we
should not preventDefault when a mousedown/touchstart happens on them.
@private
*/
_NATIVE_ELEMENTS: {
value: ["A", "IFRAME", "EMBED", "OBJECT", "VIDEO", "AUDIO", "CANVAS",
"LABEL", "INPUT", "BUTTON", "SELECT", "TEXTAREA", "KEYGEN",
"DETAILS", "COMMAND"
]
},
_externalUpdate: {
enumerable: false,
value: true
},
isAnimating: {
enumerable: false,
value: false
},
frame: {
value: function(timestamp) {
if (this.isAnimating) {
this._animationInterval();
}
this._externalUpdate = false;
}
},
_pointerSpeedMultiplier: {
enumerable: false,
value: 1
},
pointerSpeedMultiplier: {
get: function () {
return this._pointerSpeedMultiplier;
},
set: function (value) {
this._pointerSpeedMultiplier = value;
}
},
pointerStartEventPosition: {
value: null
},
_shouldDispatchTranslate: {
value: false,
enumerable: false
},
_isSelfUpdate: {
enumerable: false,
value: false
},
_translateX: {
enumerable: false,
value: 0
},
translateX: {
get: function () {
return this._translateX;
},
set: function (value) {
if (this._axis==="vertical") {
this._translateX=0;
} else {
var tmp=isNaN(value)?0:value>>0;
if ((!this._hasBouncing)||(!this._isSelfUpdate)) {
if (tmp<0) {
tmp=0;
}
if (tmp>this._maxTranslateX) {
tmp=this._maxTranslateX;
}
if (!this._isSelfUpdate) {
this.isAnimating = false;
}
}
this._translateX=tmp;
}
}
},
_translateY: {
enumerable: false,
value: 0
},
translateY: {
get: function () {
return this._translateY;
},
set: function (value) {
if (this._axis==="horizontal") {
this._translateY=0;
} else {
var tmp=isNaN(value)?0:value>>0;
if ((!this._hasBouncing)||(!this._isSelfUpdate)) {
if (tmp<0) {
tmp=0;
}
if (tmp>this._maxTranslateY) {
tmp=this._maxTranslateY;
}
if (!this._isSelfUpdate) {
this.isAnimating = false;
}
}
this._translateY=tmp;
}
}
},
_maxTranslateX: {
enumerable: false,
value: 0
},
maxTranslateX: {
get: function () {
return this._maxTranslateX;
},
set: function (value) {
var tmp=isNaN(value)?0:value>>0;
if (tmp<0) {
tmp=0;
}
if (this._maxTranslateX!=tmp) {
if (this._translateX>this._maxTranslateX) {
this.translateX=this._maxTranslateX;
}
this._maxTranslateX=tmp;
}
}
},
_maxTranslateY: {
enumerable: false,
value: 0
},
maxTranslateY: {
get: function () {
return this._maxTranslateY;
},
set: function (value) {
var tmp=isNaN(value)?0:value>>0;
if (tmp<0) {
tmp=0;
}
if (this._maxTranslateY!=tmp) {
if (this._translateY>this._maxTranslateY) {
this.translateY=this._maxTranslateY;
}
this._maxTranslateY=tmp;
}
}
},
_axis: {
enumerable: false,
value: "both"
},
axis: {
get: function () {
return this._axis;
},
set: function (value) {
switch (value) {
case "vertical":
case "horizontal":
this._axis=value;
break;
default:
this._axis="both";
break;
}
}
},
_invertAxis: {
value: false,
enumerable: false
},
invertAxis: {
get: function() {
return this._invertAxis;
},
set: function(value) {
this._invertAxis=value?true:false;
}
},
_hasMomentum: {
enumerable: false,
value: true
},
hasMomentum: {
get: function () {
return this._hasMomentum;
},
set: function (value) {
this._hasMomentum=value?true:false;
}
},
_hasBouncing: {
enumerable: false,
value: true
},
hasBouncing: {
get: function () {
return this._hasBouncing;
},
set: function (value) {
this._hasBouncing=value?true:false;
}
},
__momentumDuration: {
enumerable: false,
value: 650
},
_momentumDuration: {
get: function () {
return this.__momentumDuration;
},
set: function (value) {
this.__momentumDuration=isNaN(value)?1:value>>0;
if (this.__momentumDuration<1) this.__momentumDuration=1;
},
enumerable: false
},
__bouncingDuration: {
enumerable: false,
value: 750
},
_bouncingDuration: {
get: function () {
return this.__bouncingDuration;
},
set: function (value) {
this.__bouncingDuration=isNaN(value)?1:value>>0;
if (this.__bouncingDuration<1) this.__bouncingDuration=1;
}
},
_pointerX: {
enumerable: false,
value: null
},
_pointerY: {
enumerable: false,
value: null
},
_touchIdentifier: {
enumerable: false,
value: null
},
_isFirstMove: {
enumerable: false,
value: false
},
_start: {
enumerable: false,
value: function (x, y, target) {
this.pointerStartEventPosition = {
pageX: x,
pageY: y,
target: target
};
this._pointerX=x;
this._pointerY=y;
if (window.Touch) {
document.addEventListener("touchend", this, true);
document.addEventListener("touchmove", this, true);
} else {
document.addEventListener("mouseup", this, true);
document.addEventListener("mousemove", this, true);
}
this.isAnimating = false;
this._isFirstMove = true;
}
},
_observedPointer: {
enumerable: false,
value: null
},
/**
Returns if we should preventDefault on a touchstart/mousedown event.
@param {Event} The event
@returns {Boolean} Whether preventDefault should be called
@private
*/
_shouldPreventDefault: {
value: function(event) {
return !!event.target.tagName && TranslateComposer._NATIVE_ELEMENTS.indexOf(event.target.tagName) === -1 && !event.target.isContentEditable;
}
},
/**
Description TODO
@function
@param {Event} event TODO
*/
captureMousedown: {
enumerable: false,
value: function (event) {
if (event.button !== 0) {
return;
}
if (this._shouldPreventDefault(event)) {
event.preventDefault();
}
// Register some interest in the mouse pointer internally, we may end up claiming it but let's see if
// anybody else cares first
this._observedPointer = "mouse";
this._start(event.clientX, event.clientY, event.target);
}
},
/**
Handle the mousedown that bubbled back up from beneath the element
If nobody else claimed this pointer, we should handle it now
@function
@param {Event} event TODO
*/
handleMousedown: {
enumerable: false,
value: function (event) {
if (event.button === 0 && !this.eventManager.componentClaimingPointer(this._observedPointer, this)) {
this.eventManager.claimPointer(this._observedPointer, this);
this._start(event.clientX, event.clientY, event.target);
}
}
},
captureMousemove: {
enumerable: false,
value: function (event) {
if (this.eventManager.isPointerClaimedByComponent(this._observedPointer, this)) {
event.preventDefault();
this._move(event.clientX, event.clientY);
} else {
this._analyzeMovement(event.velocity);
}
}
},
captureMouseup: {
enumerable: false,
value: function (event) {
this._end(event);
}
},
_releaseInterest: { // unload??
value: function() {
if (window.Touch) {
document.removeEventListener("touchend", this, true);
document.removeEventListener("touchmove", this, true);
} else {
document.removeEventListener("mouseup", this, true);
document.removeEventListener("mousemove", this, true);
}
if (this.eventManager.isPointerClaimedByComponent(this._observedPointer, this)) {
this.eventManager.forfeitPointer(this._observedPointer, this);
}
this._observedPointer = null;
}
},
captureTouchstart: {
enumerable: false,
value: function (event) {
if (this._shouldPreventDefault(event)) {
event.preventDefault();
}
// If already scrolling, ignore any new touchstarts
if (this._observedPointer !== null && this.eventManager.isPointerClaimedByComponent(this._observedPointer, this)) {
return;
}
if (event.targetTouches.length === 1) {
this._observedPointer = event.targetTouches[0].identifier;
this._start(event.targetTouches[0].clientX, event.targetTouches[0].clientY, event.targetTouches[0].target);
}
}
},
handleTouchstart: {
value: function(event) {
if (!this.eventManager.componentClaimingPointer(this._observedPointer)) {
if (event.targetTouches.length === 1) {
if (this._shouldPreventDefault(event)) {
event.preventDefault();
}
this.eventManager.claimPointer(this._observedPointer, this);
this._start(event.targetTouches[0].clientX, event.targetTouches[0].clientY, event.targetTouches[0].target);
}
}
}
},
captureTouchmove: {
enumerable: false,
value: function (event) {
var i = 0, len = event.changedTouches.length;
while (i < len && event.changedTouches[i].identifier !== this._observedPointer) {
i++;
}
if (i < len) {
if (this.eventManager.isPointerClaimedByComponent(this._observedPointer, this)) {
event.preventDefault();
this._move(event.changedTouches[i].clientX, event.changedTouches[i].clientY);
} else {
this._analyzeMovement(event.changedTouches[i].velocity);
}
}
}
},
captureTouchend: {
enumerable: false,
value: function (event) {
var i = 0, len = event.changedTouches.length;
while (i < len && !this.eventManager.isPointerClaimedByComponent(event.changedTouches[i].identifier, this)) {
i++;
}
if (i < len) {
this._end(event.changedTouches[i]);
}
}
},
_analyzeMovement: {
value: function(velocity) {
if (!velocity) {
return;
}
var lowerRight = 0.7853981633974483, // pi/4
lowerLeft = 2.356194490192345, // 3pi/4
upperLeft = -2.356194490192345, // 5pi/4
upperRight = -0.7853981633974483, // 7pi/4
isUp, isDown, isRight, isLeft,
angle,
speed;
speed = velocity.speed;
if (0 === velocity.speed || isNaN(velocity.speed)) {
// If there's no speed there's not much we can infer about direction; stop
return;
}
angle = velocity.angle;
// The motion is with the grain of the element; we may want to see if we should claim the pointer
if ("horizontal" === this.axis) {
isRight = (angle <= lowerRight && angle >= upperRight);
isLeft = (angle >= lowerLeft || angle <= upperLeft);
if (isRight || isLeft) {
this._stealPointer();
}
} else if ("vertical" === this.axis) {
isUp = (angle <= upperRight && angle >= upperLeft);
isDown = (angle >= lowerRight && angle <= lowerLeft);
if (isUp || isDown) {
this._stealPointer();
}
} else if (speed >= 500) {
// TODO not hardcode this threshold speed
this._stealPointer();
}
}
},
_stealPointer: {
value: function() {
this.eventManager.claimPointer(this._observedPointer, this);
}
},
_translateEndTimeout: {
enumerable: false,
value: null
},
handleMousewheel: {
enumerable: false,
value: function (event) {
var self = this;
var oldTranslateY = this._translateY;
this.translateY = this._translateY - (event.wheelDeltaY * 20) / 120;
this._dispatchTranslateStart();
window.clearTimeout(this._translateEndTimeout);
this._translateEndTimeout = window.setTimeout(function () {
self._dispatchTranslateEnd();
}, 400);
// If we're not at one of the extremes (i.e. the scroll actully
// changed the translate) then we want to preventDefault to stop
// the page scrolling.
if (oldTranslateY !== this._translateY) {
event.preventDefault();
}
}
},
_move: {
enumerable: false,
value: function (x, y) {
var pointerDelta;
this._isSelfUpdate=true;
if (this._axis!="vertical") {
pointerDelta = this._invertAxis ? (this._pointerX-x) : (x-this._pointerX);
if ((this._translateX<0)||(this._translateX>this._maxTranslateX)) {
this.translateX+=((pointerDelta)/2)*this._pointerSpeedMultiplier;
} else {
this.translateX+=(pointerDelta)*this._pointerSpeedMultiplier;
}
}
if (this._axis!="horizontal") {
pointerDelta = this._invertAxis ? (this._pointerY-y) : (y-this._pointerY);
if ((this._translateY<0)||(this._translateY>this._maxTranslateY)) {
this.translateY+=((pointerDelta)/2)*this._pointerSpeedMultiplier;
} else {
this.translateY+=(pointerDelta)*this._pointerSpeedMultiplier;
}
}
this._isSelfUpdate=false;
this._pointerX=x;
this._pointerY=y;
if (this._isFirstMove) {
this._dispatchTranslateStart();
this._isFirstMove = false;
}
if (this._shouldDispatchTranslate) {
this._dispatchTranslate();
}
}
},
_animationInterval: {
enumerable: false,
value: false
},
_bezierTValue: {
enumerable: false,
value: function (x, p1x, p1y, p2x, p2y) {
var a=1-3*p2x+3*p1x,
b=3*p2x-6*p1x,
c=3*p1x,
t=.5,
der,
i, k, tmp;
for (i=0; i<10; i++) {
tmp=t*t;
der=3*a*tmp+2*b*t+c;
k=1-t;
t-=((3*(k*k*t*p1x+k*tmp*p2x)+tmp*t-x)/der); // der==0
}
tmp=t*t;
k=1-t;
return 3*(k*k*t*p1y+k*tmp*p2y)+tmp*t;
}
},
_dispatchTranslateStart: {
enumerable: false,
value: function () {
var translateStartEvent = document.createEvent("CustomEvent");
translateStartEvent.initCustomEvent("translateStart", true, true, null);
this.dispatchEvent(translateStartEvent);
}
},
_dispatchTranslateEnd: {
enumerable: false,
value: function () {
var translateEndEvent = document.createEvent("CustomEvent");
translateEndEvent.initCustomEvent("translateEnd", true, true, null);
this.dispatchEvent(translateEndEvent);
}
},
_dispatchTranslate: {
enumerable: false,
value: function() {
var translateEvent = document.createEvent("CustomEvent");
translateEvent.initCustomEvent("translate", true, true, null);
translateEvent.translateX = this._translateX;
translateEvent.translateY = this._translateY;
this.dispatchEvent(translateEvent);
}
},
_end: {
enumerable: false,
value: function (event) {
var animateBouncingX=false,
animateBouncingY=false,
animateMomentum=false,
momentumX,
momentumY,
startX=this._translateX,
startY,
posX=startX,
posY,
endX=startX,
endY,
self=this,
startTimeBounceX=false,
startTimeBounceY=false,
startTime=Date.now();
startY=this._translateY;
posY=startY;
endY=startY;
if ((this._hasMomentum)&&(event.velocity.speed>40)) {
if (this._axis!="vertical") {
momentumX=event.velocity.x*this._pointerSpeedMultiplier;
} else {
momentumX=0;
}
if (this._axis!="horizontal") {
momentumY=event.velocity.y*this._pointerSpeedMultiplier;
} else {
momentumY=0;
}
endX=startX-(momentumX*this.__momentumDuration/2000);
endY=startY-(momentumY*this.__momentumDuration/2000);
animateMomentum=true;
}
this._animationInterval=function () {
var time=Date.now(), t, tmpX, tmpY;
if (animateMomentum) {
t=time-startTime;
if (tself.__bouncingDuration) {
t=self.__bouncingDuration;
}
tmpX=tmpX*(1-self._bezierTValue(t/self.__bouncingDuration, .17, .93, .19, 1));
} else {
tmpX=0;
animateBouncingX=false;
}
} else {
animateBouncingX=false;
}
}
if (endY<0) {
if (tmpY<0) {
if (!startTimeBounceY) {
animateBouncingY=true;
startTimeBounceY=time;
}
t=time-startTimeBounceY;
if ((tself.__bouncingDuration) {
t=self.__bouncingDuration;
}
tmpY=tmpY*(1-self._bezierTValue(t/self.__bouncingDuration, .17, .93, .19, 1));
} else {
tmpY=0;
animateBouncingY=false;
}
} else {
animateBouncingY=false;
}
}
if (endX>self._maxTranslateX) {
if (tmpX>self._maxTranslateX) {
if (!startTimeBounceX) {
animateBouncingX=true;
startTimeBounceX=time;
}
t=time-startTimeBounceX;
if ((tself.__bouncingDuration) {
t=self.__bouncingDuration;
}
tmpX=self._maxTranslateX+(tmpX-self._maxTranslateX)*(1-self._bezierTValue(t/self.__bouncingDuration, .17, .93, .19, 1));
} else {
tmpX=self._maxTranslateX;
animateBouncingX=false;
}
} else {
animateBouncingX=false;
}
}
if (endY>self._maxTranslateY) {
if (tmpY>self._maxTranslateY) {
if (!startTimeBounceY) {
animateBouncingY=true;
startTimeBounceY=time;
}
t=time-startTimeBounceY;
if ((tself.__bouncingDuration) {
t=self.__bouncingDuration;
}
tmpY=self._maxTranslateY+(tmpY-self._maxTranslateY)*(1-self._bezierTValue(t/self.__bouncingDuration, .17, .93, .19, 1));
} else {
tmpY=self._maxTranslateY;
animateBouncingY=false;
}
} else {
animateBouncingY=false;
}
}
}
self._isSelfUpdate=true;
self.translateX=tmpX;
self.translateY=tmpY;
self._isSelfUpdate=false;
self.isAnimating = animateMomentum||animateBouncingX||animateBouncingY;
if (self.isAnimating) {
self.needsFrame=true;
} else {
this._dispatchTranslateEnd();
}
};
this._animationInterval();
this._releaseInterest();
}
},
surrenderPointer: {
value: function(pointer, demandingComponent) {
return true;
}
},
eventManager: {
get: function() {
return defaultEventManager;
}
},
load: {
value: function() {
if (window.Touch) {
this._element.addEventListener("touchstart", this, true);
this._element.addEventListener("touchstart", this, false);
} else {
this._element.addEventListener("mousedown", this, true);
this._element.addEventListener("mousedown", this, false);
this._element.addEventListener("mousewheel", this, false);
}
this.eventManager.isStoringPointerEvents = true;
}
},
addEventListener: {
value: function(type, listener, useCapture) {
Composer.addEventListener.call(this, type, listener, useCapture);
if (type === "translate") {
this._shouldDispatchTranslate = true;
}
}
}
});