/*
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.
*/
var Montage = require("montage").Montage,
defaultEventManager = require("core/event/event-manager").defaultEventManager;
var Scroll = exports.Scroll = Montage.create(Montage, {
_externalUpdate: {
enumerable: false,
value: true
},
isAnimating: {
enumerable: false,
value: false
},
component: {
value: {}
},
_needsDraw: {
enumerable: false,
value: false
},
needsDraw: {
get: function () {
return this._needsDraw;
},
set: function (value) {
this._needsDraw = value;
if (this.component) {
this.component.needsDraw = true;
}
}
},
draw: {
value: function () {
this._needsDraw = false;
if (this.isAnimating) {
this._animationInterval();
}
this._externalUpdate = false;
}
},
deserializedFromTemplate: {
value: function () {
var oldComponentDraw = this.component.draw,
self = this;
this.component.draw = function () {
self.draw();
oldComponentDraw.call(self.component);
};
}
},
_pointerSpeedMultiplier: {
enumerable: false,
value: 1
},
pointerSpeedMultiplier: {
get: function () {
return this._pointerSpeedMultiplier;
},
set: function (value) {
this._pointerSpeedMultiplier = value;
}
},
pointerStartEventPosition: {
value: null
},
_isSelfUpdate: {
enumerable: false,
value: false
},
_scrollX: {
enumerable: false,
value: 0
},
scrollX: {
get: function () {
return this._scrollX;
},
set: function (value) {
if (this._axis==="vertical") {
this._scrollX=0;
} else {
var tmp=isNaN(value)?0:value>>0;
if ((!this._hasBouncing)||(!this._isSelfUpdate)) {
if (tmp<0) {
tmp=0;
}
if (tmp>this._maxScrollX) {
tmp=this._maxScrollX;
}
if (!this._isSelfUpdate) {
this.isAnimating = false;
}
}
this._scrollX=tmp;
}
}
},
_scrollY: {
enumerable: false,
value: 0
},
scrollY: {
get: function () {
return this._scrollY;
},
set: function (value) {
if (this._axis==="horizontal") {
this._scrollY=0;
} else {
var tmp=isNaN(value)?0:value>>0;
if ((!this._hasBouncing)||(!this._isSelfUpdate)) {
if (tmp<0) {
tmp=0;
}
if (tmp>this._maxScrollY) {
tmp=this._maxScrollY;
}
if (!this._isSelfUpdate) {
this.isAnimating = false;
}
}
this._scrollY=tmp;
}
}
},
_maxScrollX: {
enumerable: false,
value: 0
},
maxScrollX: {
get: function () {
return this._maxScrollX;
},
set: function (value) {
var tmp=isNaN(value)?0:value>>0;
if (tmp<0) {
tmp=0;
}
if (this._maxScrollX!=tmp) {
if (this._scrollX>this._maxScrollX) {
this.scrollX=this._maxScrollX;
}
this._maxScrollX=tmp;
}
}
},
_maxScrollY: {
enumerable: false,
value: 0
},
maxScrollY: {
get: function () {
return this._maxScrollY;
},
set: function (value) {
var tmp=isNaN(value)?0:value>>0;
if (tmp<0) {
tmp=0;
}
if (this._maxScrollY!=tmp) {
if (this._scrollY>this._maxScrollY) {
this.scrollY=this._maxScrollY;
}
this._maxScrollY=tmp;
}
}
},
_element: {
enumerable: false,
value: null
},
element: {
get: function () {
return this._element;
},
set: function (element) {
if (this._element !== element) {
this._element = element;
this.prepareForActivationEvents();
}
}
},
_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;
}
}
},
_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;
}
},
_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
},
captureMousedown: {
enumerable: false,
value: function (event) {
// TODO this is a bit of a temporary workaround to ensure that we allow input fields
//to receive the mousedown that gives them focus and sets the cursor a the mousedown coordinates
if (!(event.target.tagName &&
("INPUT" === event.target.tagName || "SELECT" === event.target.tagName || "TEXTAREA" === event.target.tagName)) &&
!event.target.isContentEditable) {
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);
}
},
handleMousedown: {
enumerable: false,
value: function (event) {
if (!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: {
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) {
event.preventDefault();
// If already scrolling the scrollview, 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) {
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;
while (i < event.changedTouches.length && event.changedTouches[i].identifier !== this._observedPointer) {
i++;
}
if (i < event.changedTouches.length) {
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;
while (i < event.changedTouches.length && !this.eventManager.isPointerClaimedByComponent(event.changedTouches[i].identifier, this)) {
i++;
}
if (i < event.changedTouches.length) {
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 scrollview; 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);
}
},
_scrollEndTimeout: {
enumerable: false,
value: null
},
handleMousewheel: {
enumerable: false,
value: function (event) {
var self = this;
this.scrollY = this._scrollY - (event.wheelDeltaY * 20) / 120;
this._dispatchScrollStart();
window.clearTimeout(this._scrollEndTimeout);
this._scrollEndTimeout = window.setTimeout(function () {
self._dispatchScrollEnd();
}, 400);
event.preventDefault();
}
},
_move: {
enumerable: false,
value: function (x, y) {
var oldX=this._scrollX,
oldY=this._scrollY;
this._isSelfUpdate=true;
if (this._axis!="vertical") {
if ((this._scrollX<0)||(this._scrollX>this._maxScrollX)) {
this.scrollX+=((this._pointerX-x)/2)*this._pointerSpeedMultiplier;
} else {
this.scrollX+=(this._pointerX-x)*this._pointerSpeedMultiplier;
}
}
if (this._axis!="horizontal") {
if ((this._scrollY<0)||(this._scrollY>this._maxScrollY)) {
this.scrollY+=((this._pointerY-y)/2)*this._pointerSpeedMultiplier;
} else {
this.scrollY+=(this._pointerY-y)*this._pointerSpeedMultiplier;
}
}
this._isSelfUpdate=false;
this._pointerX=x;
this._pointerY=y;
if (this._isFirstMove) {
this._dispatchScrollStart();
this._isFirstMove = false;
}
}
},
_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;
}
},
_dispatchScrollStart: {
enumerable: false,
value: function () {
var scrollEndEvent = document.createEvent("CustomEvent");
scrollEndEvent.initCustomEvent("scrollStart", true, true, null);
scrollEndEvent.type = "scrollStart";
this.dispatchEvent(scrollEndEvent);
}
},
_dispatchScrollEnd: {
enumerable: false,
value: function () {
var scrollEndEvent = document.createEvent("CustomEvent");
scrollEndEvent.initCustomEvent("scrollEnd", true, true, null);
scrollEndEvent.type = "scrollEnd";
this.dispatchEvent(scrollEndEvent);
}
},
_end: {
enumerable: false,
value: function (event) {
var animateBouncingX=false,
animateBouncingY=false,
animateMomentum=false,
momentumX,
momentumY,
startX=this._scrollX,
startY,
posX=startX,
posY,
endX=startX,
endY,
self=this,
startTimeBounceX=false,
startTimeBounceY=false,
startTime=Date.now();
startY=this._scrollY;
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._maxScrollX) {
if (tmpX>self._maxScrollX) {
if (!startTimeBounceX) {
animateBouncingX=true;
startTimeBounceX=time;
}
t=time-startTimeBounceX;
if ((tself._bouncingDuration) {
t=self._bouncingDuration;
}
tmpX=self._maxScrollX+(tmpX-self._maxScrollX)*(1-self._bezierTValue(t/self._bouncingDuration, .17, .93, .19, 1));
} else {
tmpX=self._maxScrollX;
animateBouncingX=false;
}
} else {
animateBouncingX=false;
}
}
if (endY>self._maxScrollY) {
if (tmpY>self._maxScrollY) {
if (!startTimeBounceY) {
animateBouncingY=true;
startTimeBounceY=time;
}
t=time-startTimeBounceY;
if ((tself._bouncingDuration) {
t=self._bouncingDuration;
}
tmpY=self._maxScrollY+(tmpY-self._maxScrollY)*(1-self._bezierTValue(t/self._bouncingDuration, .17, .93, .19, 1));
} else {
tmpY=self._maxScrollY;
animateBouncingY=false;
}
} else {
animateBouncingY=false;
}
}
}
self._isSelfUpdate=true;
self.scrollX=tmpX;
self.scrollY=tmpY;
self._isSelfUpdate=false;
self.isAnimating = animateMomentum||animateBouncingX||animateBouncingY;
if (self.isAnimating) {
self.needsDraw=true;
} else {
this._dispatchScrollEnd();
}
};
this._animationInterval();
this._releaseInterest();
}
},
surrenderPointer: {
value: function(pointer, demandingComponent) {
return true;
}
},
eventManager: {
get: function() {
return defaultEventManager;
}
},
prepareForActivationEvents: {
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;
}
}
});