3 * Find more about the scrolling function at
4 * http://cubiq.org/iscroll
6 * Copyright (c) 2010 Matteo Spinelli, http://cubiq.org/
7 * Released under MIT license
8 * http://cubiq.org/dropbox/mit-license.txt
10 * Version 3.7.1 - Last updated: 2010.10.08
15 function iScroll (el, options) {
17 that.element = typeof el == 'object' ? el : document.getElementById(el);
18 that.wrapper = that.element.parentNode;
20 that.element.style.webkitTransitionProperty = '-webkit-transform';
21 that.element.style.webkitTransitionTimingFunction = 'cubic-bezier(0,0,0.25,1)';
22 that.element.style.webkitTransitionDuration = '0';
23 that.element.style.webkitTransform = translateOpen + '0,0' + translateClose;
29 checkDOMChanges: true,
30 topOnDOMChanges: false,
33 fadeScrollbar: isIthing || !isTouch,
34 shrinkScrollbar: isIthing || !isTouch,
35 desktopCompatibility: false,
39 scrollbarColor: 'rgba(0,0,0,0.5)',
40 onScrollEnd: function () {}
43 // User defined options
44 if (typeof options == 'object') {
46 that.options[i] = options[i];
50 if (that.options.desktopCompatibility) {
51 that.options.overflow = 'hidden';
54 that.onScrollEnd = that.options.onScrollEnd;
55 delete that.options.onScrollEnd;
57 that.wrapper.style.overflow = that.options.overflow;
61 window.addEventListener('onorientationchange' in window ? 'orientationchange' : 'resize', that, false);
63 if (isTouch || that.options.desktopCompatibility) {
64 that.element.addEventListener(START_EVENT, that, false);
65 that.element.addEventListener(MOVE_EVENT, that, false);
66 that.element.addEventListener(END_EVENT, that, false);
69 if (that.options.checkDOMChanges) {
70 that.element.addEventListener('DOMSubtreeModified', that, false);
79 handleEvent: function (e) {
92 case 'webkitTransitionEnd':
95 case 'orientationchange':
99 case 'DOMSubtreeModified':
100 that.onDOMModified(e);
105 onDOMModified: function (e) {
108 // (Hopefully) execute onDOMModified only once
109 if (e.target.parentNode != that.element) {
113 setTimeout(function () { that.refresh(); }, 0);
115 if (that.options.topOnDOMChanges && (that.x!=0 || that.y!=0)) {
116 that.scrollTo(0,0,'0');
120 refresh: function () {
122 resetX = that.x, resetY = that.y,
125 that.scrollWidth = that.wrapper.clientWidth;
126 that.scrollHeight = that.wrapper.clientHeight;
127 that.scrollerWidth = that.element.offsetWidth;
128 that.scrollerHeight = that.element.offsetHeight;
129 that.maxScrollX = that.scrollWidth - that.scrollerWidth;
130 that.maxScrollY = that.scrollHeight - that.scrollerHeight;
135 if (that.maxScrollX >= 0) {
137 } else if (that.x < that.maxScrollX) {
138 resetX = that.maxScrollX;
142 if (that.maxScrollY >= 0) {
144 } else if (that.y < that.maxScrollY) {
145 resetY = that.maxScrollY;
150 if (that.options.snap) {
151 that.maxPageX = -Math.floor(that.maxScrollX/that.scrollWidth);
152 that.maxPageY = -Math.floor(that.maxScrollY/that.scrollHeight);
154 snap = that.snap(resetX, resetY);
159 if (resetX!=that.x || resetY!=that.y) {
160 that.setTransitionTime('0');
161 that.setPosition(resetX, resetY, true);
164 that.scrollX = that.scrollerWidth > that.scrollWidth;
165 that.scrollY = !that.options.bounceLock && !that.scrollX || that.scrollerHeight > that.scrollHeight;
167 // Update horizontal scrollbar
168 if (that.options.hScrollbar && that.scrollX) {
169 that.scrollBarX = that.scrollBarX || new scrollbar('horizontal', that.wrapper, that.options.fadeScrollbar, that.options.shrinkScrollbar, that.options.scrollbarColor);
170 that.scrollBarX.init(that.scrollWidth, that.scrollerWidth);
171 } else if (that.scrollBarX) {
172 that.scrollBarX = that.scrollBarX.remove();
175 // Update vertical scrollbar
176 if (that.options.vScrollbar && that.scrollY && that.scrollerHeight > that.scrollHeight) {
177 that.scrollBarY = that.scrollBarY || new scrollbar('vertical', that.wrapper, that.options.fadeScrollbar, that.options.shrinkScrollbar, that.options.scrollbarColor);
178 that.scrollBarY.init(that.scrollHeight, that.scrollerHeight);
179 } else if (that.scrollBarY) {
180 that.scrollBarY = that.scrollBarY.remove();
184 setPosition: function (x, y, hideScrollBars) {
190 that.element.style.webkitTransform = translateOpen + that.x + 'px,' + that.y + 'px' + translateClose;
192 // Move the scrollbars
193 if (!hideScrollBars) {
194 if (that.scrollBarX) {
195 that.scrollBarX.setPosition(that.x);
197 if (that.scrollBarY) {
198 that.scrollBarY.setPosition(that.y);
203 setTransitionTime: function(time) {
207 that.element.style.webkitTransitionDuration = time;
209 if (that.scrollBarX) {
210 that.scrollBarX.bar.style.webkitTransitionDuration = time;
211 that.scrollBarX.wrapper.style.webkitTransitionDuration = has3d && that.options.fadeScrollbar ? '300ms' : '0';
213 if (that.scrollBarY) {
214 that.scrollBarY.bar.style.webkitTransitionDuration = time;
215 that.scrollBarY.wrapper.style.webkitTransitionDuration = has3d && that.options.fadeScrollbar ? '300ms' : '0';
219 touchStart: function(e) {
230 that.scrolling = true; // This is probably not needed, but may be useful if iScroll is used in conjuction with other frameworks
236 that.setTransitionTime('0');
238 // Check if the scroller is really where it should be
239 if (that.options.momentum || that.options.snap) {
240 matrix = new WebKitCSSMatrix(window.getComputedStyle(that.element).webkitTransform);
241 if (matrix.e != that.x || matrix.f != that.y) {
242 document.removeEventListener('webkitTransitionEnd', that, false);
243 that.setPosition(matrix.e, matrix.f);
248 that.touchStartX = isTouch ? e.changedTouches[0].pageX : e.pageX;
249 that.scrollStartX = that.x;
251 that.touchStartY = isTouch ? e.changedTouches[0].pageY : e.pageY;
252 that.scrollStartY = that.y;
254 that.scrollStartTime = e.timeStamp;
260 touchMove: function(e) {
261 if (!this.scrolling) {
266 pageX = isTouch ? e.changedTouches[0].pageX : e.pageX,
267 pageY = isTouch ? e.changedTouches[0].pageY : e.pageY,
268 leftDelta = that.scrollX ? pageX - that.touchStartX : 0,
269 topDelta = that.scrollY ? pageY - that.touchStartY : 0,
270 newX = that.x + leftDelta,
271 newY = that.y + topDelta;
273 //e.preventDefault();
274 e.stopPropagation(); // Stopping propagation just saves some cpu cycles (I presume)
276 that.touchStartX = pageX;
277 that.touchStartY = pageY;
279 // Slow down if outside of the boundaries
280 if (newX >= 0 || newX < that.maxScrollX) {
281 newX = that.options.bounce ? Math.round(that.x + leftDelta / 3) : (newX >= 0 || that.maxScrollX>=0) ? 0 : that.maxScrollX;
283 if (newY >= 0 || newY < that.maxScrollY) {
284 newY = that.options.bounce ? Math.round(that.y + topDelta / 3) : (newY >= 0 || that.maxScrollY>=0) ? 0 : that.maxScrollY;
287 if (that.distX + that.distY > 5) { // 5 pixels threshold
289 // Lock scroll direction
290 if (that.distX-3 > that.distY) {
293 } else if (that.distY-3 > that.distX) {
298 that.setPosition(newX, newY);
300 that.directionX = leftDelta > 0 ? -1 : 1;
301 that.directionY = topDelta > 0 ? -1 : 1;
303 that.distX+= Math.abs(leftDelta);
304 that.distY+= Math.abs(topDelta);
305 // that.dist+= Math.abs(leftDelta) + Math.abs(topDelta);
309 touchEnd: function(e) {
310 if (!this.scrolling) {
315 time = e.timeStamp - that.scrollStartTime,
316 point = isTouch ? e.changedTouches[0] : e,
318 momentumX, momentumY,
320 newPositionX = that.x, newPositionY = that.y,
323 that.scrolling = false;
326 that.resetPosition();
329 // Find the last touched element
330 target = point.target;
331 while (target.nodeType != 1) {
332 target = target.parentNode;
335 // Create the fake event
336 ev = document.createEvent('MouseEvents');
337 ev.initMouseEvent('click', true, true, e.view, 1,
338 point.screenX, point.screenY, point.clientX, point.clientY,
339 e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,
342 target.dispatchEvent(ev);
348 if (!that.options.snap && time > 250) { // Prevent slingshot effect
349 that.resetPosition();
353 if (that.options.momentum) {
354 momentumX = that.scrollX === true
355 ? that.momentum(that.x - that.scrollStartX,
357 that.options.bounce ? -that.x + that.scrollWidth/5 : -that.x,
358 that.options.bounce ? that.x + that.scrollerWidth - that.scrollWidth + that.scrollWidth/5 : that.x + that.scrollerWidth - that.scrollWidth)
359 : { dist: 0, time: 0 };
361 momentumY = that.scrollY === true
362 ? that.momentum(that.y - that.scrollStartY,
364 that.options.bounce ? -that.y + that.scrollHeight/5 : -that.y,
365 that.options.bounce ? (that.maxScrollY < 0 ? that.y + that.scrollerHeight - that.scrollHeight : 0) + that.scrollHeight/5 : that.y + that.scrollerHeight - that.scrollHeight)
366 : { dist: 0, time: 0 };
368 newDuration = Math.max(Math.max(momentumX.time, momentumY.time), 1); // The minimum animation length must be 1ms
369 newPositionX = that.x + momentumX.dist;
370 newPositionY = that.y + momentumY.dist;
373 if (that.options.snap) {
374 snap = that.snap(newPositionX, newPositionY);
375 newPositionX = snap.x;
376 newPositionY = snap.y;
377 newDuration = Math.max(snap.time, newDuration);
380 that.scrollTo(newPositionX, newPositionY, newDuration + 'ms');
383 transitionEnd: function () {
385 document.removeEventListener('webkitTransitionEnd', that, false);
386 that.resetPosition();
389 resetPosition: function () {
396 } else if (that.x < that.maxScrollX) {
397 resetX = that.maxScrollX;
400 if (that.y >= 0 || that.maxScrollY > 0) {
402 } else if (that.y < that.maxScrollY) {
403 resetY = that.maxScrollY;
406 if (resetX != that.x || resetY != that.y) {
407 that.scrollTo(resetX, resetY);
410 that.onScrollEnd(); // Execute custom code on scroll end
414 // Hide the scrollbars
415 if (that.scrollBarX) {
416 that.scrollBarX.hide();
418 if (that.scrollBarY) {
419 that.scrollBarY.hide();
424 snap: function (x, y) {
425 var that = this, time;
427 if (that.directionX > 0) {
428 x = Math.floor(x/that.scrollWidth);
429 } else if (that.directionX < 0) {
430 x = Math.ceil(x/that.scrollWidth);
432 x = Math.round(x/that.scrollWidth);
435 x = x * that.scrollWidth;
438 } else if (x < that.maxScrollX) {
439 that.pageX = that.maxPageX;
443 if (that.directionY > 0) {
444 y = Math.floor(y/that.scrollHeight);
445 } else if (that.directionY < 0) {
446 y = Math.ceil(y/that.scrollHeight);
448 y = Math.round(y/that.scrollHeight);
451 y = y * that.scrollHeight;
454 } else if (y < that.maxScrollY) {
455 that.pageY = that.maxPageY;
459 // Snap with constant speed (proportional duration)
460 time = Math.round(Math.max(
461 Math.abs(that.x - x) / that.scrollWidth * 500,
462 Math.abs(that.y - y) / that.scrollHeight * 500
465 return { x: x, y: y, time: time };
468 scrollTo: function (destX, destY, runtime) {
471 if (that.x == destX && that.y == destY) {
472 that.resetPosition();
477 that.setTransitionTime(runtime || '350ms');
478 that.setPosition(destX, destY);
480 if (runtime==='0' || runtime=='0s' || runtime=='0ms') {
481 that.resetPosition();
483 document.addEventListener('webkitTransitionEnd', that, false); // At the end of the transition check if we are still inside of the boundaries
487 scrollToPage: function (pageX, pageY, runtime) {
488 var that = this, snap;
490 if (!that.options.snap) {
491 that.pageX = -Math.round(that.x / that.scrollWidth);
492 that.pageY = -Math.round(that.y / that.scrollHeight);
495 if (pageX == 'next') {
496 pageX = ++that.pageX;
497 } else if (pageX == 'prev') {
498 pageX = --that.pageX;
501 if (pageY == 'next') {
502 pageY = ++that.pageY;
503 } else if (pageY == 'prev') {
504 pageY = --that.pageY;
507 pageX = -pageX*that.scrollWidth;
508 pageY = -pageY*that.scrollHeight;
510 snap = that.snap(pageX, pageY);
514 that.scrollTo(pageX, pageY, runtime || '500ms');
517 scrollToElement: function (el, runtime) {
518 el = typeof el == 'object' ? el : this.element.querySelector(el);
525 x = that.scrollX ? -el.offsetLeft : 0,
526 y = that.scrollY ? -el.offsetTop : 0;
530 } else if (x < that.maxScrollX) {
536 } else if (y < that.maxScrollY) {
540 that.scrollTo(x, y, runtime);
543 momentum: function (dist, time, maxDistUpper, maxDistLower) {
546 speed = Math.abs(dist) / time * 1000,
547 newDist = speed * speed / friction / 1000,
550 // Proportinally reduce speed if we are outside of the boundaries
551 if (dist > 0 && newDist > maxDistUpper) {
552 speed = speed * maxDistUpper / newDist / friction;
553 newDist = maxDistUpper;
554 } else if (dist < 0 && newDist > maxDistLower) {
555 speed = speed * maxDistLower / newDist / friction;
556 newDist = maxDistLower;
559 newDist = newDist * (dist < 0 ? -1 : 1);
560 newTime = speed / deceleration;
562 return { dist: Math.round(newDist), time: Math.round(newTime) };
565 destroy: function (full) {
568 window.removeEventListener('onorientationchange' in window ? 'orientationchange' : 'resize', that, false);
569 that.element.removeEventListener(START_EVENT, that, false);
570 that.element.removeEventListener(MOVE_EVENT, that, false);
571 that.element.removeEventListener(END_EVENT, that, false);
572 document.removeEventListener('webkitTransitionEnd', that, false);
574 if (that.options.checkDOMChanges) {
575 that.element.removeEventListener('DOMSubtreeModified', that, false);
578 if (that.scrollBarX) {
579 that.scrollBarX = that.scrollBarX.remove();
582 if (that.scrollBarY) {
583 that.scrollBarY = that.scrollBarY.remove();
587 that.wrapper.parentNode.removeChild(that.wrapper);
594 function scrollbar (dir, wrapper, fade, shrink, color) {
600 that.shrink = shrink;
603 // Create main scrollbar
604 that.bar = doc.createElement('div');
606 that.bar.style.cssText = '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:' + color + ';' +
607 '-webkit-transform:' + translateOpen + '0,0' + translateClose + ';' +
608 (dir == 'horizontal' ? '-webkit-border-radius:3px 2px;min-width:6px;min-height:5px' : '-webkit-border-radius:2px 3px;min-width:5px;min-height:6px');
610 // Create scrollbar wrapper
611 that.wrapper = doc.createElement('div');
612 that.wrapper.style.cssText = '-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;' +
613 (that.dir == 'horizontal' ? 'bottom:2px;left:2px;right:7px;height:5px' : 'top:2px;right:2px;bottom:7px;width:5px;');
615 // Add scrollbar to the DOM
616 that.wrapper.appendChild(that.bar);
617 wrapper.appendChild(that.wrapper);
620 scrollbar.prototype = {
621 init: function (scroll, size) {
627 // Create scrollbar mask
628 if (that.dir == 'horizontal') {
629 if (that.maxSize != that.wrapper.offsetWidth) {
630 that.maxSize = that.wrapper.offsetWidth;
631 ctx = doc.getCSSCanvasContext("2d", "scrollbar" + that.uid + that.dir, that.maxSize, 5);
632 ctx.fillStyle = "rgb(0,0,0)";
634 ctx.arc(2.5, 2.5, 2.5, pi/2, -pi/2, false);
635 ctx.lineTo(that.maxSize-2.5, 0);
636 ctx.arc(that.maxSize-2.5, 2.5, 2.5, -pi/2, pi/2, false);
641 if (that.maxSize != that.wrapper.offsetHeight) {
642 that.maxSize = that.wrapper.offsetHeight;
643 ctx = doc.getCSSCanvasContext("2d", "scrollbar" + that.uid + that.dir, 5, that.maxSize);
644 ctx.fillStyle = "rgb(0,0,0)";
646 ctx.arc(2.5, 2.5, 2.5, pi, 0, false);
647 ctx.lineTo(5, that.maxSize-2.5);
648 ctx.arc(2.5, that.maxSize-2.5, 2.5, 0, pi, false);
654 that.size = Math.max(Math.round(that.maxSize * that.maxSize / size), 6);
655 that.maxScroll = that.maxSize - that.size;
656 that.toWrapperProp = that.maxScroll / (scroll - size);
657 that.bar.style[that.dir == 'horizontal' ? 'width' : 'height'] = that.size + 'px';
660 setPosition: function (pos) {
663 if (that.wrapper.style.opacity != '1') {
667 pos = Math.round(that.toWrapperProp * pos);
670 pos = that.shrink ? pos + pos*3 : 0;
671 if (that.size + pos < 7) {
672 pos = -that.size + 6;
674 } else if (pos > that.maxScroll) {
675 pos = that.shrink ? pos + (pos-that.maxScroll)*3 : that.maxScroll;
676 if (that.size + that.maxScroll - pos < 7) {
677 pos = that.size + that.maxScroll - 6;
681 pos = that.dir == 'horizontal'
682 ? translateOpen + pos + 'px,0' + translateClose
683 : translateOpen + '0,' + pos + 'px' + translateClose;
685 that.bar.style.webkitTransform = pos;
690 this.wrapper.style.webkitTransitionDelay = '0';
692 this.wrapper.style.opacity = '1';
697 this.wrapper.style.webkitTransitionDelay = '350ms';
699 this.wrapper.style.opacity = '0';
702 remove: function () {
703 this.wrapper.parentNode.removeChild(this.wrapper);
708 // Is translate3d compatible?
709 var has3d = ('WebKitCSSMatrix' in window && 'm11' in new WebKitCSSMatrix()),
711 isIthing = (/iphone|ipad/gi).test(navigator.appVersion),
712 isTouch = ('ontouchstart' in window),
714 START_EVENT = isTouch ? 'touchstart' : 'mousedown',
715 MOVE_EVENT = isTouch ? 'touchmove' : 'mousemove',
716 END_EVENT = isTouch ? 'touchend' : 'mouseup',
717 // Translate3d helper
718 translateOpen = 'translate' + (has3d ? '3d(' : '('),
719 translateClose = has3d ? ',0)' : ')',
723 // Expose iScroll to the world
724 window.iScroll = iScroll;