Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial import into public repository.

  • Loading branch information...
commit e276b6d550a6dd69816888f67e8d24a1863e6c4d 0 parents
@davidaurelio authored
2  .gitignore
@@ -0,0 +1,2 @@
+*.kpf
+.DS_Store
199 demo/css/demo.css
@@ -0,0 +1,199 @@
+html, body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+}
+
+body {
+ font: 100%/1.5 Helvetica, Arial, sans-serif;
+ color: #444;
+ background: white;
+}
+
+#chrome {
+ height: 100%;
+}
+
+#demo {
+ height: 100%;
+ background: #fff;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-box-pack: justify;
+}
+
+@media only screen and (min-width: 440px) {
+ #chrome {
+ height: 611px;
+ padding: 159px 0 0 47px;
+ background: url(../img/iphone.png) no-repeat;
+ }
+
+ #demo {
+ width: 320px;
+ height: 460px;
+ -webkit-border-bottom-left-radius: 2px;
+ -webkit-border-bottom-right-radius: 2px;
+ overflow: hidden;
+ }
+}
+
+@media only screen and (min-width: 880px) {
+ #chrome {
+ height: 1136px;
+ padding: 71px 0 0 56px;
+ background: url(../img/ipad.png) no-repeat;
+ }
+
+ #demo {
+ width: 768px;
+ height: 1004px;
+ }
+}
+
+/* probably iphone portrait */
+@media screen and (width: 480px) and (max-height: 300px) {
+ #chrome {
+ background: none;
+ height: 100%;
+ padding: 0;
+ }
+
+ #demo {
+ height: 100%;
+ width: 100%;
+ -webkit-border-radius: 0;
+ }
+}
+
+/* probably ipad portrait */
+@media screen and (width: 768px) and (max-height: 1004px) {
+ #chrome {
+ background: none;
+ height: 100%;
+ padding: 0;
+ }
+
+ #demo {
+ height: 100%;
+ width: 100%;
+ -webkit-border-radius: 0;
+ }
+}
+
+/* probably ipad landscape */
+@media only screen and (width: 1024px) and (max-height: 748px) {
+ #chrome {
+ background: none;
+ height: 100%;
+ padding: 0;
+ }
+
+ #demo {
+ height: 100%;
+ width: 100%;
+ -webkit-border-radius: 0;
+ }
+}
+
+.toolbar {
+ border-bottom: 1px solid #2d3642;
+ border-top: 1px solid #cdd5df;
+ padding: 8px 4px;
+ height: 28px;
+ background: -webkit-gradient(linear, 0 0, 0 100%, from(#b0bccd), color-stop(.5,#889bb3), color-stop(.5,#8195af), to(#6d84a2));
+ overflow: hidden;
+ display: webkit-box;
+}
+
+h1 {
+ line-height: 26px;
+ font-size: 1.25em;
+ font-weight: bold;
+ text-shadow: rgba(0, 0, 0, 0.4) 0px -1px 0;
+ white-space: nowrap;
+ color: #fff;
+ text-align: center;
+ margin: 0 4px;
+ overflow: hidden;
+}
+
+.tabs {
+ border-top: 1px solid #333;
+ background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#555), to(#000000), color-stop(.5,#333),color-stop(.5,#262626));
+ display: -webkit-box;
+ text-align: center;
+}
+
+
+.tabs > * {
+ font-size: .8em;
+ height: 1.2em;
+ padding: 35px 0 .5em 0;
+ color: #808184;
+ text-decoration: none;
+ margin: .3em;
+ display: block;
+ width: 1px;
+ -webkit-box-flex: 1;
+ max-width: 25% !important;
+}
+
+.tabs > .current {
+ -webkit-border-radius: .4em;
+ background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#777), to(#222), color-stop(.5,#444),color-stop(.5,#363636));
+ color: #ccc;
+}
+
+.tabs > *:before {
+ background: none center no-repeat;
+ content: "";
+ display: block;
+ font: bold 32px/35px inherit;
+ height: 35px;
+ margin-top: -35px;
+ text-align: center;
+ text-shadow: rgba(255,255,255,0.3) 1px 1px 0px;
+}
+
+.tabs > [href="#about"]:before {
+ content: "Ts";
+ letter-spacing: -3px;
+ text-indent: -5px;
+}
+
+.tabs > [href="#horiz"]:before {
+ content: "\2194";
+}
+
+.tabs > [href="#twodim"]:before {
+ content: "\2194\2195";
+ letter-spacing: -24px;
+ text-indent: -32px;
+}
+
+.scroller {
+ -webkit-box-flex: 1;
+ display: none;
+ overflow: hidden;
+ padding: 1em;
+}
+
+.touchScroll {
+ padding: 0;
+}
+
+.touchScrollInner {
+ padding: 0.5em;
+}
+
+h2 {
+ color: #405070;
+ font-size: 1.125em;
+ text-align: center;
+ text-shadow: rgba(0,0,0,0.2) 1px 1px 0;
+}
+
+h2, p {
+ margin-bottom: .75em;
+}
BIN  demo/img/ipad.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  demo/img/iphone.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
128 demo/index.html
@@ -0,0 +1,128 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>TouchScroll Demo</title>
+ <meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <link href="css/demo.css" rel="stylesheet">
+ <link href="../src/touchscroll.css" rel="stylesheet">
+</head>
+<body>
+
+<div id="chrome">
+ <div id="demo">
+ <div class="toolbar"><h1>TouchScroll Demo</h1></div>
+
+ <div class="scroller" id="about">
+
+<h2>TouchScroll</h2>
+
+<p>TouchScroll is a JavaScript- and CSS 3-based scroller for devices using Webkit Touch (yes, that includes Android). It is meant to mimic “native” scrolling feeling and behavior as much as possible.</p>
+
+<p><strong>This demo works best on iPhone, iPad and in WebKit Nightly.</strong> Safari 4 kind of works, too.</p>
+
+<h2>A JavaScript-based scroller?</h2>
+
+<p>While Safari Mobile features native scrolling, there are two reasons to re-implement scrolling with JavaScript: The original scrolling behavior is rather slow – we were looking for a solution that feels more fluid. And the viewport behavior of Mobile Safari and other mobile browsers using WebKit doesn’t allow for fixed positioned elements.</p>
+
+<h2>How is it done?</h2>
+
+<p>TouchScroll uses a combination of JavaScript and CSS 3. All animations are done with CSS transitions – no JavaScript intervals. That means, the deceleration animation can be configured with a <a href="http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag">bezier curve</a>.</p>
+
+<p>The elasticity effect when crossing the scroller bounds is achieved by dividing the bezier curve into two sub-curves using my (yet to be finished) <a href="http://github.com/davidaurelio/css-beziers">CSS Bezier library for JavaScript</a>.</p>
+
+<p>On Safari mobile TouchScroll benefits from hardware acceleration for CSS transforms and transitions, which allows for very smooth scrolling. Android hasn’t any hardware support, which makes the animations choppier than on Apples devices.</p>
+
+<h2>Updating the scroller</h2>
+
+<p>The scroller updates its dimensions and position automatically on window resizes (<code>window.onresize</code>), orientation changes (<code>window.onorientationchange</code>) and DOM modifications inside the scroller (<code>DOMSubtreeModified</code> event). It only needs to be updated manually when it wasn’t visible on initialization (or last update) as soon it is made visible again.</p>
+
+<h2>Customization</h2>
+
+<p>TouchScroll allows you to customize many aspects of the scrolling behavior. The following aspects are configurable:</p>
+
+<ul>
+<li>minimum scrolling offset – to prevent unintended scrolling, a minumum offset can be defined. It defaults to 5 pixels.</li>
+<li>Flicking behavior: maximum duration between the last move event and the release event that triggers flicking. The default is 150&thinsp;ms.</li>
+<li>Average friction and bezier curve for the flicking animation (CSS transition)</li>
+<li>Elasticity at the borders, and the maximum bounce length when flicking.</li>
+<li>The snapback duration and its timing function.</li>
+</ul>
+
+<h2>Get it!</h2>
+
+<p>TouchScroll is released under a BSD license. You can find more information on how to get TouchScroll <a href="http://uxebu.com/blog/2010/04/27/touchscroll-a-scrolling-layer-for-webkit-mobile">on our blog</a>.</p>
+
+<h2>More info</h2>
+
+<p>For more information <a href="mailto:da%20[AT]%20uxebu.com">contact me</a> via mail, follow me <a href="http://twitter.com/void_0">on twitter (@void_0)</a> and watch out for the TouchScroll blog post on <a href="http://uxebu.com/blog">our blog</a>.</p>
+
+ </div>
+ <div class="scroller" id="horiz">
+ <h2>Yet to be done</h2>
+ </div>
+ <div class="scroller" id="twodim">
+ <h2>Yet to be done</h2>
+ </div>
+
+ <div class="tabs">
+ <a href="#about" class="current">About</a>
+ <a href="#horiz">Horizontal</a>
+ <a href="#twodim">2-Dimensional</a>
+ </div>
+ </div>
+</div>
+
+<script src="../dist/touchscroll.min.js"></script>
+<script>
+(function(){
+ scrollers = {};
+
+ function showScreen(screenId){
+ try{
+ var activeScreen = document.querySelector("#demo .scroller#" + screenId);
+ }catch(e){
+ return;
+ }
+
+ if(!activeScreen){ return; }
+
+ Array.prototype.forEach.call(document.querySelectorAll("#demo .scroller"), function(screen){
+ screen.style.display = "none";
+ });
+ activeScreen.style.display = "block";
+
+ scrollers[screenId].setupScroller(true);
+ }
+
+ Array.prototype.forEach.call(document.querySelectorAll("#demo .scroller"), function(scroller){
+ scrollers[scroller.id] = new TouchScroll(scroller, {elastic: true});
+ });
+
+ document.querySelector("#demo .tabs").addEventListener("click", function(event){
+ var screenId = event.target.getAttribute("href");
+ if(screenId){
+ showScreen(screenId.slice(1));
+
+ Array.prototype.forEach.call(this.children, function(tab){
+ tab.className = "";
+ });
+ event.target.className = "current";
+ }
+ }, false);
+
+ var hash = location.hash.slice(1);
+ showScreen(hash || "about");
+ tab = document.querySelector('#demo .tabs [href="' + location.hash + '"]');
+ if(tab){
+ Array.prototype.forEach.call(tab.parentNode.children, function(tab){
+ tab.className = "";
+ });
+ tab.className = "current";
+ }
+}())
+</script>
+
+</body>
+</html>
115 dist/touchscroll.min.js
@@ -0,0 +1,115 @@
+/*
+ Copyright (c) 2010 uxebu Consulting Ltd. & Co. KG
+ Copyright (c) 2010 David Aurelio
+ Copyright (C) 2008 Apple Inc.
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+*/
+var TouchScroll=function(){
+
+//
+// SCROLLER CONFIGURATION
+//
+var config = {
+ // the minimum move distance to trigger scrolling (in pixels)
+ threshold: 5,
+
+ // minimum scroll handle size
+ scrollHandleMinSize: 25,
+
+ // flicking detection and configuration
+ flicking: {
+ // longest duration between last touchmove and the touchend event to trigger flicking
+ triggerThreshold: 150,
+
+ // the friction factor (per milisecond).
+ // This factor is used to precalculate the flick length. Lower numbers
+ // make flicks decelerate earlier.
+ friction: 0.998,
+
+ // minimum speed needed before the animation stop (px/ms)
+ // This value is used to precalculate the flick length. Larger numbers
+ // lead to shorter flicking lengths and durations
+ minSpeed: 0.15,
+
+ // the timing function for flicking animinations (control points for a cubic bezier curve)
+ timingFunc: [0, 0.3, 0.6, 1]
+ },
+
+ // bouncing configuration
+ elasticity: {
+ // factor for the bounce length while dragging
+ factorDrag: 0.5,
+
+ // factor for the bounce length while flicking
+ factorFlick: 0.2,
+
+ // maximum bounce (in px) when flicking
+ max: 100
+ },
+
+ // snap back configuration
+ snapBack: {
+ // the timing function for snap back animations (control points for a cubic bezier curve)
+ // when bouncing out before, the first control point is overwritten to achieve a smooth
+ // transition between bounce and snapback.
+ timingFunc: [0.4, 0, 1, 1],
+
+ // default snap back time
+ defaultTime: 250,
+
+ // whether the snap back effect always uses the default time or
+ // uses the bounce out time.
+ alwaysDefaultTime: true
+ }
+};
+
+function i(a,b,c,d){if(!(a>=0&&a<=1))throw new RangeError("'p1x' must be a number between 0 and 1. Got "+a+"instead.");if(!(b>=0&&b<=1))throw new RangeError("'p1y' must be a number between 0 and 1. Got "+b+"instead.");if(!(c>=0&&c<=1))throw new RangeError("'p2x' must be a number between 0 and 1. Got "+c+"instead.");if(!(d>=0&&d<=1))throw new RangeError("'p2y' must be a number between 0 and 1. Got "+d+"instead.");this._p1={x:a,y:b};this._p2={x:c,y:d}}function t(a){a.style.webkitTransformStyle=
+"preserve-3d";a.style.webkitTransitionProperty="-webkit-transform"}function n(a,b,c,d){var e=a.style;if(c!=null)e.webkitTransitionDuration=c+"";if(d!=null)e.webkitTransitionTimingFunction=d+"";a.style.webkitTransform="translate("+b.e+"px, "+b.f+"px)"}function y(a){if(a.touches&&a.touches.length)a=a.touches[0];var b=new WebKitCSSMatrix;b.e=a.pageX;b.f=a.pageY;return b}function z(a){a.e=Math.round(a.e);a.f=Math.round(a.f);return a}function q(a,b){b=b||{};this.elastic=!!b.elastic;this.scrollers={container:a,
+outer:null,inner:null,e:null,f:null};this._scrolls={e:false,f:false};this._scrollMin={e:false,f:false};this._scrollbars=null;this._isScrolling=false;this._startEvent=null;this._currentOffset=new WebKitCSSMatrix;this._trackedEvents=null;this._flicking={e:false,f:false};this._bounces={e:null,f:null};this._animationTimeouts={e:[],f:[]};this._initDom();this.setupScroller()}i.prototype._getCoordinateForT=function(a,b,c){var d=3*b;b=3*(c-b)-d;return(((1-d-b)*a+b)*a+d)*a};i.prototype._getCoordinateDerivateForT=
+function(a,b,c){var d=3*b;b=3*(c-b)-d;return(3*(1-d-b)*a+2*b)*a+d};i.prototype._getTForCoordinate=function(a,b,c,d){if(!isFinite(d)||d<=0)throw new RangeError("'epsilon' must be a number greater than 0.");for(var e=a,f=0,h,g;f<8;f++){h=this._getCoordinateForT(e,b,c)-a;if(Math.abs(h)<d)return e;g=this._getCoordinateDerivateForT(e,b,c);if(Math.abs(g)<1.0E-6)break;e-=h/g}e=a;f=0;g=1;if(e<f)return f;if(e>g)return g;for(;f<g;){h=this._getCoordinateForT(e,b,c);if(Math.abs(h-a)<d)return e;if(a>h)f=e;else g=
+e;e=(g-f)*0.5+f}return e};i.prototype.getPointForT=function(a){if(a==0||a==1)return{x:a,y:a};else if(!(a>0)||!(a<1))throw new RangeError("'t' must be a number between 0 and 1Got "+a+" instead.");return{x:this._getCoordinateForT(a,this._p1.x,this._p2.x),y:this._getCoordinateForT(a,this._p1.y,this._p2.y)}};i.prototype.getTforX=function(a,b){return this._getTForCoordinate(a,this._p1.x,this._p2.x,b)};i.prototype.getTforY=function(a,b){return this._getTForCoordinate(a,this._p1.y,this._p2.y,b)};i.prototype._getAuxPoints=
+function(a){if(!(a>0)||!(a<1))throw new RangeError("'t' must be greater than 0 and lower than 1");var b={x:a*this._p1.x,y:a*this._p1.y},c={x:this._p1.x+a*(this._p2.x-this._p1.x),y:this._p1.y+a*(this._p2.x-this._p1.y)},d={x:this._p2.x+a*(1-this._p2.x),y:this._p2.y+a*(1-this._p2.y)},e={x:b.x+a*(c.x-b.x),y:b.y+a*(c.y-b.y)},f={x:c.x+a*(d.x-c.x),y:c.y+a*(d.y-c.y)};return{i0:b,i1:c,i2:d,j0:e,j1:f,k:{x:e.x+a*(f.x-e.x),y:e.y+a*(f.y-e.y)}}};i.prototype.divideAtT=function(a){if(a<0||a>1)throw new RangeError("'t' must be a number between 0 and 1. Got "+
+a+" instead.");if(a===0||a===1){var b=[];b[a]=i.linear();b[1-a]=this.clone();return b}b={};var c={},d=this._getAuxPoints(a);a=d.i0;var e=d.i2,f=d.j0,h=d.j1,g=d.k;d=g.x;g=g.y;b.p1={x:a.x/d,y:a.y/g};b.p2={x:f.x/d,y:f.y/g};c.p1={x:(h.x-d)/(1-d),y:(h.y-g)/(1-g)};c.p2={x:(e.x-d)/(1-d),y:(e.y-g)/(1-g)};return[new i(b.p1.x,b.p1.y,b.p2.x,b.p2.y),new i(c.p1.x,c.p1.y,c.p2.x,c.p2.y)]};i.prototype.divideAtX=function(a,b){if(a<0||a>1)throw new RangeError("'x' must be a number between 0 and 1. Got "+a+" instead.");
+return this.divideAtT(this.getTforX(a,b))};i.prototype.divideAtY=function(a,b){if(a<0||a>1)throw new RangeError("'y' must be a number between 0 and 1. Got "+a+" instead.");return this.divideAtT(this.getTforY(a,b))};i.prototype.clone=function(){return new i(this._p1.x,this._p1.y,this._p2.x,this._p2.y)};i.prototype.toString=function(){return"cubic-bezier("+[this._p1.x,this._p1.y,this._p2.x,this._p2.y].join(", ")+")"};i.linear=function(){return new i};i.ease=function(){return new i(0.25,0.1,0.25,1)};
+i.linear=function(){return new i(0,0,1,1)};i.easeIn=function(){return new i(0.42,0,1,1)};i.easeOut=function(){return new i(0,0,0.58,1)};i.easeInOut=function(){return new i(0.42,0,0.58,1)};var C=function(){if("createTouch"in document)return true;try{return!!document.createEvent("TouchEvent").initTouchEvent}catch(a){return false}}(),D=function(){var a=new WebKitCSSMatrix("matrix(1, 0, 0, 1, -20, -30)");return a.e==-20&&a.f==-30}(),r;r=C?{start:"touchstart",move:"touchmove",end:"touchend",cancel:"touchcancel"}:
+{start:"mousedown",move:"mousemove",end:"mouseup",cancel:"touchcancel"};var u;if(D)u=function(a){a=window.getComputedStyle(a).webkitTransform;return new WebKitCSSMatrix(a)};else{var E=/matrix\(\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*\,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/;u=function(a){var b=window.getComputedStyle(a).webkitTransform;a=new WebKitCSSMatrix;if(b=E.exec(b)){a.e=b[1];a.f=b[2]}return a}}var A=document.createElement("div");A.innerHTML=
+'<div class="touchScrollTrack touchScrollTrackX"><div class="touchScrollHandle"></div></div><div class="touchScrollTrack touchScrollTrackY"><div class="touchScrollHandle"></div></div>';q.handleEvent=function(a){var b=this.prototype.currentScroller;if(b)b.handleEvent(a);else a.type===r.move&&a.preventDefault()};document.addEventListener(r.move,q,false);document.addEventListener(r.end,q,false);document.addEventListener(r.cancel,q,false);q.prototype={currentScroller:null,handlerNames:{resize:"setupScroller",
+orientationchange:"setupScroller",webkitTransitionEnd:"onTransitionEnd",DOMSubtreeModified:"setupScroller",touchstart:"onTouchStart",mousedown:"onTouchStart",touchmove:"onTouchMove",mousemove:"onTouchMove",touchend:"onTouchEnd",mouseup:"onTouchEnd",touchcancel:"onTouchEnd"},_initDom:function(){var a=document.createElement("div"),b=a.cloneNode(false),c=this.scrollers.container;a.className="touchScrollInner";c.className+=" touchScroll";for(var d=0,e=c.childNodes.length;d<e;d++)a.appendChild(c.firstChild);
+b.appendChild(a);c.appendChild(b);this.scrollers.outer=this.scrollers.f=b;this.scrollers.inner=this.scrollers.e=a;t(b);t(a);a=A.cloneNode(true);d=a.querySelector(".touchScrollTrackX");e=a.querySelector(".touchScrollTrackY");var f=d.firstElementChild,h=e.firstElementChild,g=a.style;g.pointerEvents="none";g.webkitTransitionProperty="opacity";g.webkitTransitionDuration="250ms";g.opacity="0";this._scrollbars={container:a,tracks:{e:d,f:e},handles:{e:f,f:h},sizes:{e:0,f:0}};t(f);t(h);c.insertBefore(a,b);
+if(window.getComputedStyle(c).position=="static")c.style.position="relative";this.setupScroller();c.addEventListener(r.start,this,false);b.addEventListener("webkitTransitionEnd",this,false);b.addEventListener("DOMSubtreeModified",this,true);window.addEventListener("orientationchange",this,false);window.addEventListener("resize",this,false)},setupScroller:function(){var a=this.scrollers.outer.parentNode;a={e:a.offsetWidth,f:a.offsetHeight};var b=this.scrollers.inner;b={e:b.offsetWidth,f:b.offsetHeight};
+var c=this._scrollbars,d={e:a.e-b.e,f:a.f-b.f};c.container.style.height=a.f+"px";this._scrollMin=d;var e=this._scrolls={e:d.e<0,f:d.f<0};this._doScroll=e.e||e.f;c.container.className="touchScrollBars";if(e.e&&e.f)c.container.className+=" touchScrollBarsBoth";c.tracks.e.style.display=e.e?"":"none";c.tracks.f.style.display=e.f?"":"none";var f={e:c.tracks.e.offsetWidth,f:c.tracks.f.offsetHeight};c.sizes={e:Math.round(Math.max(f.e*a.e/b.e,config.scrollHandleMinSize)),f:Math.round(Math.max(f.f*a.f/b.f,
+config.scrollHandleMinSize))};c.handles.e.style.width=c.sizes.e+"px";c.handles.f.style.height=c.sizes.f+"px";c.maxOffsets={e:f.e-c.handles.e.offsetWidth,f:f.f-c.handles.f.offsetHeight};c.offsetRatios={e:e.e?(f.e-c.handles.e.offsetWidth)/d.e:0,f:e.f?(f.f-c.handles.f.offsetHeight)/d.f:0}},handleEvent:function(a){var b=this.handlerNames[a.type];b&&this[b](a)},onTouchStart:function(a){if(this._doScroll){this.__proto__.currentScroller=this;this._isScrolling=false;this._trackedEvents=[];this._determineOffset();
+this._trackEvent(a);this._startEventTarget=a.target;this._stopAnimations();this._startEvent=a;a.stopPropagation();a.preventDefault()}},onTouchMove:function(a){if(this._doScroll){var b=this._trackedEvents[1].matrix;b=y(a).translate(-b.e,-b.f,0);var c=this._isScrolling,d=c;a.stopPropagation();a.preventDefault();if(!d){d=config.threshold;d=b.e<=-d||b.e>=d||b.f<=-d||b.f>=d}if(d){if(!c){this._isScrolling=true;this.showScrollbars()}this._scrollBy(b);this._trackEvent(a)}}},onTouchEnd:function(a){var b=this._startEventTarget;
+if(!this._isScrolling&&b==a.target){for(b=a.target;b.nodeType!=1;)b=b.parentNode;var c=document.createEvent("HTMLEvents");c.initEvent("focus",false,false);b.dispatchEvent(c);c=document.createEvent("MouseEvent");c.initMouseEvent("click",true,true,a.view,1,a.screenX,a.screenY,a.clientX,a.clientY,a.ctrlKey,a.altKey,a.shiftKey,a.metaKey,a.button,null);b.dispatchEvent(c)}else if(this._isScrolling){c=this._getLastMove();if(c.duration<=config.flicking.triggerThreshold&&c.length){a=this._getFlickingDuration(c.speed);
+var d=this._getFlickingLength(c.speed,a);b=c.matrix;c=d/c.length;b.e*=c;b.f*=c;this.startFlick(b,a)}}this.isAnimating()||this._snapBack()||this.hideScrollbars();delete this._startEventTarget;this._isScrolling=false;this.__proto__.currentScroller=null},onTransitionEnd:function(a){["e","f"].forEach(function(b){if(a.target===this.scrollers[b])this._flicking[b]=false},this);this.isAnimating()||this.hideScrollbars()},isAnimating:function(){var a=this._animationTimeouts,b=this._flicking.e||this._flicking.f;
+return a.e.length>0||a.f.length>0||b},scrollBy:function(a,b){var c=new WebKitCSSMatrix;c.e=-a;c.f=-b;this._scrollBy(c)},_scrollBy:function(a){var b=this._scrolls;if(!b.e)a.e=0;if(!b.f)a.f=0;var c=this._scrollMin,d=this._currentOffset;b=d.multiply(a);var e={e:false,f:false},f={e:0,f:0};if(this.elastic){var h=config.elasticity.factorDrag,g={e:d.e<c.e||d.e>0,f:d.f<c.f||d.f>0};if(g.e)b.e-=a.e*(1-h);if(g.f)b.f-=a.f*(1-h);if(b.e<c.e||b.e>0){e.e=true;f.e=b.e>=0?b.e:c.e-b.e}if(b.f<c.f||b.f>0){e.f=true;f.f=
+b.f>=0?b.f:c.f-b.f}a={e:(!g.e||!e.e)&&(e.e||e.e),f:(!g.f||!e.f)&&(e.f||e.f)};if(a.e)if(d.e>0)b.e/=h;else if(b.e>0)b.e*=h;else if(d.e<c.e)b.e+=(c.e-d.e)/h;else if(b.e<c.e)b.e-=(c.e-b.e)*h;if(a.f)if(d.f>0)b.f/=h;else if(b.f>0)b.f*=h;else if(d.f<c.f)b.f+=(c.f-d.f)/h;else if(b.f<c.f)b.f-=(c.f-b.f)*h}else{if(b.e<c.e)b.e=c.e;else if(b.e>0)b.e=0;if(b.f<c.f)b.f=c.f;else if(b.f>0)b.f=0}this._currentOffset=b;a=b.translate(0,0,0);c=b.translate(0,0,0);a.f=c.e=0;n(this.scrollers.e,a);n(this.scrollers.f,c);d=this._scrollbars.offsetRatios;
+a.e*=d.e;c.f*=d.f;e.e?this._squeezeScrollbar("e",f.e,b.e<0):n(this._scrollbars.handles.e,a);e.f?this._squeezeScrollbar("f",f.f,b.f<0):n(this._scrollbars.handles.f,c)},_trackEvent:function(a){var b=this._trackedEvents;b[0]=b[1];b[1]={matrix:y(a),timeStamp:a.timeStamp}},showScrollbars:function(){var a=this._scrollbars.container.style;a.webkitTransitionDuration="";a.opacity="1"},hideScrollbars:function(){var a=this._scrollbars.container.style;a.webkitTransitionDuration="250ms";a.opacity="0"},_squeezeScrollbar:function(a,
+b,c,d,e){var f=this._scrollbars,h=f.handles[a].style,g=f.sizes[a];b=Math.max(g-b,1);var v=new WebKitCSSMatrix;v[a]=c?f.maxOffsets[a]:0;v[a=="f"?"d":"a"]=b/g;h.webkitTransformOrigin=c?"100% 100%":"0 0";h.webkitTransitionProperty="-webkit-transform";h.webkitTransform=v;if(d){h.webkitTransitionDuration=d+"ms";h.webkitTransitionTimingFunction=e;this._animationTimeouts[a].push(setTimeout(function(){h.webkitTransitionDuration=""},d))}else h.webkitTransitionDuration=""},_determineOffset:function(a){var b=
+u(this.scrollers.e),c=u(this.scrollers.f);b=b.multiply(c);a&&z(b);this._currentOffset=b},_stopAnimations:function(){var a=false,b=this._scrollbars;["e","f"].forEach(function(c){this.scrollers[c].style.webkitTransitionDuration="";var d=b.handles[c];d.style.webkitTransitionDuration="";t(d);b.tracks[c].style.webkitBoxPack="";c=this._animationTimeouts[c];a=a||c.length;c.forEach(function(e){clearTimeout(e)});c.length=0},this);this._determineOffset(true);this.scrollBy(0,0);this._bounces.e=this._bounces.f=
+null;return a},_getLastMove:function(){var a=this._trackedEvents,b=a[0],c=a[1];if(!b)return{duration:0,matrix:new WebKitCSSMatrix,length:0,speed:0};a=c.timeStamp-b.timeStamp;b=c.matrix.multiply(b.matrix.inverse());c=Math.sqrt(b.e*b.e+b.f*b.f);return{duration:a,matrix:b,length:c,speed:c/a}},_getFlickingDuration:function(a){a=Math.log(config.flicking.minSpeed/a)/Math.log(config.flicking.friction);return a>0?Math.round(a):0},_getFlickingLength:function(a,b){b=(1-Math.pow(config.flicking.friction,b+1))/
+(1-config.flicking.friction);return a*b},startFlick:function(a,b){if(b){var c=1/b,d=config.flicking.timingFunc,e=new i(d[0],d[1],d[2],d[3]),f=this._scrollMin,h=this._currentOffset,g=this._scrollbars;z(a);if(!this._scrolls.e)a.e=0;if(!this._scrolls.f)a.f=0;var v=this._currentOffset.multiply(a);["e","f"].forEach(function(j){var o=a[j],k=v[j],p=1;if(k<f[j])p=1-Math.max(Math.min((k-f[j])/a[j],1),0);else if(k>0)p=1-Math.max(Math.min((k-0)/a[j],1),0);var l=p*o;o=o-l;if(l||o){k=e.getTforY(p,c);p=e.getPointForT(k).x;
+var s=e.divideAtT(k),m=new WebKitCSSMatrix;m[j]=h[j];k=p*b;if(l&&p){this._flicking[j]=true;m[j]+=l;n(this.scrollers[j],m,k+"ms",s[0]);l=m.translate(0,0,0);l[j]*=g.offsetRatios[j];n(g.handles[j],l,k+"ms",s[0])}if(this.elastic&&o){l=m.translate(0,0,0);s=s[1];s._p2={x:1-config.snapBack.timingFunc[0],y:1-config.snapBack.timingFunc[1]};m=Math.min(config.elasticity.factorFlick,config.elasticity.max/Math.abs(o));l[j]+=o*m;var w=(1-p)*b*m;this._bounces[j]={timingFunc:s,duration:w+"ms",matrix:l,bounceLength:Math.abs(o*
+m)};var B=this,x=this._animationTimeouts[j];x.push(setTimeout(function(){B._playQueuedBounce(j)},k));x.push(setTimeout(function(){B._snapBack(j,config.snapBack.alwaysDefaultTime?null:w);x.length=0},k+w))}}else this._snapBack(j)},this)}},_playQueuedBounce:function(a){var b=this._bounces[a];if(b){var c=b.matrix,d=b.duration,e=b.timingFunc;n(this.scrollers[a],c,d,e);this._squeezeScrollbar(a,b.bounceLength,c[a]<0,d,e);this._bounces[a]=null;return true}return false},_snapBack:function(a,b){if(a==null){a=
+this._snapBack("e",b);b=this._snapBack("f",b);return a||b}b=b||config.snapBack.defaultTime;var c=this.scrollers[a],d=u(c),e=config.snapBack.timingFunc;e=new i(e[0],e[1],e[2],e[3]);if(d[a]<this._scrollMin[a]||d[a]>0){d[a]=Math.max(Math.min(d[a],0),this._scrollMin[a]);this._squeezeScrollbar(a,0,d[a]<0,b,e);n(c,d,b+"ms",e);return true}return false}};return q}();
65 src/touchscroll.css
@@ -0,0 +1,65 @@
+.touchScrollBars {
+ display: -webkit-box;
+ padding: 3px;
+ position: relative;
+ float: left;
+ margin-left: -100%;
+ width: 100%;
+ z-index: 2147483647;
+ -webkit-transform: translateX(100%);
+ -webkit-box-align: end;
+ -webkit-box-pack: end;
+ -webkit-box-sizing: border-box;
+}
+
+.touchScrollTrack {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
+
+.touchScrollTrackX {
+ height: 6px;
+}
+
+.touchScrollTrackY {
+ float: right;
+ width: 6px;
+}
+
+.touchScrollBarsBoth {
+ padding-bottom: 12px;
+ padding-right: 12px;
+}
+
+.touchScrollBarsBoth .touchScrollTrackX {
+ -webkit-transform: translateY(9px);
+}
+
+.touchScrollBarsBoth .touchScrollTrackY {
+ -webkit-transform: translateX(3px);
+}
+
+.touchScrollHandle {
+ border: 1px solid;
+ min-height: 4px;
+ min-width: 4px;
+ -webkit-transition-property: -webkit-transform, height, width;
+ -webkit-border-radius: 3px;
+
+ /* COLOR VARIANTS */
+/* Default */
+ border-color: rgba(255, 255, 255, 0.15);
+ background: rgba(0, 0, 0, 0.5);
+/**/
+
+/* White
+ border-color: rgba(255, 255, 255, 0.5);
+ background: rgba(255, 255, 255, 0.5);
+/**/
+
+/* Black
+ border-color: rgba(0, 0, 0, 0.5);
+ background: rgba(0, 0, 0, 0.5);
+/**/
+}
1,104 src/touchscroll.js
@@ -0,0 +1,1104 @@
+/**
+ @license
+
+ Copyright (c) 2010 uxebu Consulting Ltd. & Co. KG
+ Copyright (c) 2010 David Aurelio
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+*/
+var TouchScroll = (function(){
+ //
+ // SCROLLER CONFIGURATION
+ //
+ var config = {
+ // the minimum move distance to trigger scrolling (in pixels)
+ threshold: 5,
+
+ // minimum scroll handle size
+ scrollHandleMinSize: 25,
+
+ // flicking detection and configuration
+ flicking: {
+ // longest duration between last touchmove and the touchend event to trigger flicking
+ triggerThreshold: 150,
+
+ // the friction factor (per milisecond).
+ // This factor is used to precalculate the flick length. Lower numbers
+ // make flicks decelerate earlier.
+ friction: 0.998,
+
+ // minimum speed needed before the animation stop (px/ms)
+ // This value is used to precalculate the flick length. Larger numbers
+ // lead to shorter flicking lengths and durations
+ minSpeed: 0.15,
+
+ // the timing function for flicking animinations (control points for a cubic bezier curve)
+ timingFunc: [0, 0.3, 0.6, 1]
+ },
+
+ // bouncing configuration
+ elasticity: {
+ // factor for the bounce length while dragging
+ factorDrag: 0.5,
+
+ // factor for the bounce length while flicking
+ factorFlick: 0.2,
+
+ // maximum bounce (in px) when flicking
+ max: 100
+ },
+
+ // snap back configuration
+ snapBack: {
+ // the timing function for snap back animations (control points for a cubic bezier curve)
+ // when bouncing out before, the first control point is overwritten to achieve a smooth
+ // transition between bounce and snapback.
+ timingFunc: [0.4, 0, 1, 1],
+
+ // default snap back time
+ defaultTime: 250,
+
+ // whether the snap back effect always uses the default time or
+ // uses the bounce out time.
+ alwaysDefaultTime: true
+ }
+ };
+
+ //
+ // FEATURE DETECTION
+ //
+ /* Determine touch events support */
+ var hasTouchSupport = (function(){
+ if("createTouch" in document){ // True on the iPhone
+ return true;
+ }
+ try{
+ var event = document.createEvent("TouchEvent"); // Should throw an error if not supported
+ return !!event.initTouchEvent; // Check for existance of initialization method
+ }catch(error){
+ return false;
+ }
+ }());
+
+ /*
+ In some older versions of Android, WebKitCSSMatrix is broken and does
+ not parse a "matrix" directive properly.
+ */
+ var parsesMatrixCorrectly = (function(){
+ var m = new WebKitCSSMatrix("matrix(1, 0, 0, 1, -20, -30)");
+ return m.e == -20 && m.f == -30;
+ }());
+
+ //
+ // FEATURE BASED CODE BRANCHING
+ //
+
+ /* Define event names */
+ var events;
+ if(hasTouchSupport){
+ events = {
+ start: "touchstart",
+ move: "touchmove",
+ end: "touchend",
+ cancel: "touchcancel"
+ };
+ }else{
+ events = {
+ start: "mousedown",
+ move: "mousemove",
+ end: "mouseup",
+ cancel: "touchcancel" // unnecessary here
+ };
+ }
+
+ var getMatrixFromNode;
+ if(parsesMatrixCorrectly){
+ getMatrixFromNode = function(/*HTMLElement*/node){ /*WebKitCSSMatrix*/
+ var doc = node.ownerDocument,
+ transform = window.getComputedStyle(node).webkitTransform;
+
+ return new WebKitCSSMatrix(transform);
+ }
+ }else{
+ var reMatrix = /matrix\(\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*\,\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/;
+ getMatrixFromNode = function(/*HTMLElement*/node){ /*WebKitCSSMatrix*/
+ var doc = node.ownerDocument,
+ transform = window.getComputedStyle(node).webkitTransform,
+ matrix = new WebKitCSSMatrix(),
+ match = reMatrix.exec(transform);
+
+ if(match){
+ matrix.e = match[1];
+ matrix.f = match[2];
+ }
+
+ return matrix;
+ }
+ }
+
+ //
+ // UTILITY FUNCTIONS
+ //
+ function setTransitionProperty(/*HTMLElement*/node){
+ node.style.webkitTransformStyle = "preserve-3d";
+ node.style.webkitTransitionProperty = "-webkit-transform";
+ };
+
+ function applyMatrixToNode(/*HTMLElement*/node,
+ /*WebKitCSSMatrix*/matrix,
+ /*String?*/duration,
+ /*String?*/timingFunc){
+ var s = node.style;
+ if(duration != null){
+ s.webkitTransitionDuration = duration + "";
+ }
+ if(timingFunc != null){
+ s.webkitTransitionTimingFunction = timingFunc + "";
+ }
+
+ // This is twice as fast as than directly assigning the matrix
+ // to the style property (maybe because no function call is involved?).
+ node.style.webkitTransform = "translate(" + matrix.e + "px, " + matrix.f + "px)";
+ }
+
+ function getMatrixFromEvent(event){ /*WebKitCSSMatrix*/
+ if(event.touches && event.touches.length){
+ event = event.touches[0];
+ }
+
+ var matrix = new WebKitCSSMatrix;
+ matrix.e = event.pageX;
+ matrix.f = event.pageY;
+
+ return matrix;
+ };
+
+ function roundMatrix(/*WebKitCSSMatrix*/matrix){ /*WebKitCSSMatrix*/
+ matrix.e = Math.round(matrix.e);
+ matrix.f = Math.round(matrix.f);
+ return matrix;
+ }
+
+ // A DOM node to clone for scrollbars
+ var scrollbarsTemplate = document.createElement("div");
+ scrollbarsTemplate.innerHTML = [
+ '<div class="touchScrollTrack touchScrollTrackX">',
+ '<div class="touchScrollHandle"></div>',
+ '</div>',
+ '<div class="touchScrollTrack touchScrollTrackY">',
+ '<div class="touchScrollHandle"></div>',
+ '</div>'
+ ].join("");
+
+/*
+ === TOUCH CONTROLLER ======================================================
+ Does the actual work.
+
+ The event handling is divided into two parts:
+ The scroller constructor tracks "move", "end", and "cancel" events and
+ delegates them to the currently active scroller, if any.
+
+ Every single scroller only listens for the "start" event on its outer node,
+ and sets itself as the currently active scroller.
+*/
+
+/*
+ Every object with a "handleEvent" method can be registered as DOM Level 2
+ event listener. On event, the method is called on the registered object.
+*/
+TouchScroll.handleEvent = function handleEvent(event){
+ var currentScroller = this.prototype.currentScroller;
+ if(currentScroller){
+ currentScroller.handleEvent(event);
+ }else if(event.type === events.move){ // always cancel move events at this point
+ event.preventDefault();
@deleteme
deleteme added a note

This event.preventDefault() line prevents normal browser scrolling from occurring. Why 'always cancel move events at this point'?

@davidaurelio Owner

That was the intention when I wrote that line :-) Our usecase was to replace native scrolling completely. I can make it configurable, though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ }
+};
+
+/*
+ Listening to end, move, and cancel event.
+ Event listening takesplace during bubbling, so other scripts can cancel
+ scrolling by simply stopping event propagation.
+*/
+document.addEventListener(events.move, TouchScroll, false);
+document.addEventListener(events.end, TouchScroll, false);
+document.addEventListener(events.cancel, TouchScroll, false);
+
+/**
+ Constructor for scrollers.
+
+ @constructor
+ @param {HTMLElement} scrollElement The node to make scrollable
+ @param {Object} [options] Options for the scroller- Known options are
+ elastic {Boolean} whether the scroller bounces
+*/
+function TouchScroll(/*HTMLElement*/scrollElement, /*Object*/options){
+ options = options || {};
+ this.elastic = !!options.elastic,
+
+ // references to scroll div elements
+ this.scrollers = {
+ container: scrollElement,
+ outer: /*HTMLElement*/null,
+ inner: /*HTMLElement*/null,
+ e: /*HTMLElement*/null,
+ f: /*HTMLElement*/null
+ };
+
+ // Whether the scroller scrolls
+ this._scrolls = {e: false, f: false};
+
+ // The minimal scroll values (fully scrolled to the bottom/right)
+ // Object with attributes "e" and "f"
+ this._scrollMin = {e: false, f: false};
+
+ // References DOM nodes for scrollbar tracks and handles.
+ // Gets set up by "_initDom"
+ // {
+ // container: HTMLElement,
+ // handles:{e: HTMLElement, f: HTMLElement},
+ // maxOffsets: {e: Number, f: Number}, -> maximum offsets for the handles
+ // offsetRatios: {e: Number, f: Number}, -> Ratio of scroller offset to handle offset
+ // sizes: {e: Number, f: Number}, -> handle sizes
+ // tracks: {e: HTMLElement, f: HTMLElement},
+ // }
+ this._scrollbars = null,
+
+
+ /* ---- SCROLLER STATE ---- */
+
+ this._isScrolling = false;
+
+ this._startEvent = null;
+
+ // the current scroller offset
+ this._currentOffset = new WebKitCSSMatrix();
+
+ // Events tracked during a move action
+ // [ {timeStamp: Number, matrix: WebKitCSSMatrix} ]
+ // The last two events get tracked.
+ this._trackedEvents = /*Array*/null;
+
+ // Keeps track whether flicking is active
+ this._flicking = {e: false, f: false};
+
+ // Queued bounces
+ this._bounces = {e: null, f: null};
+
+ // Animation timeouts
+ // This implementation uses timeouts for combined animations,
+ // because the webkitTransitionEnd event fires late on iPhone 3G
+ this._animationTimeouts = {e: [], f: []};
+
+ this._initDom();
+ this.setupScroller();
+}
+
+TouchScroll.prototype = {
+ // references the currently active scroller
+ // static property!
+ currentScroller: null,
+
+ // Maps event types to method names.
+ handlerNames: {
+ resize: "setupScroller",
+ orientationchange: "setupScroller",
+ webkitTransitionEnd: "onTransitionEnd",
+ DOMSubtreeModified: "setupScroller",
+
+ touchstart: "onTouchStart",
+ mousedown: "onTouchStart",
+ touchmove: "onTouchMove",
+ mousemove: "onTouchMove",
+ touchend: "onTouchEnd",
+ mouseup: "onTouchEnd",
+ touchcancel: "onTouchEnd"
+ },
+
+ // Does DOM initialization needed for the scroller
+ _initDom: function initDom(){
+ // wrap the scroller contents with two additional <div> elements
+ var innerScroller = document.createElement("div"),
+ outerScroller = innerScroller.cloneNode(false),
+ parentNode = this.scrollers.container;
+
+ innerScroller.className = "touchScrollInner";
+ parentNode.className += " touchScroll";
+
+ for(var i = 0, iMax = parentNode.childNodes.length; i < iMax; i++){
+ innerScroller.appendChild(parentNode.firstChild);
+ }
+
+ outerScroller.appendChild(innerScroller);
+ parentNode.appendChild(outerScroller);
+
+ this.scrollers.outer = this.scrollers.f = outerScroller;
+ this.scrollers.inner = this.scrollers.e = innerScroller;
+
+ // init scroll layers for transitions
+ setTransitionProperty(outerScroller);
+ setTransitionProperty(innerScroller);
+
+ // add scrollbars
+ var scrollbarsNode = scrollbarsTemplate.cloneNode(true),
+ trackE = scrollbarsNode.querySelector(".touchScrollTrackX"),
+ trackF = scrollbarsNode.querySelector(".touchScrollTrackY"),
+ handleE = trackE.firstElementChild,
+ handleF = trackF.firstElementChild;
+
+
+ var style = scrollbarsNode.style;
+ style.pointerEvents = "none"; // make clicks/touches on scrollbars "fall through"
+ style.webkitTransitionProperty = "opacity";
+ style.webkitTransitionDuration = "250ms";
+ style.opacity = "0";
+
+ var scrollbars = this._scrollbars = {
+ container: scrollbarsNode,
+ tracks: {
+ e: trackE,
+ f: trackF
+ },
+ handles: {
+ e: handleE,
+ f: handleF
+ },
+ sizes : {e: 0, f: 0}
+ }
+
+ setTransitionProperty(handleE);
+ setTransitionProperty(handleF);
+
+ parentNode.insertBefore(scrollbarsNode, outerScroller);
+
+ /*
+ Apply relative positioning to the scrolling container.
+ This is needed to enable scrollbar positioning.
+ */
+ if(window.getComputedStyle(parentNode).position == "static"){
+ parentNode.style.position = "relative";
+ }
+
+ this.setupScroller();
+
+ // initialize event listeners
+ parentNode.addEventListener(events.start, this, false);
+ outerScroller.addEventListener("webkitTransitionEnd", this, false);
+ outerScroller.addEventListener("DOMSubtreeModified", this, true);
+ window.addEventListener("orientationchange", this, false);
+ window.addEventListener("resize", this, false);
+ },
+
+ setupScroller: function setupScroller(debug){
+ var scrollContainer = this.scrollers.outer.parentNode,
+ containerSize = {
+ e: scrollContainer.offsetWidth,
+ f: scrollContainer.offsetHeight
+ },
+ innerScroller = this.scrollers.inner,
+ scrollerSize = {
+ e: innerScroller.offsetWidth,
+ f: innerScroller.offsetHeight
+ },
+ scrollbars = this._scrollbars,
+ scrollMin = {
+ e: containerSize.e - scrollerSize.e,
+ f: containerSize.f - scrollerSize.f
+ };
+
+ scrollbars.container.style.height = containerSize.f + "px";
+
+ // Minimum scroll offsets
+ this._scrollMin = scrollMin;
+ var scrolls = this._scrolls = {
+ e: scrollMin.e < 0,
+ f: scrollMin.f < 0
+ };
+
+ this._doScroll = scrolls.e || scrolls.f;
+
+ // scrollbar container class name changes if both scrollbars are visible
+ scrollbars.container.className = "touchScrollBars";
+ if(scrolls.e && scrolls.f){
+ scrollbars.container.className += " touchScrollBarsBoth";
+ }
+
+ // hide/show scrollbars
+ scrollbars.tracks.e.style.display = scrolls.e ? "" : "none";
+ scrollbars.tracks.f.style.display = scrolls.f ? "" : "none";
+
+ var scrollbarTrackSizes = {
+ e: scrollbars.tracks.e.offsetWidth,
+ f: scrollbars.tracks.f.offsetHeight
+ };
+
+ // calculate and apply scroll bar handle sizes
+ scrollbars.sizes = {
+ e: Math.round(Math.max(
+ scrollbarTrackSizes.e * containerSize.e / scrollerSize.e,
+ config.scrollHandleMinSize
+ )),
+ f: Math.round(Math.max(
+ scrollbarTrackSizes.f * containerSize.f / scrollerSize.f,
+ config.scrollHandleMinSize
+ ))
+ };
+ scrollbars.handles.e.style.width = scrollbars.sizes.e + "px";
+ scrollbars.handles.f.style.height = scrollbars.sizes.f + "px";
+
+ // maximum scrollbar offsets
+ scrollbars.maxOffsets = {
+ e: scrollbarTrackSizes.e - scrollbars.handles.e.offsetWidth,
+ f: scrollbarTrackSizes.f - scrollbars.handles.f.offsetHeight
+ };
+
+ // calculate offset ratios
+ // (scroller.offset * offsetratio = scrollhandle.offset)
+ scrollbars.offsetRatios = {
+ e: scrolls.e ? (scrollbarTrackSizes.e - scrollbars.handles.e.offsetWidth) / scrollMin.e : 0,
+ f: scrolls.f ? (scrollbarTrackSizes.f - scrollbars.handles.f.offsetHeight) / scrollMin.f : 0
+ };
+ },
+
+ // Standard DOM Level 2 event handler
+ handleEvent: function handleEvent(event){
+ var handlerName = this.handlerNames[event.type];
+ if(handlerName){
+ this[handlerName](event);
+ }
+ },
+
+ // Handles touch start events on the scroller
+ onTouchStart: function onTouchStart(event){
+ if(!this._doScroll){
+ return;
+ }
+ this.__proto__.currentScroller = this;
+ this._isScrolling = false;
+ this._trackedEvents = [];
+ this._determineOffset();
+ this._trackEvent(event);
+ this._startEventTarget = event.target; // We track this to work around a bug in android, see below
+ var wasAnimating = this._stopAnimations();
+
+ this._startEvent = event;
+
+ event.stopPropagation();
+
+ /*
+ If the scroller was animating, prevent the default action of the event.
+ This prevents clickable elements to be activated accidentally.
+
+ Also, we need to cancel the touchstart event to prevent android from
+ queuing up move events and fire them only when the touch ends.
+ */
+ //if(wasAnimating){
+ event.preventDefault();
+ //}
+
+ },
+
+ // Handles touch move events on the scroller
+ onTouchMove: function onTouchMove(event){
+ if(!this._doScroll){
+ return;
+ }
+
+ // must be present, because touchstart fired before
+ var lastEventOffset = this._trackedEvents[1].matrix,
+ scrollOffset = getMatrixFromEvent(event).translate(
+ -lastEventOffset.e,
+ -lastEventOffset.f,
+ 0
+ ),
+ isScrolling = this._isScrolling,
+ doScroll = isScrolling;
+
+ event.stopPropagation();
+ event.preventDefault();
+
+ if(!doScroll){
+ var threshold = config.threshold,
+ doScroll = scrollOffset.e <= -threshold || scrollOffset.e >= threshold ||
+ scrollOffset.f <= -threshold || scrollOffset.f >= threshold;
+ }
+
+ if(doScroll){
+ if(!isScrolling){
+ this._isScrolling = true;
+ this.showScrollbars();
+ }
+
+ this._scrollBy(scrollOffset);
+ this._trackEvent(event);
+ }
+
+ },
+
+ onTouchEnd: function onTouchEnd(event){
+ var startTarget = this._startEventTarget;
+
+ if(!this._isScrolling && startTarget == event.target){
+ /*
+ If no scroll has been made, the touchend event should trigger
+ a focus and a click (if occurring on the same node as the
+ touchstart event).
+ Unfortunately, we've canceled the touchstart event to work around
+ a bug in android -- so we need to dispatch our own focus and
+ click events.
+ */
+
+
+ var node = event.target;
+ while(node.nodeType != 1){
+ node = node.parentNode;
+ }
+ var focusEvent = document.createEvent("HTMLEvents");
+ focusEvent.initEvent("focus", false, false);
+ node.dispatchEvent(focusEvent);
+ //node.focus();
+
+ var clickEvent = document.createEvent("MouseEvent");
+ clickEvent.initMouseEvent(
+ "click", //type
+ true, //canBubble
+ true, //cancelable
+ event.view,
+ 1, //detail (number of clicks for mouse events)
+ event.screenX,
+ event.screenY,
+ event.clientX,
+ event.clientY,
+ event.ctrlKey,
+ event.altKey,
+ event.shiftKey,
+ event.metaKey,
+ event.button,
+ null// relatedTarget
+ );
+ node.dispatchEvent(clickEvent);
+ }else if(this._isScrolling){
+ var moveSpec = this._getLastMove();
+ if(moveSpec.duration <= config.flicking.triggerThreshold && moveSpec.length){
+ /*
+ If the time between the touchend event and the last tracked
+ event is below threshold, we are triggering a flick.
+ */
+ var flickDuration = this._getFlickingDuration(moveSpec.speed),
+ flickLength = this._getFlickingLength(moveSpec.speed, flickDuration),
+ flickVector = moveSpec.matrix,
+ factor = flickLength / moveSpec.length;
+
+ flickVector.e *= factor;
+ flickVector.f *= factor;
+
+ this.startFlick(flickVector, flickDuration);
+ }
+ }
+
+ if(!(this.isAnimating())){
+ var snappingBack = this._snapBack();
+ if(!snappingBack){
+ this.hideScrollbars();
+ }
+ }
+ delete this._startEventTarget;
+ this._isScrolling = false;
+ this.__proto__.currentScroller = null;
+ },
+
+ onTransitionEnd: function onTransitionEnd(event){
+ ["e", "f"].forEach(function(axis){
+ if(event.target === this.scrollers[axis]){
+ this._flicking[axis] = false;
+ }
+ }, this);
+
+ if(!this.isAnimating()){
+ this.hideScrollbars();
+ }
+ },
+
+ isAnimating: function isAnimating(){
+ var timeouts = this._animationTimeouts;
+ var hasTimeouts = timeouts.e.length > 0 || timeouts.f.length > 0;
+ var isFlicking = this._flicking.e || this._flicking.f;
+ return hasTimeouts || isFlicking;
+ },
+
+ scrollBy: function scrollBy(/*Number*/x, /*Number*/y){
+ var matrix = new WebKitCSSMatrix();
+ matrix.e = -x;
+ matrix.f = -y;
+ this._scrollBy(matrix);
+ },
+
+ // Scrolls the layer by applying a transform matrix to it.
+ //
+ // As this method is called for every touchmove event, the code is rolled
+ // out for both axes (leading to redundancies) to get maximum performance.
+ _scrollBy: function _scrollBy(/*WebKitCSSMatrix*/matrix){
+ var scrolls = this._scrolls;
+ if(!scrolls.e){
+ matrix.e = 0;
+ }
+ if(!scrolls.f){
+ matrix.f = 0;
+ }
+
+ var scrollMin = this._scrollMin,
+ currentOffset = this._currentOffset,
+ newOffset = currentOffset.multiply(matrix),
+ isOutOfBounds = {e: false, f: false}, // whether the new position is out of the scroller bounds
+ scrollbarSizeSubstract = {e: 0, f: 0};
+
+ if(this.elastic){
+ var factor = config.elasticity.factorDrag,
+ wasOutOfBounds = { // whether the scroller was already beyond scroll bounds
+ e: currentOffset.e < scrollMin.e || currentOffset.e > 0,
+ f: currentOffset.f < scrollMin.f || currentOffset.f > 0
+ };
+
+ if(wasOutOfBounds.e){
+ // if out of scroll bounds, apply the elasticity factor
+ newOffset.e -= matrix.e * (1 - factor);
+ }
+ if(wasOutOfBounds.f){
+ newOffset.f -= matrix.f * (1 - factor);
+ }
+
+ if(newOffset.e < scrollMin.e || newOffset.e > 0){
+ isOutOfBounds.e = true;
+ scrollbarSizeSubstract.e = newOffset.e >= 0 ?
+ newOffset.e : scrollMin.e - newOffset.e;
+ }
+ if(newOffset.f < scrollMin.f || newOffset.f > 0){
+ isOutOfBounds.f = true;
+ scrollbarSizeSubstract.f = newOffset.f >= 0 ?
+ newOffset.f : scrollMin.f - newOffset.f;
+ }
+
+ var crossingBounds = { // whether the drag/scroll action went across scroller bounds
+ e: (!wasOutOfBounds.e || !isOutOfBounds.e) && (isOutOfBounds.e || isOutOfBounds.e),
+ f: (!wasOutOfBounds.f || !isOutOfBounds.f) && (isOutOfBounds.f || isOutOfBounds.f)
+ };
+
+
+ if(crossingBounds.e){
+ /*
+ If the drag went across scroll bounds, we need to apply a
+ "mixed strategy": The part of the drag outside the bounds
+ is mutliplicated by the elasticity factor.
+ */
+ if(currentOffset.e > 0){
+ newOffset.e /= factor;
+ }else if(newOffset.e > 0){
+ newOffset.e *= factor;
+ }else if(currentOffset.e < scrollMin.e){
+ newOffset.e += (scrollMin.e - currentOffset.e) / factor;
+ }else if(newOffset.e < scrollMin.e){
+ newOffset.e -= (scrollMin.e - newOffset.e) * factor;
+ }
+ }
+
+ if(crossingBounds.f){
+ if(currentOffset.f > 0){
+ newOffset.f /= factor;
+ }else if(newOffset.f > 0){
+ newOffset.f *= factor;
+ }else if(currentOffset.f < scrollMin.f){
+ newOffset.f += (scrollMin.f - currentOffset.f) / factor;
+ }else if(newOffset.f < scrollMin.f){
+ newOffset.f -= (scrollMin.f - newOffset.f) * factor;
+ }
+ }
+ }else{
+ // Constrain scrolling to scroller bounds
+ if(newOffset.e < scrollMin.e){
+ newOffset.e = scrollMin.e;
+ }else if(newOffset.e > 0){
+ newOffset.e = 0;
+ }
+
+ if(newOffset.f < scrollMin.f){
+ newOffset.f = scrollMin.f;
+ }else if(newOffset.f > 0){
+ newOffset.f = 0;
+ }
+ }
+
+ this._currentOffset = newOffset;
+
+ var offsetX = newOffset.translate(0, 0, 0),
+ offsetY = newOffset.translate(0, 0, 0);
+
+ offsetX.f = offsetY.e = 0;
+
+ applyMatrixToNode(this.scrollers.e, offsetX);
+ applyMatrixToNode(this.scrollers.f, offsetY);
+
+ // scrollbar position
+ var ratios = this._scrollbars.offsetRatios;
+ offsetX.e *= ratios.e;
+ offsetY.f *= ratios.f;
+
+ if(isOutOfBounds.e){
+ this._squeezeScrollbar("e", scrollbarSizeSubstract.e, newOffset.e < 0);
+ }else{
+ applyMatrixToNode(this._scrollbars.handles.e, offsetX);
+ }
+ if(isOutOfBounds.f){
+ this._squeezeScrollbar("f", scrollbarSizeSubstract.f, newOffset.f < 0);
+ }else{
+ applyMatrixToNode(this._scrollbars.handles.f, offsetY);
+ }
+ },
+
+ // Tracks all properties needed for scrolling
+ // We're tracking only the last two events for the moment
+ _trackEvent: function _trackEvent(event){
+ var trackedEvents = this._trackedEvents;
+ trackedEvents[0] = trackedEvents[1];
+ trackedEvents[1] = {
+ matrix: getMatrixFromEvent(event),
+ timeStamp: event.timeStamp
+ };
+ },
+
+ showScrollbars: function showScrollbars(){
+ var style = this._scrollbars.container.style;
+ style.webkitTransitionDuration = "";
+ style.opacity = "1";
+ },
+
+ hideScrollbars: function hideScrollbars(){
+ var style = this._scrollbars.container.style;
+ style.webkitTransitionDuration = "250ms";
+ style.opacity = "0";
+ },
+
+ _squeezeScrollbar: function _squeezeScrollbar(axis, substract, squeezeAtEnd, duration, timingFunc){
+ var scrollbars = this._scrollbars;
+ var handleStyle = scrollbars.handles[axis].style;
+
+ var defaultSize = scrollbars.sizes[axis];
+ var size = Math.max(defaultSize - substract, 1);
+
+ var matrix = new WebKitCSSMatrix();
+ matrix[axis] = squeezeAtEnd ? scrollbars.maxOffsets[axis] : 0;
+ matrix[axis == "f" ? "d" : "a"] = size / defaultSize;
+
+ handleStyle.webkitTransformOrigin = squeezeAtEnd ? "100% 100%" : "0 0";
+ handleStyle.webkitTransitionProperty = "-webkit-transform";
+ handleStyle.webkitTransform = matrix;
+
+ if(duration){
+ handleStyle.webkitTransitionDuration = duration + "ms";
+ handleStyle.webkitTransitionTimingFunction = timingFunc;
+ this._animationTimeouts[axis].push(setTimeout(function(){
+ handleStyle.webkitTransitionDuration = "";
+ }, duration));
+ }else{
+ handleStyle.webkitTransitionDuration = "";
+ }
+ },
+
+ _determineOffset: function _determineOffset(round){
+ var offsetX = getMatrixFromNode(this.scrollers.e),
+ offsetY = getMatrixFromNode(this.scrollers.f),
+ currentOffset = offsetX.multiply(offsetY);
+
+ if(round){
+ roundMatrix(currentOffset);
+ }
+
+ this._currentOffset = currentOffset;
+ },
+
+ _stopAnimations: function _stopAnimations(){ /*Boolean*/
+ var isAnimating = false;
+ var scrollbars = this._scrollbars;
+ ["e", "f"].forEach(function(axis){
+ this.scrollers[axis].style.webkitTransitionDuration = "";
+ var handle = scrollbars.handles[axis];
+ handle.style.webkitTransitionDuration = "";
+ setTransitionProperty(handle);
+ scrollbars.tracks[axis].style.webkitBoxPack = "";
+
+
+ var timeouts = this._animationTimeouts[axis];
+ isAnimating = isAnimating || timeouts.length;
+ timeouts.forEach(function(timeoutId){
+ clearTimeout(timeoutId);
+ });
+ timeouts.length = 0;
+ }, this);
+
+ // if animating, we stop animations by determining the current
+ // offset (rounding its values) and then setting those values
+ // to the scroller by calling "scrollBy"
+ this._determineOffset(true);
+ this.scrollBy(0, 0);
+
+ // deleting queued bounces
+ this._bounces.e = this._bounces.f = null;
+
+ return isAnimating;
+ },
+
+ _getLastMove: function _getLastMove(){
+ var trackedEvents = this._trackedEvents,
+ event0 = trackedEvents[0],
+ event1 = trackedEvents[1];
+
+ if(!event0){
+ return {duration: 0, matrix: new WebKitCSSMatrix(), length: 0, speed: 0};
+ }
+
+ var duration = event1.timeStamp - event0.timeStamp,
+ matrix = event1.matrix.multiply(event0.matrix.inverse()),
+ length = Math.sqrt(matrix.e * matrix.e + matrix.f * matrix.f);
+
+ return {
+ duration: duration, // move duration in miliseconds
+ matrix: matrix, // matrix of the move
+ length: length, // length of the move in pixels
+ speed: length / duration // speed of the move in miliseconds
+ }
+ },
+
+ // returns flicking duration in miliseconds for a given speed
+ _getFlickingDuration: function _getFlickingDuration(pixelsPerMilisecond){
+ /*
+ The duration is computed as follows:
+
+ variables:
+ m = minimum speed before stopping = config.flicking.minSpeed
+ d = duration
+ s = speed = pixelsPerMilisecond
+ f = friction per milisecond = config.flicking.friction
+
+ The minimum speed is computed as follows:
+ m = s * f ^ d
+
+ // as the minimum speed is given and we need the duration we
+ // can solve the equation for d:
+ <=> d = log(m/s) / log(f)
+ */
+ var duration = Math.log(
+ config.flicking.minSpeed /
+ pixelsPerMilisecond
+ ) /
+ Math.log(config.flicking.friction);
+
+ return duration > 0 ? Math.round(duration) : 0;
+ },
+
+ _getFlickingLength: function _getFlickingLength(initialSpeed, flickDuration){
+ /*
+ The amount of pixels to flick is the sum of the distance covered every
+ milisecond of the flicking duration.
+
+ Because the distance is decelerated by the friction factor, the speed
+ at a given time t is:
+
+ pixelsPerMilisecond * friction^t
+
+ and the distance covered is:
+
+ d = distance
+ s = initial speed = pixelsPerMilisecond
+ t = time = duration
+ f = friction per milisecond = config.flicking.friction
+
+ d = sum of s * f^n for n between 0 and t
+ <=> d = s * (sum of f^n for n between 0 and t)
+
+ which is a geometric series and thus can be simplified to:
+ d = s * (1 - f^(d+1)) / (1 - f)
+ */
+ var factor = (1 - Math.pow(config.flicking.friction, flickDuration + 1)) / (1 - config.flicking.friction);
+ return initialSpeed * factor;
+ },
+
+ startFlick: function startFlick(matrix, duration){
+ if(!duration){
+ return;
+ }
+
+ var epsilon = 1 / duration, // precision for bezier computations
+ points = config.flicking.timingFunc, // control points for the animation function
+ timingFunc = new CubicBezier(points[0], points[1], points[2], points[3]),
+ min = this._scrollMin,
+ currentOffset = this._currentOffset,
+ scrollbars = this._scrollbars;
+
+ roundMatrix(matrix);
+ if(!this._scrolls.e){
+ matrix.e = 0;
+ }
+ if(!this._scrolls.f){
+ matrix.f = 0;
+ }
+
+ var scrollTarget = this._currentOffset.multiply(matrix);
+
+ ["e", "f"].forEach(function(axis){
+ var distance = matrix[axis],
+ target = scrollTarget[axis],
+ segmentFraction = 1; // the fraction of the flick distance until crossing scroller bounds
+
+ // compute distance fraction where flicking crosses scroller bounds
+ if(target < min[axis]){
+ segmentFraction = 1 - Math.max(Math.min((target - min[axis]) / matrix[axis], 1), 0);
+ }else if(target > 0){
+ segmentFraction = 1 - Math.max(Math.min((target - 0) / matrix[axis], 1), 0);
+ }
+
+ var flick = segmentFraction * distance,
+ bounce = distance - flick;
+
+ if(!(flick || bounce)){
+ this._snapBack(axis);
+ return;
+ }
+
+ var t = timingFunc.getTforY(segmentFraction, epsilon),
+ timeFraction = timingFunc.getPointForT(t).x,
+ bezierCurves = timingFunc.divideAtT(t);
+
+ var flickTransform = new WebKitCSSMatrix();
+ flickTransform[axis] = currentOffset[axis];
+
+ var flickDuration = timeFraction * duration;
+
+ if(flick && timeFraction){
+ this._flicking[axis] = true;
+
+ // animate scroller
+ flickTransform[axis] += flick;
+ applyMatrixToNode(this.scrollers[axis], flickTransform,
+ flickDuration + "ms", bezierCurves[0]);
+
+ // animate scrollbars
+ var scrollbarTransform = flickTransform.translate(0, 0, 0);
+ scrollbarTransform[axis] *= scrollbars.offsetRatios[axis];
+ applyMatrixToNode(scrollbars.handles[axis], scrollbarTransform,
+ flickDuration + "ms", bezierCurves[0]);
+
+ }
+
+ if(this.elastic && bounce){
+ var bounceTransform = flickTransform.translate(0, 0, 0),
+ bounceTiming = bezierCurves[1];
+
+ // Creating a smooth transition from bounce out to snap back
+ bounceTiming._p2 = {
+ x: 1 - config.snapBack.timingFunc[0],
+ y: 1 - config.snapBack.timingFunc[1]
+ };
+
+ // limit the bounce to the configured maximum
+ var bounceFactor = Math.min(
+ config.elasticity.factorFlick,
+ config.elasticity.max / Math.abs(bounce)
+ );
+
+ bounceTransform[axis] += bounce * bounceFactor;
+ var bounceDuration = (1 - timeFraction) * duration * bounceFactor;
+ this._bounces[axis] = {
+ timingFunc: bounceTiming,
+ duration: bounceDuration + "ms",
+ matrix: bounceTransform,
+ bounceLength: Math.abs(bounce * bounceFactor)
+ };
+
+ // play queued animations with timeouts, because
+ // the webkitTransitionEnd event fires late on iPhone 3G
+ var that = this;
+ var timeouts = this._animationTimeouts[axis];
+ var handle = this._sc
+
+ timeouts.push(setTimeout(function(){
+ that._playQueuedBounce(axis);
+ }, flickDuration));
+
+ timeouts.push(setTimeout(function(){
+ var duration = config.snapBack.alwaysDefaultTime ? null : bounceDuration;
+ that._snapBack(axis, duration);
+ timeouts.length = 0; // clear timeouts
+ }, flickDuration + bounceDuration));
+ }
+ }, this);
+ },
+
+ _playQueuedBounce: function _playQueuedBounce(axis){
+ var bounce = this._bounces[axis];
+
+ if(bounce){
+ var scroller = this.scrollers[axis],
+ that = this,
+ matrix = bounce.matrix,
+ duration = bounce.duration,
+ timingFunc = bounce.timingFunc;
+
+ applyMatrixToNode(scroller, matrix, duration, timingFunc);
+
+ // bounce scrollbar handle
+ this._squeezeScrollbar(axis, bounce.bounceLength, matrix[axis] < 0, duration, timingFunc);
+
+ this._bounces[axis] = null;
+ return true;
+ }
+
+ return false;
+ },
+
+ _snapBack: function _snapBack(/*String?*/axis, /*Number?*/duration){ /*Boolean*/
+ if(axis == null){
+ var snapBackE = this._snapBack("e", duration);
+ var snapBackF = this._snapBack("f", duration);
+ return snapBackE || snapBackF;
+ }
+
+ duration = duration || config.snapBack.defaultTime;
+
+ var scroller = this.scrollers[axis],
+ offset = getMatrixFromNode(scroller),
+ cp = config.snapBack.timingFunc, // control points
+ timingFunc = new CubicBezier(cp[0], cp[1], cp[2], cp[3]);
+
+ if(offset[axis] < this._scrollMin[axis] || offset[axis] > 0){
+ offset[axis] = Math.max(Math.min(offset[axis], 0), this._scrollMin[axis]);
+ this._squeezeScrollbar(axis, 0, offset[axis] < 0, duration, timingFunc);
+ applyMatrixToNode(scroller, offset, duration + "ms", timingFunc);
+
+ return true;
+ }
+
+ return false;
+ }
+};
+
+return TouchScroll;
+}());
@deleteme

This event.preventDefault() line prevents normal browser scrolling from occurring. Why 'always cancel move events at this point'?

@davidaurelio

That was the intention when I wrote that line :-) Our usecase was to replace native scrolling completely. I can make it configurable, though.

Please sign in to comment.
Something went wrong with that request. Please try again.