| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534 |
- /*!
- * 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);
- };
- })()
-
- })();
|