You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
534 lines
14 KiB
534 lines
14 KiB
/*!
|
|
* stroll.js 1.2 - CSS scroll effects
|
|
* http://lab.hakim.se/scroll-effects
|
|
* MIT licensed
|
|
*
|
|
* Copyright (C) 2012 Hakim El Hattab, http://hakim.se
|
|
*/
|
|
(function(){
|
|
|
|
"use strict";
|
|
|
|
// When a list is configured as 'live', this is how frequently
|
|
// the DOM will be polled for changes
|
|
var LIVE_INTERVAL = 500;
|
|
|
|
var IS_TOUCH_DEVICE = !!( 'ontouchstart' in window );
|
|
|
|
// All of the lists that are currently bound
|
|
var lists = [];
|
|
|
|
// Set to true when there are lists to refresh
|
|
var active = false;
|
|
|
|
/**
|
|
* Updates all currently bound lists.
|
|
*/
|
|
function refresh() {
|
|
if( active ) {
|
|
requestAnimFrame( refresh );
|
|
|
|
for( var i = 0, len = lists.length; i < len; i++ ) {
|
|
lists[i].update();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts monitoring a list and applies classes to each of
|
|
* its contained elements based on its position relative to
|
|
* the list's viewport.
|
|
*
|
|
* @param {HTMLElement} element
|
|
* @param {Object} options Additional arguments;
|
|
* - live; Flags if the DOM should be repeatedly checked for changes
|
|
* repeatedly. Useful if the list contents is changing. Use
|
|
* scarcely as it has an impact on performance.
|
|
*/
|
|
function add( element, options ) {
|
|
// Only allow ul/ol
|
|
if( !element.nodeName || /^(ul|ol)$/i.test( element.nodeName ) === false ) {
|
|
return false;
|
|
}
|
|
// Delete duplicates (but continue and re-bind this list to get the
|
|
// latest properties and list items)
|
|
else if( contains( element ) ) {
|
|
remove( element );
|
|
}
|
|
|
|
var list = IS_TOUCH_DEVICE ? new TouchList( element ) : new List( element );
|
|
|
|
// Handle options
|
|
if( options && options.live ) {
|
|
list.syncInterval = setInterval( function() {
|
|
list.sync.call( list );
|
|
}, LIVE_INTERVAL );
|
|
}
|
|
|
|
// Synchronize the list with the DOM
|
|
list.sync();
|
|
|
|
// Add this element to the collection
|
|
lists.push( list );
|
|
|
|
// Start refreshing if this was the first list to be added
|
|
if( lists.length === 1 ) {
|
|
active = true;
|
|
refresh();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stops monitoring a list element and removes any classes
|
|
* that were applied to its list items.
|
|
*
|
|
* @param {HTMLElement} element
|
|
*/
|
|
function remove( element ) {
|
|
for( var i = 0; i < lists.length; i++ ) {
|
|
var list = lists[i];
|
|
|
|
if( list.element == element ) {
|
|
list.destroy();
|
|
lists.splice( i, 1 );
|
|
i--;
|
|
}
|
|
}
|
|
|
|
// Stopped refreshing if the last list was removed
|
|
if( lists.length === 0 ) {
|
|
active = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the specified element has already been bound.
|
|
*/
|
|
function contains( element ) {
|
|
for( var i = 0, len = lists.length; i < len; i++ ) {
|
|
if( lists[i].element == element ) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Calls 'method' for each DOM element discovered in
|
|
* 'target'.
|
|
*
|
|
* @param target String selector / array of UL elements /
|
|
* jQuery object / single UL element
|
|
* @param method A function to call for each element target
|
|
*/
|
|
function batch( target, method, options ) {
|
|
var i, len;
|
|
|
|
// Selector
|
|
if( typeof target === 'string' ) {
|
|
var targets = document.querySelectorAll( target );
|
|
|
|
for( i = 0, len = targets.length; i < len; i++ ) {
|
|
method.call( null, targets[i], options );
|
|
}
|
|
}
|
|
// Array (jQuery)
|
|
else if( typeof target === 'object' && typeof target.length === 'number' ) {
|
|
for( i = 0, len = target.length; i < len; i++ ) {
|
|
method.call( null, target[i], options );
|
|
}
|
|
}
|
|
// Single element
|
|
else if( target.nodeName ) {
|
|
method.call( null, target, options );
|
|
}
|
|
else {
|
|
throw 'Stroll target was of unexpected type.';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the client is capable of running the library.
|
|
*/
|
|
function isCapable() {
|
|
return !!document.body.classList;
|
|
}
|
|
|
|
/**
|
|
* The basic type of list; applies past & future classes to
|
|
* list items based on scroll state.
|
|
*/
|
|
function List( element ) {
|
|
this.element = element;
|
|
}
|
|
|
|
/**
|
|
* Fetches the latest properties from the DOM to ensure that
|
|
* this list is in sync with its contents.
|
|
*/
|
|
List.prototype.sync = function() {
|
|
this.items = Array.prototype.slice.apply( this.element.children );
|
|
|
|
// Caching some heights so we don't need to go back to the DOM so much
|
|
this.listHeight = this.element.offsetHeight;
|
|
|
|
// One loop to get the offsets from the DOM
|
|
for( var i = 0, len = this.items.length; i < len; i++ ) {
|
|
var item = this.items[i];
|
|
item._offsetHeight = item.offsetHeight;
|
|
item._offsetTop = item.offsetTop;
|
|
item._offsetBottom = item._offsetTop + item._offsetHeight;
|
|
item._state = '';
|
|
}
|
|
|
|
// Force an update
|
|
this.update( true );
|
|
}
|
|
|
|
/**
|
|
* Apply past/future classes to list items outside of the viewport
|
|
*/
|
|
List.prototype.update = function( force ) {
|
|
var scrollTop = this.element.pageYOffset || this.element.scrollTop,
|
|
scrollBottom = scrollTop + this.listHeight;
|
|
|
|
// Quit if nothing changed
|
|
if( scrollTop !== this.lastTop || force ) {
|
|
this.lastTop = scrollTop;
|
|
|
|
// One loop to make our changes to the DOM
|
|
for( var i = 0, len = this.items.length; i < len; i++ ) {
|
|
var item = this.items[i];
|
|
|
|
// Above list viewport
|
|
if( item._offsetBottom < scrollTop ) {
|
|
// Exclusion via string matching improves performance
|
|
if( item._state !== 'past' ) {
|
|
item._state = 'past';
|
|
item.classList.add( 'past' );
|
|
item.classList.remove( 'future' );
|
|
}
|
|
}
|
|
// Below list viewport
|
|
else if( item._offsetTop > scrollBottom ) {
|
|
// Exclusion via string matching improves performance
|
|
if( item._state !== 'future' ) {
|
|
item._state = 'future';
|
|
item.classList.add( 'future' );
|
|
item.classList.remove( 'past' );
|
|
}
|
|
}
|
|
// Inside of list viewport
|
|
else if( item._state ) {
|
|
if( item._state === 'past' ) item.classList.remove( 'past' );
|
|
if( item._state === 'future' ) item.classList.remove( 'future' );
|
|
item._state = '';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleans up after this list and disposes of it.
|
|
*/
|
|
List.prototype.destroy = function() {
|
|
clearInterval( this.syncInterval );
|
|
|
|
for( var j = 0, len = this.items.length; j < len; j++ ) {
|
|
var item = this.items[j];
|
|
|
|
item.classList.remove( 'past' );
|
|
item.classList.remove( 'future' );
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* A list specifically for touch devices. Simulates the style
|
|
* of scrolling you'd see on a touch device but does not rely
|
|
* on webkit-overflow-scrolling since that makes it impossible
|
|
* to read the up-to-date scroll position.
|
|
*/
|
|
function TouchList( element ) {
|
|
this.element = element;
|
|
this.element.style.overflow = 'hidden';
|
|
|
|
this.top = {
|
|
value: 0,
|
|
natural: 0
|
|
};
|
|
|
|
this.touch = {
|
|
value: 0,
|
|
offset: 0,
|
|
start: 0,
|
|
previous: 0,
|
|
lastMove: Date.now(),
|
|
accellerateTimeout: -1,
|
|
isAccellerating: false,
|
|
isActive: false
|
|
};
|
|
|
|
this.velocity = 0;
|
|
}
|
|
TouchList.prototype = new List();
|
|
|
|
/**
|
|
* Fetches the latest properties from the DOM to ensure that
|
|
* this list is in sync with its contents. This is typically
|
|
* only used once (per list) at initialization.
|
|
*/
|
|
TouchList.prototype.sync = function() {
|
|
this.items = Array.prototype.slice.apply( this.element.children );
|
|
|
|
this.listHeight = this.element.offsetHeight;
|
|
|
|
var item;
|
|
|
|
// One loop to get the properties we need from the DOM
|
|
for( var i = 0, len = this.items.length; i < len; i++ ) {
|
|
item = this.items[i];
|
|
item._offsetHeight = item.offsetHeight;
|
|
item._offsetTop = item.offsetTop;
|
|
item._offsetBottom = item._offsetTop + item._offsetHeight;
|
|
item._state = '';
|
|
|
|
// Animating opacity is a MAJOR performance hit on mobile so we can't allow it
|
|
item.style.opacity = 1;
|
|
}
|
|
|
|
this.top.natural = this.element.scrollTop;
|
|
this.top.value = this.top.natural;
|
|
this.top.max = item._offsetBottom - this.listHeight;
|
|
|
|
// Force an update
|
|
this.update( true );
|
|
|
|
this.bind();
|
|
}
|
|
|
|
/**
|
|
* Binds the events for this list. References to proxy methods
|
|
* are kept for unbinding if the list is disposed of.
|
|
*/
|
|
TouchList.prototype.bind = function() {
|
|
var scope = this;
|
|
|
|
this.touchStartDelegate = function( event ) {
|
|
scope.onTouchStart( event );
|
|
};
|
|
|
|
this.touchMoveDelegate = function( event ) {
|
|
scope.onTouchMove( event );
|
|
};
|
|
|
|
this.touchEndDelegate = function( event ) {
|
|
scope.onTouchEnd( event );
|
|
};
|
|
|
|
this.element.addEventListener( 'touchstart', this.touchStartDelegate, false );
|
|
this.element.addEventListener( 'touchmove', this.touchMoveDelegate, false );
|
|
this.element.addEventListener( 'touchend', this.touchEndDelegate, false );
|
|
}
|
|
|
|
TouchList.prototype.onTouchStart = function( event ) {
|
|
event.preventDefault();
|
|
|
|
if( event.touches.length === 1 ) {
|
|
this.touch.isActive = true;
|
|
this.touch.start = event.touches[0].clientY;
|
|
this.touch.previous = this.touch.start;
|
|
this.touch.value = this.touch.start;
|
|
this.touch.offset = 0;
|
|
|
|
if( this.velocity ) {
|
|
this.touch.isAccellerating = true;
|
|
|
|
var scope = this;
|
|
|
|
this.touch.accellerateTimeout = setTimeout( function() {
|
|
scope.touch.isAccellerating = false;
|
|
scope.velocity = 0;
|
|
}, 500 );
|
|
}
|
|
else {
|
|
this.velocity = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
TouchList.prototype.onTouchMove = function( event ) {
|
|
if( event.touches.length === 1 ) {
|
|
var previous = this.touch.value;
|
|
|
|
this.touch.value = event.touches[0].clientY;
|
|
this.touch.lastMove = Date.now();
|
|
|
|
var sameDirection = ( this.touch.value > this.touch.previous && this.velocity < 0 )
|
|
|| ( this.touch.value < this.touch.previous && this.velocity > 0 );
|
|
|
|
if( this.touch.isAccellerating && sameDirection ) {
|
|
clearInterval( this.touch.accellerateTimeout );
|
|
|
|
// Increase velocity significantly
|
|
this.velocity += ( this.touch.previous - this.touch.value ) / 10;
|
|
}
|
|
else {
|
|
this.velocity = 0;
|
|
|
|
this.touch.isAccellerating = false;
|
|
this.touch.offset = Math.round( this.touch.start - this.touch.value );
|
|
}
|
|
|
|
this.touch.previous = previous;
|
|
}
|
|
}
|
|
|
|
TouchList.prototype.onTouchEnd = function( event ) {
|
|
var distanceMoved = this.touch.start - this.touch.value;
|
|
|
|
if( !this.touch.isAccellerating ) {
|
|
// Apply velocity based on the start position of the touch
|
|
this.velocity = ( this.touch.start - this.touch.value ) / 10;
|
|
}
|
|
|
|
// Don't apply any velocity if the touch ended in a still state
|
|
if( Date.now() - this.touch.lastMove > 200 || Math.abs( this.touch.previous - this.touch.value ) < 5 ) {
|
|
this.velocity = 0;
|
|
}
|
|
|
|
this.top.value += this.touch.offset;
|
|
|
|
// Reset the variables used to determne swipe speed
|
|
this.touch.offset = 0;
|
|
this.touch.start = 0;
|
|
this.touch.value = 0;
|
|
this.touch.isActive = false;
|
|
this.touch.isAccellerating = false;
|
|
|
|
clearInterval( this.touch.accellerateTimeout );
|
|
|
|
// If a swipe was captured, prevent event propagation
|
|
if( Math.abs( this.velocity ) > 4 || Math.abs( distanceMoved ) > 10 ) {
|
|
event.preventDefault();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Apply past/future classes to list items outside of the viewport
|
|
*/
|
|
TouchList.prototype.update = function( force ) {
|
|
// Determine the desired scroll top position
|
|
var scrollTop = this.top.value + this.velocity + this.touch.offset;
|
|
|
|
// Only scroll the list if there's input
|
|
if( this.velocity || this.touch.offset ) {
|
|
// Scroll the DOM and add on the offset from touch
|
|
this.element.scrollTop = scrollTop;
|
|
|
|
// Keep the scroll value within bounds
|
|
scrollTop = Math.max( 0, Math.min( this.element.scrollTop, this.top.max ) );
|
|
|
|
// Cache the currently set scroll top and touch offset
|
|
this.top.value = scrollTop - this.touch.offset;
|
|
}
|
|
|
|
// If there is no active touch, decay velocity
|
|
if( !this.touch.isActive || this.touch.isAccellerating ) {
|
|
this.velocity *= 0.95;
|
|
}
|
|
|
|
// Cut off early, the last fraction of velocity doesn't have
|
|
// much impact on movement
|
|
if( Math.abs( this.velocity ) < 0.15 ) {
|
|
this.velocity = 0;
|
|
}
|
|
|
|
// Only proceed if the scroll position has changed
|
|
if( scrollTop !== this.top.natural || force ) {
|
|
this.top.natural = scrollTop;
|
|
this.top.value = scrollTop - this.touch.offset;
|
|
|
|
var scrollBottom = scrollTop + this.listHeight;
|
|
|
|
// One loop to make our changes to the DOM
|
|
for( var i = 0, len = this.items.length; i < len; i++ ) {
|
|
var item = this.items[i];
|
|
|
|
// Above list viewport
|
|
if( item._offsetBottom < scrollTop ) {
|
|
// Exclusion via string matching improves performance
|
|
if( this.velocity <= 0 && item._state !== 'past' ) {
|
|
item.classList.add( 'past' );
|
|
item._state = 'past';
|
|
}
|
|
}
|
|
// Below list viewport
|
|
else if( item._offsetTop > scrollBottom ) {
|
|
// Exclusion via string matching improves performance
|
|
if( this.velocity >= 0 && item._state !== 'future' ) {
|
|
item.classList.add( 'future' );
|
|
item._state = 'future';
|
|
}
|
|
}
|
|
// Inside of list viewport
|
|
else if( item._state ) {
|
|
if( item._state === 'past' ) item.classList.remove( 'past' );
|
|
if( item._state === 'future' ) item.classList.remove( 'future' );
|
|
item._state = '';
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Cleans up after this list and disposes of it.
|
|
*/
|
|
TouchList.prototype.destroy = function() {
|
|
List.prototype.destroy.apply( this );
|
|
|
|
this.element.removeEventListener( 'touchstart', this.touchStartDelegate, false );
|
|
this.element.removeEventListener( 'touchmove', this.touchMoveDelegate, false );
|
|
this.element.removeEventListener( 'touchend', this.touchEndDelegate, false );
|
|
}
|
|
|
|
|
|
/**
|
|
* Public API
|
|
*/
|
|
window.stroll = {
|
|
/**
|
|
* Binds one or more lists for scroll effects.
|
|
*
|
|
* @see #add()
|
|
*/
|
|
bind: function( target, options ) {
|
|
if( isCapable() ) {
|
|
batch( target, add, options );
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Unbinds one or more lists from scroll effects.
|
|
*
|
|
* @see #remove()
|
|
*/
|
|
unbind: function( target ) {
|
|
if( isCapable() ) {
|
|
batch( target, remove );
|
|
}
|
|
}
|
|
}
|
|
|
|
window.requestAnimFrame = (function(){
|
|
return window.requestAnimationFrame ||
|
|
window.webkitRequestAnimationFrame ||
|
|
window.mozRequestAnimationFrame ||
|
|
window.oRequestAnimationFrame ||
|
|
window.msRequestAnimationFrame ||
|
|
function( callback ){
|
|
window.setTimeout(callback, 1000 / 60);
|
|
};
|
|
})()
|
|
|
|
})();
|