Lungo.Scroll.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. /*! Overthrow v.0.1.0. An overflow:auto polyfill for responsive design. (c) 2012: Scott Jehl, Filament Group, Inc. http://filamentgroup.github.com/Overthrow/license.txt */
  2. (function( w, undefined ){
  3. var doc = w.document,
  4. docElem = doc.documentElement,
  5. classtext = "scroll-enabled",
  6. // Touch events are used in the polyfill, and thus are a prerequisite
  7. canBeFilledWithPoly = "ontouchmove" in doc,
  8. // The following attempts to determine whether the browser has native overflow support
  9. // so we can enable it but not polyfill
  10. overflowProbablyAlreadyWorks =
  11. // Features-first. iOS5 overflow scrolling property check - no UA needed here. thanks Apple :)
  12. "WebkitOverflowScrolling" in docElem.style ||
  13. // Touch events aren't supported and screen width is greater than X
  14. // ...basically, this is a loose "desktop browser" check.
  15. // It may wrongly opt-in very large tablets with no touch support.
  16. ( !canBeFilledWithPoly && w.screen.width > 1200 ) ||
  17. // Hang on to your hats.
  18. // Whitelist some popular, overflow-supporting mobile browsers for now and the future
  19. // These browsers are known to get overlow support right, but give us no way of detecting it.
  20. (function(){
  21. var ua = w.navigator.userAgent,
  22. // Webkit crosses platforms, and the browsers on our list run at least version 534
  23. webkit = ua.match( /AppleWebKit\/([0-9]+)/ ),
  24. wkversion = webkit && webkit[1],
  25. wkLte534 = webkit && wkversion >= 534;
  26. return (
  27. /* Android 3+ with webkit gte 534
  28. ~: Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13 */
  29. ua.match( /Android ([0-9]+)/ ) && RegExp.$1 >= 3 && wkLte534 ||
  30. /* Blackberry 7+ with webkit gte 534
  31. ~: Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en-US) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.0.0 Mobile Safari/534.11+ */
  32. ua.match( / Version\/([0-9]+)/ ) && RegExp.$1 >= 0 && w.blackberry && wkLte534 ||
  33. /* Blackberry Playbook with webkit gte 534
  34. ~: Mozilla/5.0 (PlayBook; U; RIM Tablet OS 1.0.0; en-US) AppleWebKit/534.8+ (KHTML, like Gecko) Version/0.0.1 Safari/534.8+ */
  35. ua.indexOf( /PlayBook/ ) > -1 && RegExp.$1 >= 0 && wkLte534 ||
  36. /* Firefox Mobile (Fennec) 4 and up
  37. ~: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:2.1.1) Gecko/ Firefox/4.0.2pre Fennec/4.0. */
  38. ua.match( /Fennec\/([0-9]+)/ ) && RegExp.$1 >= 4 ||
  39. /* WebOS 3 and up (TouchPad too)
  40. ~: Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.0; U; en-US) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/233.48 Safari/534.6 TouchPad/1.0 */
  41. ua.match( /wOSBrowser\/([0-9]+)/ ) && RegExp.$1 >= 233 && wkLte534 ||
  42. /* Nokia Browser N8
  43. ~: Mozilla/5.0 (Symbian/3; Series60/5.2 NokiaN8-00/012.002; Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/533.4 (KHTML, like Gecko) NokiaBrowser/7.3.0 Mobile Safari/533.4 3gpp-gba
  44. ~: Note: the N9 doesn't have native overflow with one-finger touch. wtf */
  45. ua.match( /NokiaBrowser\/([0-9\.]+)/ ) && parseFloat(RegExp.$1) === 7.3 && webkit && wkversion >= 533
  46. );
  47. })(),
  48. // Easing can use any of Robert Penner's equations (http://www.robertpenner.com/easing_terms_of_use.html). By default, overthrow includes ease-out-cubic
  49. // arguments: t = current iteration, b = initial value, c = end value, d = total iterations
  50. // use w.overthrow.easing to provide a custom function externally, or pass an easing function as a callback to the toss method
  51. defaultEasing = function (t, b, c, d) {
  52. return c*((t=t/d-1)*t*t + 1) + b;
  53. },
  54. enabled = false,
  55. // Keeper of intervals
  56. timeKeeper,
  57. /* toss scrolls and element with easing
  58. // elem is the element to scroll
  59. // options hash:
  60. * left is the desired horizontal scroll. Default is "+0". For relative distances, pass a string with "+" or "-" in front.
  61. * top is the desired vertical scroll. Default is "+0". For relative distances, pass a string with "+" or "-" in front.
  62. * duration is the number of milliseconds the throw will take. Default is 100.
  63. * easing is an optional custom easing function. Default is w.overthrow.easing. Must follow the easing function signature
  64. */
  65. toss = function( elem, options ){
  66. var i = 0,
  67. sLeft = elem.scrollLeft,
  68. sTop = elem.scrollTop,
  69. // Toss defaults
  70. o = {
  71. top: "+0",
  72. left: "+0",
  73. duration: 100,
  74. easing: w.overthrow.easing
  75. },
  76. endLeft, endTop;
  77. // Mixin based on predefined defaults
  78. if( options ){
  79. for( var j in o ){
  80. if( options[ j ] !== undefined ){
  81. o[ j ] = options[ j ];
  82. }
  83. }
  84. }
  85. // Convert relative values to ints
  86. // First the left val
  87. if( typeof o.left === "string" ){
  88. o.left = parseFloat( o.left );
  89. endLeft = o.left + sLeft;
  90. }
  91. else {
  92. endLeft = o.left;
  93. o.left = o.left - sLeft;
  94. }
  95. // Then the top val
  96. if( typeof o.top === "string" ){
  97. o.top = parseFloat( o.top );
  98. endTop = o.top + sTop;
  99. }
  100. else {
  101. endTop = o.top;
  102. o.top = o.top - sTop;
  103. }
  104. timeKeeper = setInterval(function(){
  105. if( i++ < o.duration ){
  106. elem.scrollLeft = o.easing( i, sLeft, o.left, o.duration );
  107. elem.scrollTop = o.easing( i, sTop, o.top, o.duration );
  108. }
  109. else{
  110. if( endLeft !== elem.scrollLeft ){
  111. elem.scrollLeft = endLeft;
  112. }
  113. if( endTop !== elem.scrollTop ){
  114. elem.scrollTop = endTop;
  115. }
  116. intercept();
  117. }
  118. }, 1 );
  119. // Return the values, post-mixin, with end values specified
  120. return { top: endTop, left: endLeft, duration: o.duration, easing: o.easing };
  121. },
  122. // find closest overthrow (elem or a parent)
  123. closest = function( target, ascend ){
  124. return !ascend && target.className && target.className.indexOf( "scroll" ) > -1 && target || closest( target.parentNode );
  125. },
  126. // Intercept any throw in progress
  127. intercept = function(){
  128. clearInterval( timeKeeper );
  129. },
  130. // Enable and potentially polyfill overflow
  131. enable = function(){
  132. // If it's on,
  133. if( enabled ){
  134. return;
  135. }
  136. // It's on.
  137. enabled = true;
  138. // If overflowProbablyAlreadyWorks or at least the element canBeFilledWithPoly, add a class to cue CSS that assumes overflow scrolling will work (setting height on elements and such)
  139. if( overflowProbablyAlreadyWorks || canBeFilledWithPoly ){
  140. docElem.className += " " + classtext;
  141. }
  142. // Destroy everything later. If you want to.
  143. w.overthrow.forget = function(){
  144. // Strip the class name from docElem
  145. docElem.className = docElem.className.replace( classtext, "" );
  146. // Remove touch binding (check for method support since this part isn't qualified by touch support like the rest)
  147. if( doc.removeEventListener ){
  148. doc.removeEventListener( "touchstart", start, false );
  149. }
  150. // reset easing to default
  151. w.overthrow.easing = defaultEasing;
  152. // Let 'em know
  153. enabled = false;
  154. };
  155. // If overflowProbablyAlreadyWorks or it doesn't look like the browser canBeFilledWithPoly, our job is done here. Exit viewport left.
  156. if( overflowProbablyAlreadyWorks || !canBeFilledWithPoly ){
  157. return;
  158. }
  159. // Fill 'er up!
  160. // From here down, all logic is associated with touch scroll handling
  161. // elem references the overthrow element in use
  162. var elem,
  163. // The last several Y values are kept here
  164. lastTops = [],
  165. // The last several X values are kept here
  166. lastLefts = [],
  167. // lastDown will be true if the last scroll direction was down, false if it was up
  168. lastDown,
  169. // lastRight will be true if the last scroll direction was right, false if it was left
  170. lastRight,
  171. // For a new gesture, or change in direction, reset the values from last scroll
  172. resetVertTracking = function(){
  173. lastTops = [];
  174. lastDown = null;
  175. },
  176. resetHorTracking = function(){
  177. lastLefts = [];
  178. lastRight = null;
  179. },
  180. // After releasing touchend, throw the overthrow element, depending on momentum
  181. finishScroll = function(){
  182. // Come up with a distance and duration based on how
  183. // Multipliers are tweaked to a comfortable balance across platforms
  184. var top = ( lastTops[ 0 ] - lastTops[ lastTops.length -1 ] ) * 8,
  185. left = ( lastLefts[ 0 ] - lastLefts[ lastLefts.length -1 ] ) * 8,
  186. duration = Math.max( Math.abs( left ), Math.abs( top ) ) / 8;
  187. // Make top and left relative-style strings (positive vals need "+" prefix)
  188. top = ( top > 0 ? "+" : "" ) + top;
  189. left = ( left > 0 ? "+" : "" ) + left;
  190. // Make sure there's a significant amount of throw involved, otherwise, just stay still
  191. if( !isNaN( duration ) && duration > 0 && ( Math.abs( left ) > 80 || Math.abs( top ) > 80 ) ){
  192. toss( elem, { left: left, top: top, duration: duration } );
  193. }
  194. },
  195. // On webkit, touch events hardly trickle through textareas and inputs
  196. // Disabling CSS pointer events makes sure they do, but it also makes the controls innaccessible
  197. // Toggling pointer events at the right moments seems to do the trick
  198. // Thanks Thomas Bachem http://stackoverflow.com/a/5798681 for the following
  199. inputs,
  200. setPointers = function( val ){
  201. inputs = elem.querySelectorAll( "textarea, input" );
  202. for( var i = 0, il = inputs.length; i < il; i++ ) {
  203. inputs[ i ].style.pointerEvents = val;
  204. }
  205. },
  206. // For nested overthrows, changeScrollTarget restarts a touch event cycle on a parent or child overthrow
  207. changeScrollTarget = function( startEvent, ascend ){
  208. if( doc.createEvent ){
  209. var newTarget = ( !ascend || ascend === undefined ) && elem.parentNode || elem.touchchild || elem,
  210. tEnd;
  211. if( newTarget !== elem ){
  212. tEnd = doc.createEvent( "HTMLEvents" );
  213. tEnd.initEvent( "touchend", true, true );
  214. elem.dispatchEvent( tEnd );
  215. newTarget.touchchild = elem;
  216. elem = newTarget;
  217. newTarget.dispatchEvent( startEvent );
  218. }
  219. }
  220. },
  221. // Touchstart handler
  222. // On touchstart, touchmove and touchend are freshly bound, and all three share a bunch of vars set by touchstart
  223. // Touchend unbinds them again, until next time
  224. start = function( e ){
  225. // Stop any throw in progress
  226. intercept();
  227. // Reset the distance and direction tracking
  228. resetVertTracking();
  229. resetHorTracking();
  230. elem = closest( e.target );
  231. if( !elem || elem === docElem || e.touches.length > 1 ){
  232. return;
  233. }
  234. setPointers( "none" );
  235. var touchStartE = e,
  236. scrollT = elem.scrollTop,
  237. scrollL = elem.scrollLeft,
  238. height = elem.offsetHeight,
  239. width = elem.offsetWidth,
  240. startY = e.touches[ 0 ].pageY,
  241. startX = e.touches[ 0 ].pageX,
  242. scrollHeight = elem.scrollHeight,
  243. scrollWidth = elem.scrollWidth,
  244. // Touchmove handler
  245. move = function( e ){
  246. var ty = scrollT + startY - e.touches[ 0 ].pageY,
  247. tx = scrollL + startX - e.touches[ 0 ].pageX,
  248. down = ty >= ( lastTops.length ? lastTops[ 0 ] : 0 ),
  249. right = tx >= ( lastLefts.length ? lastLefts[ 0 ] : 0 );
  250. // If there's room to scroll the current container, prevent the default window scroll
  251. if( ( ty > 0 && ty < scrollHeight - height ) || ( tx > 0 && tx < scrollWidth - width ) ){
  252. e.preventDefault();
  253. }
  254. // This bubbling is dumb. Needs a rethink.
  255. else {
  256. changeScrollTarget( touchStartE );
  257. }
  258. // If down and lastDown are inequal, the y scroll has changed direction. Reset tracking.
  259. if( lastDown && down !== lastDown ){
  260. resetVertTracking();
  261. }
  262. // If right and lastRight are inequal, the x scroll has changed direction. Reset tracking.
  263. if( lastRight && right !== lastRight ){
  264. resetHorTracking();
  265. }
  266. // remember the last direction in which we were headed
  267. lastDown = down;
  268. lastRight = right;
  269. // set the container's scroll
  270. elem.scrollTop = ty;
  271. elem.scrollLeft = tx;
  272. lastTops.unshift( ty );
  273. lastLefts.unshift( tx );
  274. if( lastTops.length > 3 ){
  275. lastTops.pop();
  276. }
  277. if( lastLefts.length > 3 ){
  278. lastLefts.pop();
  279. }
  280. },
  281. // Touchend handler
  282. end = function( e ){
  283. // Apply momentum based easing for a graceful finish
  284. finishScroll();
  285. // Bring the pointers back
  286. setPointers( "auto" );
  287. setTimeout( function(){
  288. setPointers( "none" );
  289. }, 450 );
  290. elem.removeEventListener( "touchmove", move, false );
  291. elem.removeEventListener( "touchend", end, false );
  292. };
  293. elem.addEventListener( "touchmove", move, false );
  294. elem.addEventListener( "touchend", end, false );
  295. };
  296. // Bind to touch, handle move and end within
  297. doc.addEventListener( "touchstart", start, false );
  298. };
  299. // Expose overthrow API
  300. w.overthrow = {
  301. set: enable,
  302. forget: function(){},
  303. easing: defaultEasing,
  304. toss: toss,
  305. intercept: intercept,
  306. closest: closest,
  307. support: overflowProbablyAlreadyWorks ? "native" : canBeFilledWithPoly && "polyfilled" || "none"
  308. };
  309. // Auto-init
  310. enable();
  311. })( this );