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,
35 if (typeof options == 'object') {
36 for (var i in options) {
37 this.options[i] = options[i];
43 this.element.addEventListener('touchstart', this);
44 this.element.addEventListener('touchmove', this);
45 this.element.addEventListener('touchend', this);
46 window.addEventListener('orientationchange', this);
48 if (this.options.checkDOMChanges) {
49 this.element.addEventListener('DOMSubtreeModified', this);
57 handleEvent: function (e) {
59 case 'touchstart': this.onTouchStart(e); break;
60 case 'touchmove': this.onTouchMove(e); break;
61 case 'touchend': this.onTouchEnd(e); break;
62 case 'webkitTransitionEnd': this.onTransitionEnd(e); break;
63 case 'orientationchange': this.refresh(); break;
64 case 'DOMSubtreeModified': this.onDOMModified(e); break;
68 onDOMModified: function (e) {
71 if (this.options.topOnDOMChanges && (this.x!=0 || this.y!=0)) {
72 this.scrollTo(0,0,'0');
76 refresh: function () {
77 this.scrollWidth = this.wrapper.clientWidth;
78 this.scrollHeight = this.wrapper.clientHeight;
79 this.maxScrollX = this.scrollWidth - this.element.offsetWidth;
80 this.maxScrollY = this.scrollHeight - this.element.offsetHeight;
82 var resetX = this.x, resetY = this.y;
84 if (this.maxScrollX >= 0) {
86 } else if (this.x < this.maxScrollX) {
87 resetX = this.maxScrollX;
91 if (this.maxScrollY >= 0) {
93 } else if (this.y < this.maxScrollY) {
94 resetY = this.maxScrollY;
97 if (resetX!=this.x || resetY!=this.y) {
98 this.scrollTo(resetX,resetY,'0');
101 this.scrollX = this.element.offsetWidth > this.scrollWidth ? true : false;
102 this.scrollY = this.element.offsetHeight > this.scrollHeight ? true : false;
104 // Update horizontal scrollbar
105 if (this.options.hScrollBar && this.scrollX) {
106 this.scrollBarX = (this.scrollBarX instanceof scrollbar) ? this.scrollBarX : new scrollbar('horizontal', this.wrapper);
107 this.scrollBarX.init(this.scrollWidth, this.element.offsetWidth);
108 } else if (this.scrollBarX) {
109 this.scrollBarX = this.scrollBarX.remove();
112 // Update vertical scrollbar
113 if (this.options.vScrollBar && this.scrollY) {
114 this.scrollBarY = (this.scrollBarY instanceof scrollbar) ? this.scrollBarY : new scrollbar('vertical', this.wrapper);
115 this.scrollBarY.init(this.scrollHeight, this.element.offsetHeight);
116 } else if (this.scrollBarY) {
117 this.scrollBarY = this.scrollBarY.remove();
121 setPosition: function (x, y) {
122 this.x = x !== null ? x : this.x;
123 this.y = y !== null ? y : this.y;
125 this.element.style.webkitTransform = 'translate3d(' + this.x + 'px,' + this.y + 'px,0)';
127 // Move the scrollbars
128 if (this.scrollBarX) {
129 this.scrollBarX.setPosition(this.scrollBarX.maxScroll / this.maxScrollX * this.x);
131 if (this.scrollBarY) {
132 this.scrollBarY.setPosition(this.scrollBarY.maxScroll / this.maxScrollY * this.y);
136 onTouchStart: function(e) {
137 if (e.targetTouches.length != 1) {
141 /* iOS 4 以外 特に androidだと ここで preventDefault しないと
142 まともにスクロールしない/ブラウザがスクロールしてしまう/
143 スクロールしたあとの位置にあるリンクがクリックされちゃうとか
144 いろいろ起きる。逆に iOS4だと ここで preventDefault しちゃうと
145 長押ししたときの ディフォルトのポップアップが出てこなくなる */
146 /* iOS4 意外だと ディフォルトのポップアップが使えなくなるが どうやら 打つ手なし... */
147 if (!navigator.appVersion.match(/iPhone OS 4/)) {
152 this.element.style.webkitTransitionDuration = '0';
154 if (this.scrollBarX) {
155 this.scrollBarX.bar.style.webkitTransitionDuration = '0, 250ms';
157 if (this.scrollBarY) {
158 this.scrollBarY.bar.style.webkitTransitionDuration = '0, 250ms';
161 // Check if elem is really where it should be
162 var theTransform = new WebKitCSSMatrix(window.getComputedStyle(this.element).webkitTransform);
163 if (theTransform.m41 != this.x || theTransform.m42 != this.y) {
164 this.setPosition(theTransform.m41, theTransform.m42);
167 this.touchStartX = e.touches[0].pageX;
168 this.scrollStartX = this.x;
170 this.touchStartY = e.touches[0].pageY;
171 this.scrollStartY = this.y;
173 this.scrollStartTime = e.timeStamp;
177 onTouchMove: function(e) {
178 if (e.targetTouches.length != 1) {
182 /* touchstart の preventDefault やめて ここに追加すると iPhone4 だといい感じ*/
183 if (navigator.appVersion.match(/iPhone OS 4/)) {
186 var leftDelta = this.scrollX === true ? e.touches[0].pageX - this.touchStartX : 0,
187 topDelta = this.scrollY === true ? e.touches[0].pageY - this.touchStartY : 0,
188 newX = this.x + leftDelta,
189 newY = this.y + topDelta;
191 if (Math.abs(topDelta) > this.options.min_move || Math.abs(leftDelta) > this.options.min_move) {
192 // Slow down if outside of the boundaries
193 if (newX > 0 || newX < this.maxScrollX) {
194 newX = this.options.bounce ? Math.round(this.x + leftDelta / 4) : this.x;
196 if (newY > 0 || newY < this.maxScrollY) {
197 newY = this.options.bounce ? Math.round(this.y + topDelta / 4) : this.y;
200 if (this.scrollBarX && !this.scrollBarX.visible) {
201 this.scrollBarX.show();
203 if (this.scrollBarY && !this.scrollBarY.visible) {
204 this.scrollBarY.show();
207 this.setPosition(newX, newY);
209 this.touchStartX = e.touches[0].pageX;
210 this.touchStartY = e.touches[0].pageY;
213 // Prevent slingshot effect
214 if( e.timeStamp-this.scrollStartTime > 250 ) {
215 this.scrollStartX = this.x;
216 this.scrollStartY = this.y;
217 this.scrollStartTime = e.timeStamp;
222 onTouchEnd: function(e) {
223 if (e.targetTouches.length > 0) {
228 /* iOS4 では touchStart イベントは preventDefault() してないので
229 click イベントを発生させちゃうと 2重にクリックが発生したような
231 if (!navigator.appVersion.match(/iPhone OS 4/)) {
232 // Find the last touched element
233 var theTarget = e.changedTouches[0].target;
234 if (theTarget.nodeType == 3) {
235 theTarget = theTarget.parentNode;
237 // Create the fake event
238 var theEvent = document.createEvent('MouseEvents');
239 theEvent.initMouseEvent("click", true, true, document.defaultView,
240 e.detail, e.screenX, e.screenY, e.clientX, e.clientY,
241 e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,
242 e.button, e.relatedTarget);
243 theTarget.dispatchEvent(theEvent);
249 var time = e.timeStamp - this.scrollStartTime;
251 var momentumX = this.scrollX === true
252 ? this.momentum(this.x - this.scrollStartX,
254 this.options.bounce ? -this.x + this.scrollWidth/4 : -this.x,
255 this.options.bounce ? this.x + this.element.offsetWidth - this.scrollWidth + this.scrollWidth/4 : this.x + this.element.offsetWidth - this.scrollWidth)
256 : { dist: 0, time: 0 };
258 var momentumY = this.scrollY === true
259 ? this.momentum(this.y - this.scrollStartY,
261 this.options.bounce ? -this.y + this.scrollHeight/4 : -this.y,
262 this.options.bounce ? this.y + this.element.offsetHeight - this.scrollHeight + this.scrollHeight/4 : this.y + this.element.offsetHeight - this.scrollHeight)
263 : { dist: 0, time: 0 };
265 if (!momentumX.dist && !momentumY.dist) {
266 this.resetPosition();
270 var newDuration = Math.max(Math.max(momentumX.time, momentumY.time), 1); // The minimum animation length must be 1ms
271 var newPositionX = this.x + momentumX.dist;
272 var newPositionY = this.y + momentumY.dist;
274 this.element.addEventListener('webkitTransitionEnd', this);
276 this.scrollTo(newPositionX, newPositionY, newDuration + 'ms');
278 // Move the scrollbars
279 if (this.scrollBarX) {
280 this.scrollBarX.scrollTo(this.scrollBarX.maxScroll / this.maxScrollX * newPositionX, newDuration + 'ms');
282 if (this.scrollBarY) {
283 this.scrollBarY.scrollTo(this.scrollBarY.maxScroll / this.maxScrollY * newPositionY, newDuration + 'ms');
287 onTransitionEnd: function () {
288 this.element.removeEventListener('webkitTransitionEnd', this);
289 this.resetPosition();
292 resetPosition: function () {
298 } else if (this.x < this.maxScrollX) {
299 resetX = this.maxScrollX;
304 } else if (this.y < this.maxScrollY) {
305 resetY = this.maxScrollY;
308 if (resetX != this.x || resetY != this.y) {
309 this.scrollTo(resetX, resetY, '500ms');
311 if (this.scrollBarX && resetX != this.x) {
312 this.scrollBarX.scrollTo(this.scrollBarX.maxScroll / this.maxScrollX * resetX, '500ms');
314 if (this.scrollBarY && resetY != this.y) {
315 this.scrollBarY.scrollTo(this.scrollBarY.maxScroll / this.maxScrollY * resetY, '500ms');
319 // Hide the scrollbars
320 if (this.scrollBarX) {
321 this.scrollBarX.hide();
323 if (this.scrollBarY) {
324 this.scrollBarY.hide();
328 scrollTo: function (destX, destY, runtime) {
329 this.element.style.webkitTransitionDuration = runtime || '400ms';
330 this.setPosition(destX, destY);
333 momentum: function (dist, time, maxDistUpper, maxDistLower) {
336 speed = Math.abs(dist) / time * 1000,
337 newDist = speed * speed / (20 * friction) / 1000;
339 // Proportinally reduce speed if we are outside of the boundaries
340 if (dist > 0 && newDist > maxDistUpper) {
341 speed = speed * maxDistUpper / newDist;
342 newDist = maxDistUpper;
344 if (dist < 0 && newDist > maxDistLower) {
345 speed = speed * maxDistLower / newDist;
346 newDist = maxDistLower;
349 newDist = newDist * (dist < 0 ? -1 : 1);
351 var newTime = speed / deceleration;
353 return { dist: Math.round(newDist), time: Math.round(newTime) };
357 var scrollbar = function (dir, wrapper) {
359 this.bar = document.createElement('div');
360 this.bar.className = 'scrollbar ' + dir;
361 this.bar.style.webkitTransitionTimingFunction = 'cubic-bezier(0,0,0.25,1)';
362 this.bar.style.webkitTransform = 'translate3d(0,0,0)';
363 this.bar.style.webkitTransitionProperty = '-webkit-transform,opacity';
364 this.bar.style.webkitTransitionDuration = '0,300ms';
365 this.bar.style.pointerEvents = 'none';
366 this.bar.style.opacity = '0';
368 wrapper.appendChild(this.bar);
371 scrollbar.prototype = {
377 init: function (scroll, size) {
378 var offset = this.dir == 'horizontal' ? this.bar.offsetWidth - this.bar.clientWidth : this.bar.offsetHeight - this.bar.clientHeight;
379 this.maxSize = scroll - 8; // 8 = distance from top + distance from bottom
380 this.size = Math.round(this.maxSize * this.maxSize / size) + offset;
381 this.maxScroll = this.maxSize - this.size;
382 this.bar.style[this.dir == 'horizontal' ? 'width' : 'height'] = (this.size - offset) + 'px';
385 setPosition: function (pos) {
388 } else if (pos > this.maxScroll) {
389 pos = this.maxScroll;
392 pos = this.dir == 'horizontal' ? 'translate3d(' + Math.round(pos) + 'px,0,0)' : 'translate3d(0,' + Math.round(pos) + 'px,0)';
393 this.bar.style.webkitTransform = pos;
396 scrollTo: function (pos, runtime) {
397 this.bar.style.webkitTransitionDuration = (runtime || '400ms') + ',300ms';
398 this.setPosition(pos);
403 this.bar.style.opacity = '1';
407 this.visible = false;
408 this.bar.style.opacity = '0';
411 remove: function () {
412 this.bar.parentNode.removeChild(this.bar);
417 // Expose iScroll to the world
418 window.iScroll = iScroll;