OSDN Git Service

update iscroll.js v3.6 base
authorISHIKAWA Mutsumi <ishikawa@hanzubon.jp>
Wed, 25 Aug 2010 10:35:31 +0000 (19:35 +0900)
committerISHIKAWA Mutsumi <ishikawa@hanzubon.jp>
Wed, 25 Aug 2010 10:35:31 +0000 (19:35 +0900)
data/public/iscroll/iscroll.js

index e7d56d6..8bb6f48 100644 (file)
 /**
- * 
+ *
  * Find more about the scrolling function at
- * http://cubiq.org/scrolling-div-for-mobile-webkit-turns-3/16
+ * http://cubiq.org/iscroll
  *
- * Copyright (c) 2009 Matteo Spinelli, http://cubiq.org/
+ * Copyright (c) 2010 Matteo Spinelli, http://cubiq.org/
  * Released under MIT license
  * http://cubiq.org/dropbox/mit-license.txt
- * 
- * Version 3.2.1 - Last updated: 2010.06.03
- * 
+ *
+ * Version 3.6 - Last updated: 2010.08.24
+ *
  */
 
 (function(){
-
 function iScroll (el, options) {
-       this.element = typeof el == 'object' ? el : document.getElementById(el);
-       this.wrapper = this.element.parentNode;
-
-       this.element.style.webkitTransitionProperty = '-webkit-transform';
-       this.element.style.webkitTransitionTimingFunction = 'cubic-bezier(0,0,0.25,1)';
-       this.element.style.webkitTransitionDuration = '0';
-       this.element.style.webkitTransform = 'translate3d(0,0,0)';
-
-       // Get options
-       this.options = {
-               bounce: true,
+       var that = this, i;
+       that.element = typeof el == 'object' ? el : document.getElementById(el);
+       that.wrapper = that.element.parentNode;
+
+       that.element.style.webkitTransitionProperty = '-webkit-transform';
+       that.element.style.webkitTransitionTimingFunction = 'cubic-bezier(0,0,0.25,1)';
+       that.element.style.webkitTransitionDuration = '0';
+       that.element.style.webkitTransform = translateOpen + '0,0' + translateClose;
+
+       // Default options
+       that.options = {
+               bounce: has3d,
+               momentum: has3d,
                checkDOMChanges: true,
                topOnDOMChanges: false,
-               hScrollBar: true,
-           vScrollBar: true,
-           min_move: 5
+               hScrollbar: has3d,
+               vScrollbar: has3d,
+               fadeScrollbar: isIphone || isIpad || !isTouch,
+               shrinkScrollbar: isIphone || isIpad || !isTouch,
+               desktopCompatibility: false,
+               overflow: 'hidden',
+               snap: false
        };
-       
+
+       // User defined options
        if (typeof options == 'object') {
-               for (var i in options) {
-                       this.options[i] = options[i];
+               for (i in options) {
+                       that.options[i] = options[i];
                }
        }
 
-       this.refresh();
-       
-       this.element.addEventListener('touchstart', this);
-       this.element.addEventListener('touchmove', this);
-       this.element.addEventListener('touchend', this);
-       window.addEventListener('orientationchange', this);
+       if (that.options.desktopCompatibility) {
+               that.options.overflow = 'hidden';
+       }
+
+       that.wrapper.style.overflow = that.options.overflow;
 
-       if (this.options.checkDOMChanges) {
-               this.element.addEventListener('DOMSubtreeModified', this);
+       that.refresh();
+
+       window.addEventListener('onorientationchange' in window ? 'orientationchange' : 'resize', that, false);
+
+       if (isTouch || that.options.desktopCompatibility) {
+               that.element.addEventListener(START_EVENT, that, false);
+               that.element.addEventListener(MOVE_EVENT, that, false);
+               that.element.addEventListener(END_EVENT, that, false);
+       }
+
+       if (that.options.checkDOMChanges) {
+               that.element.addEventListener('DOMSubtreeModified', that, false);
        }
 }
 
 iScroll.prototype = {
        x: 0,
        y: 0,
+       enabled: true,
 
        handleEvent: function (e) {
+               var that = this;
+
                switch (e.type) {
-                       case 'touchstart': this.onTouchStart(e); break;
-                       case 'touchmove': this.onTouchMove(e); break;
-                       case 'touchend': this.onTouchEnd(e); break;
-                       case 'webkitTransitionEnd': this.onTransitionEnd(e); break;
-                       case 'orientationchange': this.refresh(); break;
-                       case 'DOMSubtreeModified': this.onDOMModified(e); break;
+                       case START_EVENT:
+                               that.touchStart(e);
+                               break;
+                       case MOVE_EVENT:
+                               that.touchMove(e);
+                               break;
+                       case END_EVENT:
+                               that.touchEnd(e);
+                               break;
+                       case 'webkitTransitionEnd':
+                               that.transitionEnd();
+                               break;
+                       case 'orientationchange':
+                       case 'resize':
+                               that.refresh();
+                               break;
+                       case 'DOMSubtreeModified':
+                               that.onDOMModified(e);
+                               break;
                }
        },
-       
+
        onDOMModified: function (e) {
-               this.refresh();
-               
-               if (this.options.topOnDOMChanges && (this.x!=0 || this.y!=0)) {
-                       this.scrollTo(0,0,'0');
+               var that = this;
+
+               // (Hopefully) execute onDOMModified only once
+               if (e.target.parentNode != that.element) {
+                       return;
+               }
+
+               setTimeout(function () { that.refresh(); }, 0);
+
+               if (that.options.topOnDOMChanges && (that.x!=0 || that.y!=0)) {
+                       that.scrollTo(0,0,'0');
                }
        },
 
        refresh: function () {
-               this.scrollWidth = this.wrapper.clientWidth;
-               this.scrollHeight = this.wrapper.clientHeight;
-               this.maxScrollX = this.scrollWidth - this.element.offsetWidth;
-               this.maxScrollY = this.scrollHeight - this.element.offsetHeight;
-
-               var resetX = this.x, resetY = this.y;
-               if (this.scrollX) {
-                       if (this.maxScrollX >= 0) {
+               var that = this,
+                       resetX = this.x, resetY = this.y,
+                       snap;
+
+               that.scrollWidth = that.wrapper.clientWidth;
+               that.scrollHeight = that.wrapper.clientHeight;
+               that.scrollerWidth = that.element.offsetWidth;
+               that.scrollerHeight = that.element.offsetHeight;
+               that.maxScrollX = that.scrollWidth - that.scrollerWidth;
+               that.maxScrollY = that.scrollHeight - that.scrollerHeight;
+               that.directionX = 0;
+               that.directionY = 0;
+
+               if (that.scrollX) {
+                       if (that.maxScrollX >= 0) {
                                resetX = 0;
-                       } else if (this.x < this.maxScrollX) {
-                               resetX = this.maxScrollX;
+                       } else if (that.x < that.maxScrollX) {
+                               resetX = that.maxScrollX;
                        }
                }
-               if (this.scrollY) {
-                       if (this.maxScrollY >= 0) {
+               if (that.scrollY) {
+                       if (that.maxScrollY >= 0) {
                                resetY = 0;
-                       } else if (this.y < this.maxScrollY) {
-                               resetY = this.maxScrollY;
+                       } else if (that.y < that.maxScrollY) {
+                               resetY = that.maxScrollY;
                        }
                }
-               if (resetX!=this.x || resetY!=this.y) {
-                       this.scrollTo(resetX,resetY,'0');
+               // Snap
+               if (that.options.snap) {
+                       that.maxPageX = -Math.floor(that.maxScrollX/that.scrollWidth);
+                       that.maxPageY = -Math.floor(that.maxScrollY/that.scrollHeight);
+
+                       snap = that.snap(resetX, resetY);
+                       resetX = snap.x;
+                       resetY = snap.y;
+               }
+
+               if (resetX!=that.x || resetY!=that.y) {
+                       that.setTransitionTime('0');
+                       that.setPosition(resetX, resetY, true);
                }
 
-               this.scrollX = this.element.offsetWidth > this.scrollWidth ? true : false;
-               this.scrollY = this.element.offsetHeight > this.scrollHeight ? true : false;
+               that.scrollX = that.scrollerWidth > that.scrollWidth;
+               that.scrollY = !that.scrollX || that.scrollerHeight > that.scrollHeight;
 
                // Update horizontal scrollbar
-               if (this.options.hScrollBar && this.scrollX) {
-                       this.scrollBarX = (this.scrollBarX instanceof scrollbar) ? this.scrollBarX : new scrollbar('horizontal', this.wrapper);
-                       this.scrollBarX.init(this.scrollWidth, this.element.offsetWidth);
-               } else if (this.scrollBarX) {
-                       this.scrollBarX = this.scrollBarX.remove();
+               if (that.options.hScrollbar && that.scrollX) {
+                       that.scrollBarX = that.scrollBarX || new scrollbar('horizontal', that.wrapper, that.options.fadeScrollbar, that.options.shrinkScrollbar);
+                       that.scrollBarX.init(that.scrollWidth, that.scrollerWidth);
+               } else if (that.scrollBarX) {
+                       that.scrollBarX = that.scrollBarX.remove();
                }
 
                // Update vertical scrollbar
-               if (this.options.vScrollBar && this.scrollY) {
-                       this.scrollBarY = (this.scrollBarY instanceof scrollbar) ? this.scrollBarY : new scrollbar('vertical', this.wrapper);
-                       this.scrollBarY.init(this.scrollHeight, this.element.offsetHeight);
-               } else if (this.scrollBarY) {
-                       this.scrollBarY = this.scrollBarY.remove();
+               if (that.options.vScrollbar && that.scrollY && that.scrollerHeight > that.scrollHeight) {
+                       that.scrollBarY = that.scrollBarY || new scrollbar('vertical', that.wrapper, that.options.fadeScrollbar, that.options.shrinkScrollbar);
+                       that.scrollBarY.init(that.scrollHeight, that.scrollerHeight);
+               } else if (that.scrollBarY) {
+                       that.scrollBarY = that.scrollBarY.remove();
                }
        },
 
-       setPosition: function (x, y) { 
-               this.x = x !== null ? x : this.x;
-               this.y = y !== null ? y : this.y;
+       setPosition: function (x, y, hideScrollBars) {
+               var that = this;
 
-               this.element.style.webkitTransform = 'translate3d(' + this.x + 'px,' + this.y + 'px,0)';
+               that.x = x;
+               that.y = y;
+
+               that.element.style.webkitTransform = translateOpen + that.x + 'px,' + that.y + 'px' + translateClose;
 
                // Move the scrollbars
-               if (this.scrollBarX) {
-                       this.scrollBarX.setPosition(this.scrollBarX.maxScroll / this.maxScrollX * this.x);
+               if (!hideScrollBars) {
+                       if (that.scrollBarX) {
+                               that.scrollBarX.setPosition(that.x);
+                       }
+                       if (that.scrollBarY) {
+                               that.scrollBarY.setPosition(that.y);
+                       }
+               }
+       },
+
+       setTransitionTime: function(time) {
+               var that = this;
+
+               time = time || '0';
+               that.element.style.webkitTransitionDuration = time;
+
+               if (that.scrollBarX) {
+                       that.scrollBarX.bar.style.webkitTransitionDuration = time;
+                       that.scrollBarX.wrapper.style.webkitTransitionDuration = has3d && that.options.fadeScrollbar ? '300ms' : '0';
                }
-               if (this.scrollBarY) {
-                       this.scrollBarY.setPosition(this.scrollBarY.maxScroll / this.maxScrollY * this.y);
+               if (that.scrollBarY) {
+                       that.scrollBarY.bar.style.webkitTransitionDuration = time;
+                       that.scrollBarY.wrapper.style.webkitTransitionDuration = has3d && that.options.fadeScrollbar ? '300ms' : '0';
                }
        },
-               
-       onTouchStart: function(e) {
-           if (e.targetTouches.length != 1) {
-               return false;
-        }
-
-           /* iOS 4 以外 特に androidだと ここで preventDefault しないと
-              まともにスクロールしない/ブラウザがスクロールしてしまう/
-              スクロールしたあとの位置にあるリンクがクリックされちゃうとか
-              いろいろ起きる。逆に iOS4だと ここで preventDefault しちゃうと
-              長押ししたときの ディフォルトのポップアップが出てこなくなる */
-           /* iOS4 意外だと ディフォルトのポップアップが使えなくなるが どうやら 打つ手なし... */
-           if (!navigator.appVersion.match(/iPhone OS 4/)) {
+
+       touchStart: function(e) {
+               var that = this,
+                       matrix;
+
+            /* iOS 4 以外 特に androidだと ここで preventDefault しないと
+               まともにスクロールしない/ブラウザがスクロールしてしまう/
+               スクロールしたあとの位置にあるリンクがクリックされちゃうとか
+               いろいろ起きる。逆に iOS4だと ここで preventDefault しちゃうと
+               長押ししたときの ディフォルトのポップアップが出てこなくなる */
+            /* iOS4 意外だと ディフォルトのポップアップが使えなくなるが どうやら打つ手なし... */
+           if (!isIphone && !isIpad) {
                e.preventDefault();
            }
                e.stopPropagation();
-               
-               this.element.style.webkitTransitionDuration = '0';
-               
-               if (this.scrollBarX) {
-                       this.scrollBarX.bar.style.webkitTransitionDuration = '0, 250ms';
-               }
-               if (this.scrollBarY) {
-                       this.scrollBarY.bar.style.webkitTransitionDuration = '0, 250ms';
+
+               if (!that.enabled) {
+                       return;
                }
 
-               // Check if elem is really where it should be
-               var theTransform = new WebKitCSSMatrix(window.getComputedStyle(this.element).webkitTransform);
-               if (theTransform.m41 != this.x || theTransform.m42 != this.y) {
-                       this.setPosition(theTransform.m41, theTransform.m42);
+               that.scrolling = true;          // This is probably not needed, but may be useful if iScroll is used in conjuction with other frameworks
+
+               that.moved = false;
+               that.dist = 0;
+
+               that.setTransitionTime('0');
+
+               // Check if the scroller is really where it should be
+               if (that.options.momentum || that.options.snap) {
+                       matrix = new WebKitCSSMatrix(window.getComputedStyle(that.element).webkitTransform);
+                       if (matrix.e != that.x || matrix.f != that.y) {
+                               document.removeEventListener('webkitTransitionEnd', that, false);
+                               that.setPosition(matrix.e, matrix.f);
+                               that.moved = true;
+                       }
                }
 
-               this.touchStartX = e.touches[0].pageX;
-               this.scrollStartX = this.x;
+               that.touchStartX = isTouch ? e.changedTouches[0].pageX : e.pageX;
+               that.scrollStartX = that.x;
 
-               this.touchStartY = e.touches[0].pageY;
-               this.scrollStartY = this.y;
+               that.touchStartY = isTouch ? e.changedTouches[0].pageY : e.pageY;
+               that.scrollStartY = that.y;
 
-               this.scrollStartTime = e.timeStamp;
-               this.moved = false;
+               that.scrollStartTime = e.timeStamp;
+
+               that.directionX = 0;
+               that.directionY = 0;
        },
-       
-       onTouchMove: function(e) {
-               if (e.targetTouches.length != 1) {
-                       return false;
+
+       touchMove: function(e) {
+               var that = this,
+                       pageX = isTouch ? e.changedTouches[0].pageX : e.pageX,
+                       pageY = isTouch ? e.changedTouches[0].pageY : e.pageY,
+                       leftDelta = that.scrollX ? pageX - that.touchStartX : 0,
+                       topDelta = that.scrollY ? pageY - that.touchStartY : 0,
+                       newX = that.x + leftDelta,
+                       newY = that.y + topDelta;
+
+               if (!that.scrolling) {
+                       return;
                }
 
-           /* touchstart の preventDefault やめて ここに追加すると iPhone4 だといい感じ*/
-           if (navigator.appVersion.match(/iPhone OS 4/)) {
-               e.preventDefault();
-           }
-               var leftDelta = this.scrollX === true ? e.touches[0].pageX - this.touchStartX : 0,
-                       topDelta = this.scrollY === true ? e.touches[0].pageY - this.touchStartY : 0,
-                       newX = this.x + leftDelta,
-                       newY = this.y + topDelta;
+               /* touchstart の preventDefault やめて ここに追加すると iPhone4 だといい感じ*/
+
+               if (isIphone || isIpad) {
+                   e.preventDefault();
+               }
+               e.stopPropagation();    // Stopping propagation just saves some cpu cycles (I presume)
+
+               that.touchStartX = pageX;
+               that.touchStartY = pageY;
 
-           if (Math.abs(topDelta) > this.options.min_move || Math.abs(leftDelta) > this.options.min_move) {
                // Slow down if outside of the boundaries
-               if (newX > 0 || newX < this.maxScrollX) { 
-                       newX = this.options.bounce ? Math.round(this.x + leftDelta / 4) : this.x;
+               if (newX >= 0 || newX < that.maxScrollX) {
+                       newX = that.options.bounce ? Math.round(that.x + leftDelta / 3) : (newX >= 0 || that.maxScrollX>=0) ? 0 : that.maxScrollX;
                }
-               if (newY > 0 || newY < this.maxScrollY) { 
-                       newY = this.options.bounce ? Math.round(this.y + topDelta / 4) : this.y;
+               if (newY >= 0 || newY < that.maxScrollY) {
+                       newY = that.options.bounce ? Math.round(that.y + topDelta / 3) : (newY >= 0 || that.maxScrollY>=0) ? 0 : that.maxScrollY;
                }
 
-               if (this.scrollBarX && !this.scrollBarX.visible) {
-                       this.scrollBarX.show();
+               if (that.dist > 5) {                    // 5 pixels threshold is needed on Android, but also on iPhone looks more natural
+                       that.setPosition(newX, newY);
+                       that.moved = true;
+                       that.directionX = leftDelta > 0 ? -1 : 1;
+                       that.directionY = topDelta > 0 ? -1 : 1;
+               } else {
+                       that.dist+= Math.abs(leftDelta) + Math.abs(topDelta);
                }
-               if (this.scrollBarY && !this.scrollBarY.visible) {
-                       this.scrollBarY.show();
+       },
+
+       touchEnd: function(e) {
+               var that = this,
+                       time = e.timeStamp - that.scrollStartTime,
+                       point = isTouch ? e.changedTouches[0] : e,
+                       target, ev,
+                       momentumX, momentumY,
+                       newDuration = 0,
+                       newPositionX = that.x, newPositionY = that.y,
+                       snap;
+
+               if (!that.scrolling) {
+                       return;
                }
+               that.scrolling = false;
 
-               this.setPosition(newX, newY);
+               if (!that.moved) {
+                       that.resetPosition();
 
-               this.touchStartX = e.touches[0].pageX;
-               this.touchStartY = e.touches[0].pageY;
-               this.moved = true;
+                   /* iOS4 では touchStart イベントは preventDefault() してないので
+                       click イベントを発生させちゃうと 2重にクリックが発生したような
+                       状態になるので、スルーでいい */
+                       if (isAndroid) {
+                               // Find the last touched element
+                               target = point.target;
+                               while (target.nodeType != 1) {
+                                       target = target.parentNode;
+                               }
+
+                               // Create the fake event
+                               target.style.pointerEvents = 'auto';
+                               ev = document.createEvent('MouseEvents');
+                               ev.initMouseEvent('click', true, true, e.view, 1,
+                                       point.screenX, point.screenY, point.clientX, point.clientY,
+                                       e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,
+                                       0, null);
+                               ev._fake = true;
+                               target.dispatchEvent(ev);
+                       }
 
-               // Prevent slingshot effect
-               if( e.timeStamp-this.scrollStartTime > 250 ) {
-                       this.scrollStartX = this.x;
-                       this.scrollStartY = this.y;
-                       this.scrollStartTime = e.timeStamp;
+                       return;
                }
-           }
-       },
-       
-       onTouchEnd: function(e) {
-               if (e.targetTouches.length > 0) {
-                       return false;
+
+               if (!that.options.snap && time > 250) {                 // Prevent slingshot effect
+                       that.resetPosition();
+                       return;
                }
 
-               if (!this.moved) {
-                   /* iOS4 では touchStart イベントは preventDefault() してないので
-                      click イベントを発生させちゃうと 2重にクリックが発生したような
-                      状態になるので、スルーでいい */
-                   if (!navigator.appVersion.match(/iPhone OS 4/)) {
-                       // Find the last touched element
-                       var theTarget = e.changedTouches[0].target;
-                       if (theTarget.nodeType == 3) {
-                               theTarget = theTarget.parentNode;
-                       }
-                       // Create the fake event
-                       var theEvent = document.createEvent('MouseEvents');
-                       theEvent.initMouseEvent("click", true, true, document.defaultView,
-                                                                       e.detail, e.screenX, e.screenY, e.clientX, e.clientY,
-                                                                       e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,
-                                                                       e.button, e.relatedTarget);
-                       theTarget.dispatchEvent(theEvent);
-                       return false;
-                   }
-                   return true;
-               }
-               
-               var time = e.timeStamp - this.scrollStartTime;
-
-               var momentumX = this.scrollX === true
-                       ? this.momentum(this.x - this.scrollStartX,
-                                                       time,
-                                                       this.options.bounce ? -this.x + this.scrollWidth/4 : -this.x,
-                                                       this.options.bounce ? this.x + this.element.offsetWidth - this.scrollWidth + this.scrollWidth/4 : this.x + this.element.offsetWidth - this.scrollWidth)
-                       : { dist: 0, time: 0 };
-
-               var momentumY = this.scrollY === true
-                       ? this.momentum(this.y - this.scrollStartY,
-                                                       time,
-                                                       this.options.bounce ? -this.y + this.scrollHeight/4 : -this.y,
-                                                       this.options.bounce ? this.y + this.element.offsetHeight - this.scrollHeight + this.scrollHeight/4 : this.y + this.element.offsetHeight - this.scrollHeight)
-                       : { dist: 0, time: 0 };
-
-               if (!momentumX.dist && !momentumY.dist) {
-                       this.resetPosition();
-                       return false;
-               }
-
-               var newDuration = Math.max(Math.max(momentumX.time, momentumY.time), 1);                // The minimum animation length must be 1ms
-               var newPositionX = this.x + momentumX.dist;
-               var newPositionY = this.y + momentumY.dist;
-
-               this.element.addEventListener('webkitTransitionEnd', this);
-
-               this.scrollTo(newPositionX, newPositionY, newDuration + 'ms');
+               if (that.options.momentum) {
+                       momentumX = that.scrollX === true
+                               ? that.momentum(that.x - that.scrollStartX,
+                                                               time,
+                                                               that.options.bounce ? -that.x + that.scrollWidth/5 : -that.x,
+                                                               that.options.bounce ? that.x + that.scrollerWidth - that.scrollWidth + that.scrollWidth/5 : that.x + that.scrollerWidth - that.scrollWidth)
+                               : { dist: 0, time: 0 };
+
+                       momentumY = that.scrollY === true
+                               ? that.momentum(that.y - that.scrollStartY,
+                                                               time,
+                                                               that.options.bounce ? -that.y + that.scrollHeight/5 : -that.y,
+                                                               that.options.bounce ? (that.maxScrollY < 0 ? that.y + that.scrollerHeight - that.scrollHeight : 0) + that.scrollHeight/5 : that.y + that.scrollerHeight - that.scrollHeight)
+                               : { dist: 0, time: 0 };
+
+                       newDuration = Math.max(Math.max(momentumX.time, momentumY.time), 1);            // The minimum animation length must be 1ms
+                       newPositionX = that.x + momentumX.dist;
+                       newPositionY = that.y + momentumY.dist;
+               }
 
-               // Move the scrollbars
-               if (this.scrollBarX) {
-                       this.scrollBarX.scrollTo(this.scrollBarX.maxScroll / this.maxScrollX * newPositionX, newDuration + 'ms');       
-               }               
-               if (this.scrollBarY) {
-                       this.scrollBarY.scrollTo(this.scrollBarY.maxScroll / this.maxScrollY * newPositionY, newDuration + 'ms');
+               if (that.options.snap) {
+                       snap = that.snap(newPositionX, newPositionY);
+                       newPositionX = snap.x;
+                       newPositionY = snap.y;
+                       newDuration = Math.max(snap.time, newDuration);
                }
+
+               that.scrollTo(newPositionX, newPositionY, newDuration + 'ms');
        },
-       
-       onTransitionEnd: function () {
-               this.element.removeEventListener('webkitTransitionEnd', this);
-               this.resetPosition();
+
+       transitionEnd: function () {
+               var that = this;
+               document.removeEventListener('webkitTransitionEnd', that, false);
+               that.resetPosition();
        },
 
        resetPosition: function () {
-               var resetX = this.x,
-                       resetY = this.y;
-               
-               if (this.x >= 0) {
+               var that = this,
+                       resetX = that.x,
+                       resetY = that.y;
+
+               if (that.x >= 0) {
                        resetX = 0;
-               } else if (this.x < this.maxScrollX) {
-                       resetX = this.maxScrollX;
+               } else if (that.x < that.maxScrollX) {
+                       resetX = that.maxScrollX;
                }
 
-               if (this.y >= 0) {
+               if (that.y >= 0 || that.maxScrollY > 0) {
                        resetY = 0;
-               } else if (this.y < this.maxScrollY) {
-                       resetY = this.maxScrollY;
+               } else if (that.y < that.maxScrollY) {
+                       resetY = that.maxScrollY;
                }
 
-               if (resetX != this.x || resetY != this.y) {
-                       this.scrollTo(resetX, resetY, '500ms');
+               if (resetX != that.x || resetY != that.y) {
+                       that.scrollTo(resetX, resetY);
+               } else {
+                       if (that.moved) {
+                               that.onScrollEnd();             // Execute custom code on scroll end
+                               that.moved = false;
+                       }
 
-                       if (this.scrollBarX && resetX != this.x) {
-                               this.scrollBarX.scrollTo(this.scrollBarX.maxScroll / this.maxScrollX * resetX, '500ms');
-                       }                       
-                       if (this.scrollBarY && resetY != this.y) {
-                               this.scrollBarY.scrollTo(this.scrollBarY.maxScroll / this.maxScrollY * resetY, '500ms');
+                       // Hide the scrollbars
+                       if (that.scrollBarX) {
+                               that.scrollBarX.hide();
+                       }
+                       if (that.scrollBarY) {
+                               that.scrollBarY.hide();
                        }
                }
-               
-               // Hide the scrollbars
-               if (this.scrollBarX) {
-                       this.scrollBarX.hide();
+       },
+
+       snap: function (x, y) {
+               var that = this, time;
+
+               if (that.directionX > 0) {
+                       x = Math.floor(x/that.scrollWidth);
+               } else if (that.directionX < 0) {
+                       x = Math.ceil(x/that.scrollWidth);
+               } else {
+                       x = Math.round(x/that.scrollWidth);
+               }
+               that.pageX = -x;
+               x = x * that.scrollWidth;
+               if (x > 0) {
+                       x = that.pageX = 0;
+               } else if (x < that.maxScrollX) {
+                       that.pageX = that.maxPageX;
+                       x = that.maxScrollX;
                }
-               if (this.scrollBarY) {
-                       this.scrollBarY.hide();
+
+               if (that.directionY > 0) {
+                       y = Math.floor(y/that.scrollHeight);
+               } else if (that.directionY < 0) {
+                       y = Math.ceil(y/that.scrollHeight);
+               } else {
+                       y = Math.round(y/that.scrollHeight);
+               }
+               that.pageY = -y;
+               y = y * that.scrollHeight;
+               if (y > 0) {
+                       y = that.pageY = 0;
+               } else if (y < that.maxScrollY) {
+                       that.pageY = that.maxPageY;
+                       y = that.maxScrollY;
                }
+
+               // Snap with constant speed (proportional duration)
+               time = Math.round(Math.max(
+                               Math.abs(that.x - x) / that.scrollWidth * 500,
+                               Math.abs(that.y - y) / that.scrollHeight * 500
+                       ));
+
+               return { x: x, y: y, time: time };
        },
 
        scrollTo: function (destX, destY, runtime) {
-               this.element.style.webkitTransitionDuration = runtime || '400ms';
-               this.setPosition(destX, destY);
+               var that = this;
+
+               if (that.x == destX && that.y == destY) {
+                       that.resetPosition();
+                       return;
+               }
+
+               that.moved = true;
+               that.setTransitionTime(runtime || '350ms');
+               that.setPosition(destX, destY);
+
+               if (runtime==='0' || runtime=='0s' || runtime=='0ms') {
+                       that.resetPosition();
+               } else {
+                       document.addEventListener('webkitTransitionEnd', that, false);  // At the end of the transition check if we are still inside of the boundaries
+               }
+       },
+
+       scrollToPage: function (pageX, pageY, runtime) {
+               var that = this, snap;
+
+               if (!that.options.snap) {
+                       that.pageX = -Math.round(that.x / that.scrollWidth);
+                       that.pageY = -Math.round(that.y / that.scrollHeight);
+               }
+
+               if (pageX == 'next') {
+                       pageX = ++that.pageX;
+               } else if (pageX == 'prev') {
+                       pageX = --that.pageX;
+               }
+
+               if (pageY == 'next') {
+                       pageY = ++that.pageY;
+               } else if (pageY == 'prev') {
+                       pageY = --that.pageY;
+               }
+
+               pageX = -pageX*that.scrollWidth;
+               pageY = -pageY*that.scrollHeight;
+
+               snap = that.snap(pageX, pageY);
+               pageX = snap.x;
+               pageY = snap.y;
+
+               that.scrollTo(pageX, pageY, runtime || '500ms');
+       },
+
+       scrollToElement: function (el, runtime) {
+               el = typeof el == 'object' ? el : this.element.querySelector(el);
+
+               if (!el) {
+                       return;
+               }
+
+               var that = this,
+                       x = that.scrollX ? -el.offsetLeft : 0,
+                       y = that.scrollY ? -el.offsetTop : 0;
+
+               if (x >= 0) {
+                       x = 0;
+               } else if (x < that.maxScrollX) {
+                       x = that.maxScrollX;
+               }
+
+               if (y >= 0) {
+                       y = 0;
+               } else if (y < that.maxScrollY) {
+                       y = that.maxScrollY;
+               }
+
+               that.scrollTo(x, y, runtime);
        },
 
        momentum: function (dist, time, maxDistUpper, maxDistLower) {
-               var friction = 0.1,
-                       deceleration = 1.5,
+               var friction = 2.5,
+                       deceleration = 1.2,
                        speed = Math.abs(dist) / time * 1000,
-                       newDist = speed * speed / (20 * friction) / 1000;
+                       newDist = speed * speed / friction / 1000,
+                       newTime = 0;
 
-               // Proportinally reduce speed if we are outside of the boundaries 
+               // Proportinally reduce speed if we are outside of the boundaries
                if (dist > 0 && newDist > maxDistUpper) {
-                       speed = speed * maxDistUpper / newDist;
+                       speed = speed * maxDistUpper / newDist / friction;
                        newDist = maxDistUpper;
-               }
-               if (dist < 0 && newDist > maxDistLower) {
-                       speed = speed * maxDistLower / newDist;
+               } else if (dist < 0 && newDist > maxDistLower) {
+                       speed = speed * maxDistLower / newDist / friction;
                        newDist = maxDistLower;
                }
-               
+
                newDist = newDist * (dist < 0 ? -1 : 1);
-               
-               var newTime = speed / deceleration;
+               newTime = speed / deceleration;
 
                return { dist: Math.round(newDist), time: Math.round(newTime) };
+       },
+
+       onScrollEnd: function () {},
+
+       destroy: function (full) {
+               var that = this;
+
+               window.removeEventListener('onorientationchange' in window ? 'orientationchange' : 'resize', that, false);
+               that.element.removeEventListener(START_EVENT, that, false);
+               that.element.removeEventListener(MOVE_EVENT, that, false);
+               that.element.removeEventListener(END_EVENT, that, false);
+               document.removeEventListener('webkitTransitionEnd', that, false);
+
+               if (that.options.checkDOMChanges) {
+                       that.element.removeEventListener('DOMSubtreeModified', that, false);
+               }
+
+               if (that.scrollBarX) {
+                       that.scrollBarX = that.scrollBarX.remove();
+               }
+
+               if (that.scrollBarY) {
+                       that.scrollBarY = that.scrollBarY.remove();
+               }
+
+               if (full) {
+                       that.wrapper.parentNode.removeChild(that.wrapper);
+               }
+
+               return null;
        }
 };
 
-var scrollbar = function (dir, wrapper) {
-       this.dir = dir;
-       this.bar = document.createElement('div');
-       this.bar.className = 'scrollbar ' + dir;
-       this.bar.style.webkitTransitionTimingFunction = 'cubic-bezier(0,0,0.25,1)';
-       this.bar.style.webkitTransform = 'translate3d(0,0,0)';
-       this.bar.style.webkitTransitionProperty = '-webkit-transform,opacity';
-       this.bar.style.webkitTransitionDuration = '0,300ms';
-       this.bar.style.pointerEvents = 'none';
-       this.bar.style.opacity = '0';
-
-       wrapper.appendChild(this.bar);
+function scrollbar (dir, wrapper, fade, shrink) {
+       var that = this, style;
+
+       that.dir = dir;
+       that.fade = fade;
+       that.shrink = shrink;
+       that.uid = ++uid;
+
+       // Create main scrollbar
+       that.bar = document.createElement('div');
+
+       style = 'position:absolute;top:0;left:0;-webkit-transition-timing-function:cubic-bezier(0,0,0.25,1);pointer-events:none;-webkit-transition-duration:0;-webkit-transition-delay:0;-webkit-transition-property:-webkit-transform;z-index:10;background:rgba(0,0,0,0.5);' +
+               '-webkit-transform:' + translateOpen + '0,0' + translateClose + ';' +
+               (dir == 'horizontal' ? '-webkit-border-radius:3px 2px;min-width:6px;min-height:5px' : '-webkit-border-radius:2px 3px;min-width:5px;min-height:6px');
+
+       that.bar.setAttribute('style', style);
+
+       // Create scrollbar wrapper
+       that.wrapper = document.createElement('div');
+       style = '-webkit-mask:-webkit-canvas(scrollbar' + that.uid + that.dir + ');position:absolute;z-index:10;pointer-events:none;overflow:hidden;opacity:0;-webkit-transition-duration:' + (fade ? '300ms' : '0') + ';-webkit-transition-delay:0;-webkit-transition-property:opacity;' +
+               (that.dir == 'horizontal' ? 'bottom:2px;left:2px;right:7px;height:5px' : 'top:2px;right:2px;bottom:7px;width:5px;');
+       that.wrapper.setAttribute('style', style);
+
+       // Add scrollbar to the DOM
+       that.wrapper.appendChild(that.bar);
+       wrapper.appendChild(that.wrapper);
 }
 
 scrollbar.prototype = {
-       size: 0,
-       maxSize: 0,
-       maxScroll: 0,
-       visible: false,
-       
        init: function (scroll, size) {
-               var offset = this.dir == 'horizontal' ? this.bar.offsetWidth - this.bar.clientWidth : this.bar.offsetHeight - this.bar.clientHeight;
-               this.maxSize = scroll - 8;              // 8 = distance from top + distance from bottom
-               this.size = Math.round(this.maxSize * this.maxSize / size) + offset;
-               this.maxScroll = this.maxSize - this.size;
-               this.bar.style[this.dir == 'horizontal' ? 'width' : 'height'] = (this.size - offset) + 'px';
+               var that = this,
+                       ctx;
+
+               // Create scrollbar mask
+               if (that.dir == 'horizontal') {
+                       if (that.maxSize != that.wrapper.offsetWidth) {
+                               that.maxSize = that.wrapper.offsetWidth;
+                               ctx = document.getCSSCanvasContext("2d", "scrollbar" + that.uid + that.dir, that.maxSize, 5);
+                               ctx.fillStyle = "rgb(0,0,0)";
+                               ctx.beginPath();
+                               ctx.arc(2.5, 2.5, 2.5, Math.PI/2, -Math.PI/2, false);
+                               ctx.lineTo(that.maxSize-2.5, 0);
+                               ctx.arc(that.maxSize-2.5, 2.5, 2.5, -Math.PI/2, Math.PI/2, false);
+                               ctx.closePath();
+                               ctx.fill();
+                       }
+               } else {
+                       if (that.maxSize != that.wrapper.offsetHeight) {
+                               that.maxSize = that.wrapper.offsetHeight;
+                               ctx = document.getCSSCanvasContext("2d", "scrollbar" + that.uid + that.dir, 5, that.maxSize);
+                               ctx.fillStyle = "rgb(0,0,0)";
+                               ctx.beginPath();
+                               ctx.arc(2.5, 2.5, 2.5, Math.PI, 0, false);
+                               ctx.lineTo(5, that.maxSize-2.5);
+                               ctx.arc(2.5, that.maxSize-2.5, 2.5, 0, Math.PI, false);
+                               ctx.closePath();
+                               ctx.fill();
+                       }
+               }
+
+               that.size = Math.max(Math.round(that.maxSize * that.maxSize / size), 6);
+               that.maxScroll = that.maxSize - that.size;
+               that.toWrapperProp = that.maxScroll / (scroll - size);
+               that.bar.style[that.dir == 'horizontal' ? 'width' : 'height'] = that.size + 'px';
        },
-       
+
        setPosition: function (pos) {
+               var that = this;
+
+               if (that.wrapper.style.opacity != '1') {
+                       that.show();
+               }
+
+               pos = Math.round(that.toWrapperProp * pos);
+
                if (pos < 0) {
-                       pos = 0;
-               } else if (pos > this.maxScroll) {
-                       pos = this.maxScroll;
+                       pos = that.shrink ? pos + pos*3 : 0;
+                       if (that.size + pos < 7) {
+                               pos = -that.size + 6;
+                       }
+               } else if (pos > that.maxScroll) {
+                       pos = that.shrink ? pos + (pos-that.maxScroll)*3 : that.maxScroll;
+                       if (that.size + that.maxScroll - pos < 7) {
+                               pos = that.size + that.maxScroll - 6;
+                       }
                }
 
-               pos = this.dir == 'horizontal' ? 'translate3d(' + Math.round(pos) + 'px,0,0)' : 'translate3d(0,' + Math.round(pos) + 'px,0)';
-               this.bar.style.webkitTransform = pos;
-       },
-       
-       scrollTo: function (pos, runtime) {
-               this.bar.style.webkitTransitionDuration = (runtime || '400ms') + ',300ms';
-               this.setPosition(pos);
+               pos = that.dir == 'horizontal'
+                       ? translateOpen + pos + 'px,0' + translateClose
+                       : translateOpen + '0,' + pos + 'px' + translateClose;
+
+               that.bar.style.webkitTransform = pos;
        },
-       
+
        show: function () {
-               this.visible = true;
-               this.bar.style.opacity = '1';
+               if (has3d) {
+                       this.wrapper.style.webkitTransitionDelay = '0';
+               }
+               this.wrapper.style.opacity = '1';
        },
 
        hide: function () {
-               this.visible = false;
-               this.bar.style.opacity = '0';
+               if (has3d) {
+                       this.wrapper.style.webkitTransitionDelay = '350ms';
+               }
+               this.wrapper.style.opacity = '0';
        },
-       
+
        remove: function () {
-               this.bar.parentNode.removeChild(this.bar);
+               this.wrapper.parentNode.removeChild(this.wrapper);
                return null;
        }
 };
 
+// Is translate3d compatible?
+var has3d = ('WebKitCSSMatrix' in window && 'm11' in new WebKitCSSMatrix()),
+       // Device sniffing
+       isIphone = (/iphone/gi).test(navigator.appVersion),
+       isIpad = (/ipad/gi).test(navigator.appVersion),
+       isAndroid = (/android/gi).test(navigator.appVersion),
+       isTouch = isIphone || isIpad || isAndroid,
+       // Event sniffing
+       START_EVENT = isTouch ? 'touchstart' : 'mousedown',
+       MOVE_EVENT = isTouch ? 'touchmove' : 'mousemove',
+       END_EVENT = isTouch ? 'touchend' : 'mouseup',
+       // Translate3d helper
+       translateOpen = 'translate' + (has3d ? '3d(' : '('),
+       translateClose = has3d ? ',0)' : ')',
+       // Unique ID
+       uid = 0;
+
 // Expose iScroll to the world
 window.iScroll = iScroll;
 })();