3 * Find more about the scrolling function at
4 * http://cubiq.org/scrolling-div-for-mobile-webkit-turns-3/16
6 * Copyright (c) 2009 Matteo Spinelli, http://cubiq.org/
7 * Released under MIT license
8 * http://cubiq.org/dropbox/mit-license.txt
10 * Version 3.2.1 - Last updated: 2010.06.03
16 function iScroll (el, options) {
17 this.element = typeof el == 'object' ? el : document.getElementById(el);
18 this.wrapper = this.element.parentNode;
20 this.element.style.webkitTransitionProperty = '-webkit-transform';
21 this.element.style.webkitTransitionTimingFunction = 'cubic-bezier(0,0,0.25,1)';
22 this.element.style.webkitTransitionDuration = '0';
23 this.element.style.webkitTransform = 'translate3d(0,0,0)';
28 checkDOMChanges: true,
29 topOnDOMChanges: false,
34 if (typeof options == 'object') {
35 for (var i in options) {
36 this.options[i] = options[i];
42 this.element.addEventListener('touchstart', this);
43 this.element.addEventListener('touchmove', this);
44 this.element.addEventListener('touchend', this);
45 window.addEventListener('orientationchange', this);
47 if (this.options.checkDOMChanges) {
48 this.element.addEventListener('DOMSubtreeModified', this);
56 handleEvent: function (e) {
58 case 'touchstart': this.onTouchStart(e); break;
59 case 'touchmove': this.onTouchMove(e); break;
60 case 'touchend': this.onTouchEnd(e); break;
61 case 'webkitTransitionEnd': this.onTransitionEnd(e); break;
62 case 'orientationchange': this.refresh(); break;
63 case 'DOMSubtreeModified': this.onDOMModified(e); break;
67 onDOMModified: function (e) {
70 if (this.options.topOnDOMChanges && (this.x!=0 || this.y!=0)) {
71 this.scrollTo(0,0,'0');
75 refresh: function () {
76 this.scrollWidth = this.wrapper.clientWidth;
77 this.scrollHeight = this.wrapper.clientHeight;
78 this.maxScrollX = this.scrollWidth - this.element.offsetWidth;
79 this.maxScrollY = this.scrollHeight - this.element.offsetHeight;
81 var resetX = this.x, resetY = this.y;
83 if (this.maxScrollX >= 0) {
85 } else if (this.x < this.maxScrollX) {
86 resetX = this.maxScrollX;
90 if (this.maxScrollY >= 0) {
92 } else if (this.y < this.maxScrollY) {
93 resetY = this.maxScrollY;
96 if (resetX!=this.x || resetY!=this.y) {
97 this.scrollTo(resetX,resetY,'0');
100 this.scrollX = this.element.offsetWidth > this.scrollWidth ? true : false;
101 this.scrollY = this.element.offsetHeight > this.scrollHeight ? true : false;
103 // Update horizontal scrollbar
104 if (this.options.hScrollBar && this.scrollX) {
105 this.scrollBarX = (this.scrollBarX instanceof scrollbar) ? this.scrollBarX : new scrollbar('horizontal', this.wrapper);
106 this.scrollBarX.init(this.scrollWidth, this.element.offsetWidth);
107 } else if (this.scrollBarX) {
108 this.scrollBarX = this.scrollBarX.remove();
111 // Update vertical scrollbar
112 if (this.options.vScrollBar && this.scrollY) {
113 this.scrollBarY = (this.scrollBarY instanceof scrollbar) ? this.scrollBarY : new scrollbar('vertical', this.wrapper);
114 this.scrollBarY.init(this.scrollHeight, this.element.offsetHeight);
115 } else if (this.scrollBarY) {
116 this.scrollBarY = this.scrollBarY.remove();
120 setPosition: function (x, y) {
121 this.x = x !== null ? x : this.x;
122 this.y = y !== null ? y : this.y;
124 this.element.style.webkitTransform = 'translate3d(' + this.x + 'px,' + this.y + 'px,0)';
126 // Move the scrollbars
127 if (this.scrollBarX) {
128 this.scrollBarX.setPosition(this.scrollBarX.maxScroll / this.maxScrollX * this.x);
130 if (this.scrollBarY) {
131 this.scrollBarY.setPosition(this.scrollBarY.maxScroll / this.maxScrollY * this.y);
135 onTouchStart: function(e) {
136 if (e.targetTouches.length != 1) {
142 this.element.style.webkitTransitionDuration = '0';
144 if (this.scrollBarX) {
145 this.scrollBarX.bar.style.webkitTransitionDuration = '0, 250ms';
147 if (this.scrollBarY) {
148 this.scrollBarY.bar.style.webkitTransitionDuration = '0, 250ms';
151 // Check if elem is really where it should be
152 var theTransform = new WebKitCSSMatrix(window.getComputedStyle(this.element).webkitTransform);
153 if (theTransform.m41 != this.x || theTransform.m42 != this.y) {
154 this.setPosition(theTransform.m41, theTransform.m42);
157 this.touchStartX = e.touches[0].pageX;
158 this.scrollStartX = this.x;
160 this.touchStartY = e.touches[0].pageY;
161 this.scrollStartY = this.y;
163 this.scrollStartTime = e.timeStamp;
167 onTouchMove: function(e) {
168 if (e.targetTouches.length != 1) {
174 var leftDelta = this.scrollX === true ? e.touches[0].pageX - this.touchStartX : 0,
175 topDelta = this.scrollY === true ? e.touches[0].pageY - this.touchStartY : 0,
176 newX = this.x + leftDelta,
177 newY = this.y + topDelta;
179 // Slow down if outside of the boundaries
180 if (newX > 0 || newX < this.maxScrollX) {
181 newX = this.options.bounce ? Math.round(this.x + leftDelta / 4) : this.x;
183 if (newY > 0 || newY < this.maxScrollY) {
184 newY = this.options.bounce ? Math.round(this.y + topDelta / 4) : this.y;
187 if (this.scrollBarX && !this.scrollBarX.visible) {
188 this.scrollBarX.show();
190 if (this.scrollBarY && !this.scrollBarY.visible) {
191 this.scrollBarY.show();
194 this.setPosition(newX, newY);
196 this.touchStartX = e.touches[0].pageX;
197 this.touchStartY = e.touches[0].pageY;
200 // Prevent slingshot effect
201 if( e.timeStamp-this.scrollStartTime > 250 ) {
202 this.scrollStartX = this.x;
203 this.scrollStartY = this.y;
204 this.scrollStartTime = e.timeStamp;
208 onTouchEnd: function(e) {
209 if (e.targetTouches.length > 0) {
214 // Find the last touched element
215 var theTarget = e.changedTouches[0].target;
216 if (theTarget.nodeType == 3) {
217 theTarget = theTarget.parentNode;
219 // Create the fake event
220 var theEvent = document.createEvent('MouseEvents');
221 theEvent.initMouseEvent("click", true, true, document.defaultView,
222 e.detail, e.screenX, e.screenY, e.clientX, e.clientY,
223 e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,
224 e.button, e.relatedTarget);
225 theTarget.dispatchEvent(theEvent);
229 var time = e.timeStamp - this.scrollStartTime;
231 var momentumX = this.scrollX === true
232 ? this.momentum(this.x - this.scrollStartX,
234 this.options.bounce ? -this.x + this.scrollWidth/4 : -this.x,
235 this.options.bounce ? this.x + this.element.offsetWidth - this.scrollWidth + this.scrollWidth/4 : this.x + this.element.offsetWidth - this.scrollWidth)
236 : { dist: 0, time: 0 };
238 var momentumY = this.scrollY === true
239 ? this.momentum(this.y - this.scrollStartY,
241 this.options.bounce ? -this.y + this.scrollHeight/4 : -this.y,
242 this.options.bounce ? this.y + this.element.offsetHeight - this.scrollHeight + this.scrollHeight/4 : this.y + this.element.offsetHeight - this.scrollHeight)
243 : { dist: 0, time: 0 };
245 if (!momentumX.dist && !momentumY.dist) {
246 this.resetPosition();
250 var newDuration = Math.max(Math.max(momentumX.time, momentumY.time), 1); // The minimum animation length must be 1ms
251 var newPositionX = this.x + momentumX.dist;
252 var newPositionY = this.y + momentumY.dist;
254 this.element.addEventListener('webkitTransitionEnd', this);
256 this.scrollTo(newPositionX, newPositionY, newDuration + 'ms');
258 // Move the scrollbars
259 if (this.scrollBarX) {
260 this.scrollBarX.scrollTo(this.scrollBarX.maxScroll / this.maxScrollX * newPositionX, newDuration + 'ms');
262 if (this.scrollBarY) {
263 this.scrollBarY.scrollTo(this.scrollBarY.maxScroll / this.maxScrollY * newPositionY, newDuration + 'ms');
267 onTransitionEnd: function () {
268 this.element.removeEventListener('webkitTransitionEnd', this);
269 this.resetPosition();
272 resetPosition: function () {
278 } else if (this.x < this.maxScrollX) {
279 resetX = this.maxScrollX;
284 } else if (this.y < this.maxScrollY) {
285 resetY = this.maxScrollY;
288 if (resetX != this.x || resetY != this.y) {
289 this.scrollTo(resetX, resetY, '500ms');
291 if (this.scrollBarX && resetX != this.x) {
292 this.scrollBarX.scrollTo(this.scrollBarX.maxScroll / this.maxScrollX * resetX, '500ms');
294 if (this.scrollBarY && resetY != this.y) {
295 this.scrollBarY.scrollTo(this.scrollBarY.maxScroll / this.maxScrollY * resetY, '500ms');
299 // Hide the scrollbars
300 if (this.scrollBarX) {
301 this.scrollBarX.hide();
303 if (this.scrollBarY) {
304 this.scrollBarY.hide();
308 scrollTo: function (destX, destY, runtime) {
309 this.element.style.webkitTransitionDuration = runtime || '400ms';
310 this.setPosition(destX, destY);
313 momentum: function (dist, time, maxDistUpper, maxDistLower) {
316 speed = Math.abs(dist) / time * 1000,
317 newDist = speed * speed / (20 * friction) / 1000;
319 // Proportinally reduce speed if we are outside of the boundaries
320 if (dist > 0 && newDist > maxDistUpper) {
321 speed = speed * maxDistUpper / newDist;
322 newDist = maxDistUpper;
324 if (dist < 0 && newDist > maxDistLower) {
325 speed = speed * maxDistLower / newDist;
326 newDist = maxDistLower;
329 newDist = newDist * (dist < 0 ? -1 : 1);
331 var newTime = speed / deceleration;
333 return { dist: Math.round(newDist), time: Math.round(newTime) };
337 var scrollbar = function (dir, wrapper) {
339 this.bar = document.createElement('div');
340 this.bar.className = 'scrollbar ' + dir;
341 this.bar.style.webkitTransitionTimingFunction = 'cubic-bezier(0,0,0.25,1)';
342 this.bar.style.webkitTransform = 'translate3d(0,0,0)';
343 this.bar.style.webkitTransitionProperty = '-webkit-transform,opacity';
344 this.bar.style.webkitTransitionDuration = '0,300ms';
345 this.bar.style.pointerEvents = 'none';
346 this.bar.style.opacity = '0';
348 wrapper.appendChild(this.bar);
351 scrollbar.prototype = {
357 init: function (scroll, size) {
358 var offset = this.dir == 'horizontal' ? this.bar.offsetWidth - this.bar.clientWidth : this.bar.offsetHeight - this.bar.clientHeight;
359 this.maxSize = scroll - 8; // 8 = distance from top + distance from bottom
360 this.size = Math.round(this.maxSize * this.maxSize / size) + offset;
361 this.maxScroll = this.maxSize - this.size;
362 this.bar.style[this.dir == 'horizontal' ? 'width' : 'height'] = (this.size - offset) + 'px';
365 setPosition: function (pos) {
368 } else if (pos > this.maxScroll) {
369 pos = this.maxScroll;
372 pos = this.dir == 'horizontal' ? 'translate3d(' + Math.round(pos) + 'px,0,0)' : 'translate3d(0,' + Math.round(pos) + 'px,0)';
373 this.bar.style.webkitTransform = pos;
376 scrollTo: function (pos, runtime) {
377 this.bar.style.webkitTransitionDuration = (runtime || '400ms') + ',300ms';
378 this.setPosition(pos);
383 this.bar.style.opacity = '1';
387 this.visible = false;
388 this.bar.style.opacity = '0';
391 remove: function () {
392 this.bar.parentNode.removeChild(this.bar);
397 // Expose iScroll to the world
398 window.iScroll = iScroll;