Mini Shell
/*!
* 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);
};
})()
})();
Zerion Mini Shell 1.0