|
/* See LICENSE for terms of usage */
(function() {
// Number of pixels finger must move to determine horizontal or vertical motion
var kLockThreshold = 10;
// Factor which reduces the length of motion by each move of the finger
var kTouchMultiplier = 1;
// Maximum velocity for motion after user releases finger
var kMaxVelocity = 720 / (window.devicePixelRatio||1);
// Rate of deceleration after user releases finger
var kDecelRate = 350;
// Percentage of the page which content can be overscrolled before it must bounce back
var kBounceLimit = 0.5;
// Rate of deceleration when content has overscrolled and is slowing down before bouncing back
var kBounceDecelRate = 600;
// Duration of animation when bouncing back
var kBounceTime = 90;
// Percentage of viewport which must be scrolled past in order to snap to the next page
var kPageLimit = 0.3;
// Velocity at which the animation will advance to the next page
var kPageEscapeVelocity = 50;
// Vertical margin of scrollbar
var kScrollbarMargin = 1;
// Time to scroll to top
var kScrollToTopTime = 200;
var isWebkit = "webkitTransform" in document.documentElement.style;
var isFirefox = "MozTransform" in document.documentElement.style;
var isTouch = "ontouchstart" in window;
// ===============================================================================================
var startX, startY, touchX, touchY, touchDown, touchMoved, justChangedOrientation;
var animationInterval = 0;
var touchTargets = [];
var scrollers = {
'horizontal': createXTarget,
'vertical': createYTarget
};
window.scrollability = {
globalScrolling: false,
scrollers: scrollers,
flashIndicators: function() {
var scrollables = document.querySelectorAll('.scrollable.vertical');
for (var i = 0; i < scrollables.length; ++i) {
scrollability.scrollTo(scrollables[i], 0, 0, 20, true);
}
},
scrollToTop: function() {
var scrollables = document.getElementsByClassName('scrollable');
if (scrollables.length) {
var scrollable = scrollables[0];
if (scrollable.className.indexOf('vertical') != -1) {
scrollability.scrollTo(scrollable, 0, 0, kScrollToTopTime);
}
}
},
scrollTo: function(element, x, y, animationTime, muteDelegate) {
stopAnimation();
var target = createTargetForElement(element);
if (target) {
if (muteDelegate) {
target.delegate = null;
}
target = wrapTarget(target);
touchTargets = [target];
touchMoved = true;
if (animationTime) {
var orig = element[target.key];
var dest = target.filter(x, y);
var dir = dest - orig;
var startTime = new Date().getTime();
animationInterval = setInterval(function() {
var d = new Date().getTime() - startTime;
var pos = orig + ((dest-orig) * (d/animationTime));
if ((dir < 0 && pos < dest) || (dir > 0 && pos > dest)) {
pos = dest;
}
target.updater(pos);
if (pos == dest) {
clearInterval(animationInterval);
setTimeout(stopAnimation, 200);
}
}, 20);
} else {
target.updater(y);
stopAnimation();
}
}
}
};
function onLoad() {
scrollability.flashIndicators();
}
function onScroll(event) {
setTimeout(function() {
if (justChangedOrientation) {
justChangedOrientation = false;
} else if (isTouch) {
scrollability.scrollToTop();
}
});
}
function onOrientationChange(event) {
justChangedOrientation = true;
}
function onTouchStart(event) {
stopAnimation();
var touchCandidate = event.target;
var touch = event.touches[0];
var touched = null;
var startTime = new Date().getTime();
touchX = startX = touch.clientX;
touchY = startY = touch.clientY;
touchDown = true;
touchMoved = false;
touchTargets = getTouchTargets(event.target, touchX, touchY, startTime);
if (!touchTargets.length && !scrollability.globalScrolling) {
return true;
}
var holdTimeout = setTimeout(function() {
holdTimeout = 0;
touched = setTouched(touchCandidate);
}, 50);
var d = document;
d.addEventListener('touchmove', onTouchMove, false);
d.addEventListener('touchend', onTouchEnd, false);
animationInterval = setInterval(touchAnimation, 0);
function onTouchMove(event) {
event.preventDefault();
touchMoved = true;
if (holdTimeout) {
clearTimeout(holdTimeout);
holdTimeout = 0;
}
if (touched) {
releaseTouched(touched);
touched = null;
}
var touch = event.touches[0];
touchX = touch.clientX;
touchY = touch.clientY;
// Reduce the candidates down to the one whose axis follows the finger most closely
if (touchTargets.length > 1) {
for (var i = 0; i < touchTargets.length; ++i) {
var target = touchTargets[i];
if (target.disable && target.disable(touchX, touchY, startX, startY)) {
target.terminator();
touchTargets.splice(i, 1);
break;
}
}
}
}
function onTouchEnd(event) {
if (holdTimeout) {
clearTimeout(holdTimeout);
holdTimeout = 0;
}
// Simulate a click event when releasing the finger
if (touched) {
var evt = document.createEvent('MouseEvents');
evt.initMouseEvent('click', true, true, window, 1);
touched[0].dispatchEvent(evt);
releaseTouched(touched);
}
d.removeEventListener('touchmove', onTouchMove, false);
d.removeEventListener('touchend', onTouchEnd, false);
touchDown = false;
}
}
function wrapTarget(target, startX, startY, startTime) {
var delegate = target.delegate;
var constrained = target.constrained;
var paginated = target.paginated;
var viewport = target.viewport || 0;
var scrollbar = target.scrollbar;
var position = target.node[target.key];
var min = target.min;
var max = target.max;
var absMin = min;
var absMax = Math.round(max/viewport)*viewport;
var pageSpacing = 0;
var velocity = 0;
var decelerating = 0;
var decelOrigin, decelDelta;
var bounceLimit = target.bounce;
var pageLimit = viewport * kPageLimit;
var lastTouch = startTouch = target.filter(startX, startY);
var lastTime = startTime;
var stillTime = 0;
var stillThreshold = 20;
var snapped = false;
var locked = false;
if (paginated) {
var excess = Math.round(Math.abs(absMin) % viewport);
var pageCount = ((Math.abs(absMin)-excess) / viewport)+1;
var pageSpacing = excess / pageCount;
var positionSpacing = Math.round(position) % viewport;
var pagePosition = Math.round((position-positionSpacing)/viewport) * viewport;
min = max = Math.round(pagePosition + absMax)+positionSpacing;
absMin += pageSpacing;
}
if (delegate && delegate.onStartScroll) {
if (!delegate.onStartScroll()) {
return null;
}
}
if (scrollbar) {
target.node.parentNode.appendChild(scrollbar);
}
function animator(touch, time) {
var deltaTime = 1 / (time - lastTime);
lastTime = time;
var continues = true;
if (touchDown) {
var delta = (touch - lastTouch) * kTouchMultiplier;
if (!delta) {
// Heuristics to prevent out delta=0 changes from making velocity=0 and
// stopping all motion in its tracks. We need to distinguish when the finger
// has actually stopped moving from when the timer fired too quickly.
if (!stillTime) {
stillTime = time;
}
if (time - stillTime < stillThreshold) {
return true;
}
} else {
stillTime = 0;
}
if (!locked && Math.abs(touch - startTouch) > kLockThreshold) {
locked = true;
if (delegate && delegate.onLockScroll) {
delegate.onLockScroll(target.key);
}
}
lastTouch = touch;
velocity = delta / deltaTime;
// Apply resistance along the edges
if (position > max && constrained) {
var excess = position - max;
velocity *= (1.0 - excess / bounceLimit);
} else if (position < min && constrained) {
var excess = min - position;
velocity *= (1.0 - excess / bounceLimit);
}
} else {
if (paginated && !snapped) {
// When finger is released, decide whether to jump to next/previous page
// or to snap back to the current page
snapped = true;
if (Math.abs(position - max) > pageLimit || Math.abs(velocity) > kPageEscapeVelocity) {
if (position > max) {
if (max != absMax) {
max += viewport+pageSpacing;
min += viewport+pageSpacing;
if (delegate && delegate.onScrollPage) {
var totalSpacing = min % viewport;
var page = -Math.round((position+viewport-totalSpacing)/viewport);
delegate.onScrollPage(page, -1);
}
}
} else {
if (min != absMin) {
max -= viewport+pageSpacing;
min -= viewport+pageSpacing;
if (delegate && delegate.onScrollPage) {
var totalSpacing = min % viewport;
var page = -Math.round((position-viewport-totalSpacing)/viewport);
delegate.onScrollPage(page, 1);
}
}
}
}
}
if (position > max && constrained) {
if (velocity > 0) {
// Slowing down
var excess = position - max;
var elasticity = (1.0 - excess / bounceLimit);
velocity = Math.max(velocity - kBounceDecelRate * deltaTime, 0) * elasticity;
decelerating = 0;
} else {
// Bouncing back
if (!decelerating) {
decelOrigin = position;
decelDelta = max - position;
}
position = easeOutExpo(decelerating, decelOrigin, decelDelta, kBounceTime);
return update(position, ++decelerating <= kBounceTime && Math.floor(position) > max);
}
} else if (position < min && constrained) {
if (velocity < 0) {
// Slowing down
var excess = min - position;
var elasticity = (1.0 - excess / bounceLimit);
velocity = Math.min(velocity + kBounceDecelRate * deltaTime, 0) * elasticity;
decelerating = 0;
} else {
// Bouncing back
if (!decelerating) {
decelOrigin = position;
decelDelta = min - position;
}
position = easeOutExpo(decelerating, decelOrigin, decelDelta, kBounceTime);
return update(position, ++decelerating <= kBounceTime && Math.ceil(position) < min);
}
} else {
// Slowing down
if (!decelerating) {
if (velocity < 0 && velocity < -kMaxVelocity) {
velocity = -kMaxVelocity;
} else if (velocity > 0 && velocity > kMaxVelocity) {
velocity = kMaxVelocity;
}
decelOrigin = velocity;
}
velocity = easeOutExpo(decelerating, decelOrigin, -decelOrigin, kDecelRate);
if (++decelerating > kDecelRate || Math.floor(velocity) == 0) {
continues = false;
}
}
}
position += velocity * deltaTime;
return update(position, continues);
}
function update(pos, continues) {
position = pos;
target.node[target.key] = position;
target.update(target.node, position);
if (delegate && delegate.onScroll) {
delegate.onScroll(position);
}
// Update the scrollbar
var range = -min - max;
if (scrollbar && viewport < range) {
var viewable = viewport - kScrollbarMargin*2;
var height = (viewable/range) * viewable;
var scrollPosition = 0;
if (position > max) {
height = Math.max(height - (position-max), 7);
scrollPosition = 0;
} else if (position < min) {
height = Math.max(height - (min - position), 7);
scrollPosition = (viewable-height);
} else {
scrollPosition = Math.round((Math.abs(position) / range) * (viewable-height));
}
scrollPosition += kScrollbarMargin;
scrollbar.style.height = Math.round(height) + 'px';
moveElement(scrollbar, 0, Math.round(scrollPosition));
if (touchMoved) {
scrollbar.style.webkitTransition = 'none';
scrollbar.style.opacity = '1';
}
}
return continues;
}
function terminator() {
// Snap to the integer endpoint, since position may be a subpixel value while animating
if (paginated) {
var pageIndex = Math.round(position/viewport);
update(pageIndex * (viewport+pageSpacing));
} else if (position > max && constrained) {
update(max);
} else if (position < min && constrained) {
update(min);
}
// Hide the scrollbar
if (scrollbar) {
scrollbar.style.opacity = '0';
scrollbar.style.webkitTransition = 'opacity 0.33s linear';
}
if (delegate && delegate.onEndScroll) {
delegate.onEndScroll();
}
}
target.updater = update;
target.animator = animator;
target.terminator = terminator;
return target;
}
function touchAnimation() {
var time = new Date().getTime();
// Animate each of the targets
for (var i = 0; i < touchTargets.length; ++i) {
var target = touchTargets[i];
// Translate the x/y touch into the value needed by each of the targets
var touch = target.filter(touchX, touchY);
if (!target.animator(touch, time)) {
target.terminator();
touchTargets.splice(i--, 1);
}
}
if (!touchTargets.length) {
stopAnimation();
}
}
// *************************************************************************************************
function getTouchTargets(node, touchX, touchY, startTime) {
var targets = [];
findTargets(node, targets, touchX, touchY, startTime);
var candidates = document.querySelectorAll('.scrollable.global');
for (var j = 0; j < candidates.length; ++j) {
findTargets(candidates[j], targets, touchX, touchY, startTime);
}
return targets;
}
function findTargets(element, targets, touchX, touchY, startTime) {
while (element) {
if (element.nodeType == 1) {
var target = createTargetForElement(element, touchX, touchY, startTime);
if (target) {
// Look out for duplicates
var exists = false;
for (var j = 0; j < targets.length; ++j) {
if (targets[j].node == element) {
exists = true;
break;
}
}
if (!exists) {
target = wrapTarget(target, touchX, touchY, startTime);
if (target) {
targets.push(target);
}
}
}
}
element = element.parentNode;
}
}
function createTargetForElement(element, touchX, touchY, startTime) {
var classes = element.className.split(' ');
for (var i = 0; i < classes.length; ++i) {
var name = classes[i];
if (scrollers[name]) {
var target = scrollers[name](element);
target.key = 'scrollable_'+name;
target.paginated = classes.indexOf('paginated') != -1;
if (!(target.key in element)) {
element[target.key] = target.initial ? target.initial(element) : 0;
}
return target;
}
}
}
function setTouched(target) {
var touched = [];
for (var n = target; n; n = n.parentNode) {
if (n.nodeType == 1) {
n.className = (n.className ? n.className + ' ' : '') + 'touched';
touched.push(n);
}
}
return touched;
}
function releaseTouched(touched) {
for (var i = 0; i < touched.length; ++i) {
var n = touched[i];
n.className = n.className.replace('touched', '');
}
}
function stopAnimation() {
if (animationInterval) {
clearInterval(animationInterval);
animationInterval = 0;
for (var i = 0; i < touchTargets.length; ++i) {
var target = touchTargets[i];
target.terminator();
}
touchTargets = [];
}
}
function moveElement(element, x, y) {
if (isWebkit) {
element.style.webkitTransform = 'translate3d('
+(x ? (x+'px') : '0')+','
+(y ? (y+'px') : '0')+','
+'0)';
} else if (isFirefox) {
element.style.MozTransform = 'translate('
+(x ? (x+'px') : '0')+','
+(y ? (y+'px') : '0')+')';
}
}
function initScrollbar(element) {
if (!element.scrollableScrollbar) {
var scrollbar = element.scrollableScrollbar = document.createElement('div');
scrollbar.className = 'scrollableScrollbar';
// We hardcode this CSS here to avoid having to provide a CSS file
scrollbar.style.cssText = [
'position: absolute',
'top: 0',
'right: 1px',
'width: 7px',
'min-height: 7px',
'opacity: 0',
'-webkit-transform: translate3d(0,0,0)',
'-webkit-box-sizing: border-box',
'-webkit-border-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAUhJREFUeNp0Ur1OwzAQtt1CaZQQgUjDhuicrEwoqjJlzpBAXoIHywtkcwfECyQPwIgKQkoyFJWq5k6cJcsUS5/sO993/1wpxazjAU4BJyR/A3aA0TSaGu85kbSO0y0AM/pH8lYr8ZwBLpBUluVtGIaPjuM8IYIgeEAdObwkB4xTqgv8iOP4vuu6lZEFRkUDHkWRbNv2mVJ/x4g+1pPn+RJICRlzk4Q3/lVVdUP1nwtqgpJSYqQJGbMj96RpmhXJM01kwzBcWU2x36zv+wXppro5TAihvat/HCjxa6R0V7FY5rruhx3BTtfzvDeS95rI0zSVcB+MpijL0SHLsjW9d3ocIRZvjINbKSsYx5rGsQdsNHFOC8CKolhCh+/GcbxG2ff9TZIkL3Vdv5KjT8AXN3b12MqZi4yRBiTZu7olmEvOacH/LPmPAAMA2bZzzeYUC40AAAAASUVORK5CYII=") 6 2 6 2 / 3px 1px 3px 1px round round',
'z-index: 2147483647',
].join(';');
}
return element.scrollableScrollbar;
}
function easeOutExpo(t, b, c, d) {
return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
}
// *************************************************************************************************
function createXTarget(element) {
var parent = element.parentNode;
return {
node: element,
min: -parent.scrollWidth + parent.offsetWidth,
max: 0,
viewport: parent.offsetWidth,
bounce: parent.offsetWidth * kBounceLimit,
constrained: true,
delegate: element.scrollDelegate,
filter: function(x, y) {
return x;
},
disable: function (x, y, startX, startY) {
var dx = Math.abs(x - startX);
var dy = Math.abs(y - startY);
if (dy > dx && dy > kLockThreshold) {
return true;
}
},
update: function(element, position) {
moveElement(element, position, element.scrollable_vertical||0);
}
};
}
function createYTarget(element) {
var parent = element.parentNode;
return {
node: element,
scrollbar: initScrollbar(element),
min: -parent.scrollHeight + parent.offsetHeight,
max: 0,
viewport: parent.offsetHeight,
bounce: parent.offsetHeight * kBounceLimit,
constrained: true,
delegate: element.scrollDelegate,
filter: function(x, y) {
return y;
},
disable: function(x, y, startX, startY) {
var dx = Math.abs(x - startX);
var dy = Math.abs(y - startY);
if (dx > dy && dx > kLockThreshold) {
return true;
}
},
update: function(element, position) {
moveElement(element, element.scrollable_horizontal||0, position);
}
};
}
document.addEventListener('touchstart', onTouchStart, false);
document.addEventListener('scroll', onScroll, false);
document.addEventListener('orientationchange', onOrientationChange, false);
window.addEventListener('load', onLoad, false);
})();
|