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.1beta1 - Last updated: 2010.05.06
14 function iScroll (el, options) {
15 this.element = typeof el == 'object' ? el : document.getElementById(el);
16 this.wrapper = this.element.parentNode;
18 this.wrapper.style.overflow = 'hidden';
19 this.wrapper.style.position = 'relative';
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)';
32 if (typeof options == 'object') {
33 for (var i in options) {
34 this.options[i] = options[i];
40 this.element.addEventListener('touchstart', this);
41 this.element.addEventListener('touchmove', this);
42 this.element.addEventListener('touchend', this);
43 this.element.addEventListener('DOMSubtreeModified', this);
44 window.addEventListener('orientationchange', this);
51 handleEvent: function (e) {
53 case 'touchstart': this.onTouchStart(e); break;
54 case 'touchmove': this.onTouchMove(e); break;
55 case 'touchend': this.onTouchEnd(e); break;
56 case 'webkitTransitionEnd': this.onTransitionEnd(e); break;
57 case 'orientationchange': this.refresh(); /*this.scrollTo(0,0,'0');*/ break;
58 case 'DOMSubtreeModified': this.refresh(); break;
62 refresh: function () {
63 this.element.style.webkitTransitionDuration = '0';
64 this.scrollWidth = this.wrapper.clientWidth;
65 this.scrollHeight = this.wrapper.clientHeight;
66 this.maxScrollX = this.scrollWidth - this.element.offsetWidth;
67 this.maxScrollY = this.scrollHeight - this.element.offsetHeight;
69 var resetX = this.x, resetY = this.y;
70 if (this.scrollX && this.x < this.maxScrollX) {
71 resetX = this.maxScrollX;
73 if (this.scrollY && this.y < this.maxScrollY) {
74 resetY = this.maxScrollY;
76 // If content width/lenght has been modified, reset scroller position
77 this.scrollTo(resetX,resetY,'0');
79 this.scrollX = this.element.offsetWidth > this.scrollWidth ? true : false;
80 this.scrollY = this.element.offsetHeight > this.scrollHeight ? true : false;
82 // Update horizontal scrollbar
83 if (this.options.hScrollBar && this.scrollX) {
84 this.scrollBarX = new scrollbar('horizontal', this.wrapper);
85 this.scrollBarX.init(this.scrollWidth, this.element.offsetWidth);
86 } else if (this.scrollBarX) {
87 this.scrollBarX = this.scrollBarX.remove();
90 // Update vertical scrollbar
91 if (this.options.vScrollBar && this.scrollY) {
92 this.scrollBarY = new scrollbar('vertical', this.wrapper);
93 this.scrollBarY.init(this.scrollHeight, this.element.offsetHeight);
94 } else if (this.scrollBarY) {
95 this.scrollBarY = this.scrollBarY.remove();
107 setPosition: function (x, y) {
108 this._x = x !== null ? x : this._x;
109 this._y = y !== null ? y : this._y;
111 this.element.style.webkitTransform = 'translate3d(' + this._x + 'px,' + this._y + 'px,0)';
113 // Move the scrollbars
114 if (this.scrollBarX) {
115 this.scrollBarX.setPosition(this.scrollBarX.maxScroll / this.maxScrollX * this._x);
117 if (this.scrollBarY) {
118 this.scrollBarY.setPosition(this.scrollBarY.maxScroll / this.maxScrollY * this._y);
122 onTouchStart: function(e) {
123 if (e.targetTouches.length != 1) {
130 this.element.style.webkitTransitionDuration = '0';
132 if (this.scrollBarX) {
133 this.scrollBarX.bar.style.webkitTransitionDuration = '0, 250ms';
135 if (this.scrollBarY) {
136 this.scrollBarY.bar.style.webkitTransitionDuration = '0, 250ms';
139 // Check if elem is really where it should be
140 var theTransform = new WebKitCSSMatrix(window.getComputedStyle(this.element).webkitTransform);
141 if (theTransform.m41 != this.x || theTransform.m42 != this.y) {
142 this.setPosition(theTransform.m41, theTransform.m42);
145 this.touchStartX = e.touches[0].pageX;
146 this.scrollStartX = this.x;
148 this.touchStartY = e.touches[0].pageY;
149 this.scrollStartY = this.y;
151 this.scrollStartTime = e.timeStamp;
155 onTouchMove: function(e) {
156 if (e.targetTouches.length != 1) {
160 var leftDelta = this.scrollX === true ? e.touches[0].pageX - this.touchStartX : 0;
161 var topDelta = this.scrollY === true ? e.touches[0].pageY - this.touchStartY : 0;
163 if (this.x > 0 || this.x < this.maxScrollX) {
164 leftDelta = Math.round(leftDelta / 4); // Slow down if outside of the boundaries
167 if (this.y > 0 || this.y < this.maxScrollY) {
168 topDelta = Math.round(topDelta / 4); // Slow down if outside of the boundaries
171 if (this.scrollBarX && !this.scrollBarX.visible) {
172 this.scrollBarX.show();
174 if (this.scrollBarY && !this.scrollBarY.visible) {
175 this.scrollBarY.show();
178 this.setPosition(this.x + leftDelta, this.y + topDelta);
180 this.touchStartX = e.touches[0].pageX;
181 this.touchStartY = e.touches[0].pageY;
184 // Prevent slingshot effect
185 if( e.timeStamp-this.scrollStartTime > 250 ) {
186 this.scrollStartX = this.x;
187 this.scrollStartY = this.y;
188 this.scrollStartTime = e.timeStamp;
192 onTouchEnd: function(e) {
193 if (e.targetTouches.length > 0) {
198 var theEvent = document.createEvent('MouseEvents');
199 theEvent.initMouseEvent("click", true, true, document.defaultView, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null);
200 e.changedTouches[0].target.dispatchEvent(theEvent);
204 var time = e.timeStamp - this.scrollStartTime;
206 var momentumX = this.scrollX === true
207 ? this.momentum(this.x - this.scrollStartX,
210 this.x + this.element.offsetWidth - this.scrollWidth + 50)
211 : { dist: 0, time: 0 };
213 var momentumY = this.scrollY === true
214 ? this.momentum(this.y - this.scrollStartY,
216 -this.y + /*this.scrollHeight/3*/ 50,
217 this.y + this.element.offsetHeight - this.scrollHeight + /*this.scrollHeight/3*/ 50)
218 : { dist: 0, time: 0 };
220 if (!momentumX.dist && !momentumY.dist) {
221 this.onTransitionEnd(); // I know, I know... This is lame
225 var newDuration = Math.max(momentumX.time, momentumY.time);
226 var newPositionX = this.x + momentumX.dist;
227 var newPositionY = this.y + momentumY.dist;
229 this.element.addEventListener('webkitTransitionEnd', this);
231 this.scrollTo(newPositionX, newPositionY, newDuration + 'ms');
233 // Move the scrollbars
234 if (this.scrollBarX) {
235 this.scrollBarX.scrollTo(this.scrollBarX.maxScroll / this.maxScrollX * newPositionX, newDuration + 'ms');
237 if (this.scrollBarY) {
238 this.scrollBarY.scrollTo(this.scrollBarY.maxScroll / this.maxScrollY * newPositionY, newDuration + 'ms');
242 onTransitionEnd: function () {
243 this.element.removeEventListener('webkitTransitionEnd', this);
244 this.resetPosition();
246 // Hide the scrollbars
247 if (this.scrollBarX) {
248 this.scrollBarX.hide();
250 if (this.scrollBarY) {
251 this.scrollBarY.hide();
255 resetPosition: function () {
256 var resetX = resetY = null;
257 if (this.x > 0 || this.x < this.maxScrollX) {
258 resetX = this.x >= 0 ? 0 : this.maxScrollX;
261 if (this.y > 0 || this.y < this.maxScrollY) {
262 resetY = this.y >= 0 ? 0 : this.maxScrollY;
265 if (resetX !== null || resetY !== null) {
266 this.scrollTo(resetX, resetY, '500ms');
268 if (this.scrollBarX) {
269 this.scrollBarX.scrollTo(this.scrollBarX.maxScroll / this.maxScrollX * (resetX || this.x), '500ms');
271 if (this.scrollBarY) {
272 this.scrollBarY.scrollTo(this.scrollBarY.maxScroll / this.maxScrollY * (resetY || this.y), '500ms');
277 scrollTo: function (destX, destY, runtime) {
278 this.element.style.webkitTransitionDuration = runtime || '400ms';
279 this.setPosition(destX, destY);
282 momentum: function (dist, time, maxDist1, maxDist2) {
286 var speed = Math.abs(dist) / time * 1000;
287 var newDist = speed * speed / (20 * friction) / 1000;
289 // Proportinally reduce speed if we are outside of the boundaries
290 if (dist > 0 && maxDist1 !== undefined && newDist > maxDist1) {
291 speed = speed * maxDist1 / newDist;
294 if (dist < 0 && maxDist2 !== undefined && newDist > maxDist2) {
295 speed = speed * maxDist2 / newDist;
299 newDist = newDist * (dist < 0 ? -1 : 1);
301 var newTime = -speed / -deceleration;
302 if (newTime < 1) { // We can't go back in time
306 return { dist: Math.round(newDist), time: Math.round(newTime) };
310 var scrollbar = function (dir, wrapper) {
312 this.bar = document.createElement('div');
313 this.bar.className = 'scrollbar ' + dir;
314 this.bar.style.webkitTransitionTimingFunction = 'cubic-bezier(0,0,0.25,1)';
315 this.bar.style.webkitTransform = 'translate3d(0,0,0)';
316 this.bar.style.webkitTransitionProperty = '-webkit-transform,opacity';
317 this.bar.style.webkitTransitionDuration = '0,250ms';
318 this.bar.style.pointerEvents = 'none';
319 this.bar.style.opacity = '0';
321 wrapper.appendChild(this.bar);
324 scrollbar.prototype = {
330 init: function (scroll, size) {
331 var offset = this.dir == 'horizontal' ? this.bar.offsetWidth - this.bar.clientWidth : this.bar.offsetHeight - this.bar.clientHeight;
332 this.maxSize = scroll - 8; // 8 = distance from top + distance from bottom
333 this.size = Math.round(this.maxSize * this.maxSize / size) + offset;
334 this.maxScroll = this.maxSize - this.size;
335 this.bar.style[this.dir == 'horizontal' ? 'width' : 'height'] = (this.size - offset) + 'px';
338 setPosition: function (pos) {
341 } else if (pos > this.maxScroll) {
342 pos = this.maxScroll;
345 pos = this.dir == 'horizontal' ? 'translate3d(' + Math.round(pos) + 'px,0,0)' : 'translate3d(0,' + Math.round(pos) + 'px,0)';
346 this.bar.style.webkitTransform = pos;
349 scrollTo: function (pos, runtime) {
350 this.bar.style.webkitTransitionDuration = (runtime || '400ms') + ',250ms';
351 this.setPosition(pos);
356 this.bar.style.opacity = '1';
360 this.visible = false;
361 this.bar.style.opacity = '0';
364 remove: function () {
365 this.bar.parentNode.removeChild(this.bar);