Dashboard sipadu mbip
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

stroll.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. /*!
  2. * stroll.js 1.2 - CSS scroll effects
  3. * http://lab.hakim.se/scroll-effects
  4. * MIT licensed
  5. *
  6. * Copyright (C) 2012 Hakim El Hattab, http://hakim.se
  7. */
  8. (function(){
  9. "use strict";
  10. // When a list is configured as 'live', this is how frequently
  11. // the DOM will be polled for changes
  12. var LIVE_INTERVAL = 500;
  13. var IS_TOUCH_DEVICE = !!( 'ontouchstart' in window );
  14. // All of the lists that are currently bound
  15. var lists = [];
  16. // Set to true when there are lists to refresh
  17. var active = false;
  18. /**
  19. * Updates all currently bound lists.
  20. */
  21. function refresh() {
  22. if( active ) {
  23. requestAnimFrame( refresh );
  24. for( var i = 0, len = lists.length; i < len; i++ ) {
  25. lists[i].update();
  26. }
  27. }
  28. }
  29. /**
  30. * Starts monitoring a list and applies classes to each of
  31. * its contained elements based on its position relative to
  32. * the list's viewport.
  33. *
  34. * @param {HTMLElement} element
  35. * @param {Object} options Additional arguments;
  36. * - live; Flags if the DOM should be repeatedly checked for changes
  37. * repeatedly. Useful if the list contents is changing. Use
  38. * scarcely as it has an impact on performance.
  39. */
  40. function add( element, options ) {
  41. // Only allow ul/ol
  42. if( !element.nodeName || /^(ul|ol)$/i.test( element.nodeName ) === false ) {
  43. return false;
  44. }
  45. // Delete duplicates (but continue and re-bind this list to get the
  46. // latest properties and list items)
  47. else if( contains( element ) ) {
  48. remove( element );
  49. }
  50. var list = IS_TOUCH_DEVICE ? new TouchList( element ) : new List( element );
  51. // Handle options
  52. if( options && options.live ) {
  53. list.syncInterval = setInterval( function() {
  54. list.sync.call( list );
  55. }, LIVE_INTERVAL );
  56. }
  57. // Synchronize the list with the DOM
  58. list.sync();
  59. // Add this element to the collection
  60. lists.push( list );
  61. // Start refreshing if this was the first list to be added
  62. if( lists.length === 1 ) {
  63. active = true;
  64. refresh();
  65. }
  66. }
  67. /**
  68. * Stops monitoring a list element and removes any classes
  69. * that were applied to its list items.
  70. *
  71. * @param {HTMLElement} element
  72. */
  73. function remove( element ) {
  74. for( var i = 0; i < lists.length; i++ ) {
  75. var list = lists[i];
  76. if( list.element == element ) {
  77. list.destroy();
  78. lists.splice( i, 1 );
  79. i--;
  80. }
  81. }
  82. // Stopped refreshing if the last list was removed
  83. if( lists.length === 0 ) {
  84. active = false;
  85. }
  86. }
  87. /**
  88. * Checks if the specified element has already been bound.
  89. */
  90. function contains( element ) {
  91. for( var i = 0, len = lists.length; i < len; i++ ) {
  92. if( lists[i].element == element ) {
  93. return true;
  94. }
  95. }
  96. return false;
  97. }
  98. /**
  99. * Calls 'method' for each DOM element discovered in
  100. * 'target'.
  101. *
  102. * @param target String selector / array of UL elements /
  103. * jQuery object / single UL element
  104. * @param method A function to call for each element target
  105. */
  106. function batch( target, method, options ) {
  107. var i, len;
  108. // Selector
  109. if( typeof target === 'string' ) {
  110. var targets = document.querySelectorAll( target );
  111. for( i = 0, len = targets.length; i < len; i++ ) {
  112. method.call( null, targets[i], options );
  113. }
  114. }
  115. // Array (jQuery)
  116. else if( typeof target === 'object' && typeof target.length === 'number' ) {
  117. for( i = 0, len = target.length; i < len; i++ ) {
  118. method.call( null, target[i], options );
  119. }
  120. }
  121. // Single element
  122. else if( target.nodeName ) {
  123. method.call( null, target, options );
  124. }
  125. else {
  126. throw 'Stroll target was of unexpected type.';
  127. }
  128. }
  129. /**
  130. * Checks if the client is capable of running the library.
  131. */
  132. function isCapable() {
  133. return !!document.body.classList;
  134. }
  135. /**
  136. * The basic type of list; applies past & future classes to
  137. * list items based on scroll state.
  138. */
  139. function List( element ) {
  140. this.element = element;
  141. }
  142. /**
  143. * Fetches the latest properties from the DOM to ensure that
  144. * this list is in sync with its contents.
  145. */
  146. List.prototype.sync = function() {
  147. this.items = Array.prototype.slice.apply( this.element.children );
  148. // Caching some heights so we don't need to go back to the DOM so much
  149. this.listHeight = this.element.offsetHeight;
  150. // One loop to get the offsets from the DOM
  151. for( var i = 0, len = this.items.length; i < len; i++ ) {
  152. var item = this.items[i];
  153. item._offsetHeight = item.offsetHeight;
  154. item._offsetTop = item.offsetTop;
  155. item._offsetBottom = item._offsetTop + item._offsetHeight;
  156. item._state = '';
  157. }
  158. // Force an update
  159. this.update( true );
  160. }
  161. /**
  162. * Apply past/future classes to list items outside of the viewport
  163. */
  164. List.prototype.update = function( force ) {
  165. var scrollTop = this.element.pageYOffset || this.element.scrollTop,
  166. scrollBottom = scrollTop + this.listHeight;
  167. // Quit if nothing changed
  168. if( scrollTop !== this.lastTop || force ) {
  169. this.lastTop = scrollTop;
  170. // One loop to make our changes to the DOM
  171. for( var i = 0, len = this.items.length; i < len; i++ ) {
  172. var item = this.items[i];
  173. // Above list viewport
  174. if( item._offsetBottom < scrollTop ) {
  175. // Exclusion via string matching improves performance
  176. if( item._state !== 'past' ) {
  177. item._state = 'past';
  178. item.classList.add( 'past' );
  179. item.classList.remove( 'future' );
  180. }
  181. }
  182. // Below list viewport
  183. else if( item._offsetTop > scrollBottom ) {
  184. // Exclusion via string matching improves performance
  185. if( item._state !== 'future' ) {
  186. item._state = 'future';
  187. item.classList.add( 'future' );
  188. item.classList.remove( 'past' );
  189. }
  190. }
  191. // Inside of list viewport
  192. else if( item._state ) {
  193. if( item._state === 'past' ) item.classList.remove( 'past' );
  194. if( item._state === 'future' ) item.classList.remove( 'future' );
  195. item._state = '';
  196. }
  197. }
  198. }
  199. }
  200. /**
  201. * Cleans up after this list and disposes of it.
  202. */
  203. List.prototype.destroy = function() {
  204. clearInterval( this.syncInterval );
  205. for( var j = 0, len = this.items.length; j < len; j++ ) {
  206. var item = this.items[j];
  207. item.classList.remove( 'past' );
  208. item.classList.remove( 'future' );
  209. }
  210. }
  211. /**
  212. * A list specifically for touch devices. Simulates the style
  213. * of scrolling you'd see on a touch device but does not rely
  214. * on webkit-overflow-scrolling since that makes it impossible
  215. * to read the up-to-date scroll position.
  216. */
  217. function TouchList( element ) {
  218. this.element = element;
  219. this.element.style.overflow = 'hidden';
  220. this.top = {
  221. value: 0,
  222. natural: 0
  223. };
  224. this.touch = {
  225. value: 0,
  226. offset: 0,
  227. start: 0,
  228. previous: 0,
  229. lastMove: Date.now(),
  230. accellerateTimeout: -1,
  231. isAccellerating: false,
  232. isActive: false
  233. };
  234. this.velocity = 0;
  235. }
  236. TouchList.prototype = new List();
  237. /**
  238. * Fetches the latest properties from the DOM to ensure that
  239. * this list is in sync with its contents. This is typically
  240. * only used once (per list) at initialization.
  241. */
  242. TouchList.prototype.sync = function() {
  243. this.items = Array.prototype.slice.apply( this.element.children );
  244. this.listHeight = this.element.offsetHeight;
  245. var item;
  246. // One loop to get the properties we need from the DOM
  247. for( var i = 0, len = this.items.length; i < len; i++ ) {
  248. item = this.items[i];
  249. item._offsetHeight = item.offsetHeight;
  250. item._offsetTop = item.offsetTop;
  251. item._offsetBottom = item._offsetTop + item._offsetHeight;
  252. item._state = '';
  253. // Animating opacity is a MAJOR performance hit on mobile so we can't allow it
  254. item.style.opacity = 1;
  255. }
  256. this.top.natural = this.element.scrollTop;
  257. this.top.value = this.top.natural;
  258. this.top.max = item._offsetBottom - this.listHeight;
  259. // Force an update
  260. this.update( true );
  261. this.bind();
  262. }
  263. /**
  264. * Binds the events for this list. References to proxy methods
  265. * are kept for unbinding if the list is disposed of.
  266. */
  267. TouchList.prototype.bind = function() {
  268. var scope = this;
  269. this.touchStartDelegate = function( event ) {
  270. scope.onTouchStart( event );
  271. };
  272. this.touchMoveDelegate = function( event ) {
  273. scope.onTouchMove( event );
  274. };
  275. this.touchEndDelegate = function( event ) {
  276. scope.onTouchEnd( event );
  277. };
  278. this.element.addEventListener( 'touchstart', this.touchStartDelegate, false );
  279. this.element.addEventListener( 'touchmove', this.touchMoveDelegate, false );
  280. this.element.addEventListener( 'touchend', this.touchEndDelegate, false );
  281. }
  282. TouchList.prototype.onTouchStart = function( event ) {
  283. event.preventDefault();
  284. if( event.touches.length === 1 ) {
  285. this.touch.isActive = true;
  286. this.touch.start = event.touches[0].clientY;
  287. this.touch.previous = this.touch.start;
  288. this.touch.value = this.touch.start;
  289. this.touch.offset = 0;
  290. if( this.velocity ) {
  291. this.touch.isAccellerating = true;
  292. var scope = this;
  293. this.touch.accellerateTimeout = setTimeout( function() {
  294. scope.touch.isAccellerating = false;
  295. scope.velocity = 0;
  296. }, 500 );
  297. }
  298. else {
  299. this.velocity = 0;
  300. }
  301. }
  302. }
  303. TouchList.prototype.onTouchMove = function( event ) {
  304. if( event.touches.length === 1 ) {
  305. var previous = this.touch.value;
  306. this.touch.value = event.touches[0].clientY;
  307. this.touch.lastMove = Date.now();
  308. var sameDirection = ( this.touch.value > this.touch.previous && this.velocity < 0 )
  309. || ( this.touch.value < this.touch.previous && this.velocity > 0 );
  310. if( this.touch.isAccellerating && sameDirection ) {
  311. clearInterval( this.touch.accellerateTimeout );
  312. // Increase velocity significantly
  313. this.velocity += ( this.touch.previous - this.touch.value ) / 10;
  314. }
  315. else {
  316. this.velocity = 0;
  317. this.touch.isAccellerating = false;
  318. this.touch.offset = Math.round( this.touch.start - this.touch.value );
  319. }
  320. this.touch.previous = previous;
  321. }
  322. }
  323. TouchList.prototype.onTouchEnd = function( event ) {
  324. var distanceMoved = this.touch.start - this.touch.value;
  325. if( !this.touch.isAccellerating ) {
  326. // Apply velocity based on the start position of the touch
  327. this.velocity = ( this.touch.start - this.touch.value ) / 10;
  328. }
  329. // Don't apply any velocity if the touch ended in a still state
  330. if( Date.now() - this.touch.lastMove > 200 || Math.abs( this.touch.previous - this.touch.value ) < 5 ) {
  331. this.velocity = 0;
  332. }
  333. this.top.value += this.touch.offset;
  334. // Reset the variables used to determne swipe speed
  335. this.touch.offset = 0;
  336. this.touch.start = 0;
  337. this.touch.value = 0;
  338. this.touch.isActive = false;
  339. this.touch.isAccellerating = false;
  340. clearInterval( this.touch.accellerateTimeout );
  341. // If a swipe was captured, prevent event propagation
  342. if( Math.abs( this.velocity ) > 4 || Math.abs( distanceMoved ) > 10 ) {
  343. event.preventDefault();
  344. }
  345. };
  346. /**
  347. * Apply past/future classes to list items outside of the viewport
  348. */
  349. TouchList.prototype.update = function( force ) {
  350. // Determine the desired scroll top position
  351. var scrollTop = this.top.value + this.velocity + this.touch.offset;
  352. // Only scroll the list if there's input
  353. if( this.velocity || this.touch.offset ) {
  354. // Scroll the DOM and add on the offset from touch
  355. this.element.scrollTop = scrollTop;
  356. // Keep the scroll value within bounds
  357. scrollTop = Math.max( 0, Math.min( this.element.scrollTop, this.top.max ) );
  358. // Cache the currently set scroll top and touch offset
  359. this.top.value = scrollTop - this.touch.offset;
  360. }
  361. // If there is no active touch, decay velocity
  362. if( !this.touch.isActive || this.touch.isAccellerating ) {
  363. this.velocity *= 0.95;
  364. }
  365. // Cut off early, the last fraction of velocity doesn't have
  366. // much impact on movement
  367. if( Math.abs( this.velocity ) < 0.15 ) {
  368. this.velocity = 0;
  369. }
  370. // Only proceed if the scroll position has changed
  371. if( scrollTop !== this.top.natural || force ) {
  372. this.top.natural = scrollTop;
  373. this.top.value = scrollTop - this.touch.offset;
  374. var scrollBottom = scrollTop + this.listHeight;
  375. // One loop to make our changes to the DOM
  376. for( var i = 0, len = this.items.length; i < len; i++ ) {
  377. var item = this.items[i];
  378. // Above list viewport
  379. if( item._offsetBottom < scrollTop ) {
  380. // Exclusion via string matching improves performance
  381. if( this.velocity <= 0 && item._state !== 'past' ) {
  382. item.classList.add( 'past' );
  383. item._state = 'past';
  384. }
  385. }
  386. // Below list viewport
  387. else if( item._offsetTop > scrollBottom ) {
  388. // Exclusion via string matching improves performance
  389. if( this.velocity >= 0 && item._state !== 'future' ) {
  390. item.classList.add( 'future' );
  391. item._state = 'future';
  392. }
  393. }
  394. // Inside of list viewport
  395. else if( item._state ) {
  396. if( item._state === 'past' ) item.classList.remove( 'past' );
  397. if( item._state === 'future' ) item.classList.remove( 'future' );
  398. item._state = '';
  399. }
  400. }
  401. }
  402. };
  403. /**
  404. * Cleans up after this list and disposes of it.
  405. */
  406. TouchList.prototype.destroy = function() {
  407. List.prototype.destroy.apply( this );
  408. this.element.removeEventListener( 'touchstart', this.touchStartDelegate, false );
  409. this.element.removeEventListener( 'touchmove', this.touchMoveDelegate, false );
  410. this.element.removeEventListener( 'touchend', this.touchEndDelegate, false );
  411. }
  412. /**
  413. * Public API
  414. */
  415. window.stroll = {
  416. /**
  417. * Binds one or more lists for scroll effects.
  418. *
  419. * @see #add()
  420. */
  421. bind: function( target, options ) {
  422. if( isCapable() ) {
  423. batch( target, add, options );
  424. }
  425. },
  426. /**
  427. * Unbinds one or more lists from scroll effects.
  428. *
  429. * @see #remove()
  430. */
  431. unbind: function( target ) {
  432. if( isCapable() ) {
  433. batch( target, remove );
  434. }
  435. }
  436. }
  437. window.requestAnimFrame = (function(){
  438. return window.requestAnimationFrame ||
  439. window.webkitRequestAnimationFrame ||
  440. window.mozRequestAnimationFrame ||
  441. window.oRequestAnimationFrame ||
  442. window.msRequestAnimationFrame ||
  443. function( callback ){
  444. window.setTimeout(callback, 1000 / 60);
  445. };
  446. })()
  447. })();