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.6 - Last updated: 2010.08.24
14 // does not appear to work yet...
17 Roo.namespace('Roo.ux');
19 Roo.ux.Iscroll = function(el, options)
22 that.element = typeof el == 'object' ? el : document.getElementById(el);
23 that.wrapper = that.element.parentNode;
25 that.element.style.webkitTransitionProperty = '-webkit-transform';
26 that.element.style.webkitTransitionTimingFunction = 'cubic-bezier(0,0,0.25,1)';
27 that.element.style.webkitTransitionDuration = '0';
28 that.element.style.webkitTransform = Roo.ux.Iscroll.translateOpen + '0,0' + Roo.ux.Iscroll.translateClose;
32 bounce: Roo.ux.Iscroll.has3d,
33 momentum: Roo.ux.Iscroll.has3d,
34 checkDOMChanges: true,
35 topOnDOMChanges: false,
36 hScrollbar: Roo.ux.Iscroll.has3d,
37 vScrollbar: Roo.ux.Iscroll.has3d,
38 fadeScrollbar: Roo.ux.Iscroll.isIphone || Roo.ux.Iscroll.isIpad || !Roo.ux.Iscroll.isTouch,
39 shrinkScrollbar: Roo.ux.Iscroll.isIphone || Roo.ux.Iscroll.isIpad || !Roo.ux.Iscroll.isTouch,
40 desktopCompatibility: false,
45 // User defined options
46 if (typeof options == 'object') {
48 that.options[i] = options[i];
52 if (that.options.desktopCompatibility) {
53 that.options.overflow = 'hidden';
56 that.wrapper.style.overflow = that.options.overflow;
60 window.addEventListener('onorientationchange' in window ? 'orientationchange' : 'resize', that, false);
62 if (Roo.ux.Iscroll.isTouch || that.options.desktopCompatibility) {
63 that.element.addEventListener(Roo.ux.Iscroll.START_EVENT, that, false);
64 that.element.addEventListener(Roo.ux.Iscroll.MOVE_EVENT, that, false);
65 that.element.addEventListener(Roo.ux.Iscroll.END_EVENT, that, false);
68 if (that.options.checkDOMChanges) {
69 that.element.addEventListener('DOMSubtreeModified', that, false);
73 Roo.apply(Roo.ux.Iscroll, {
74 has3d : ('WebKitCSSMatrix' in window && 'm11' in new WebKitCSSMatrix()),
76 isIphone : (/iphone/gi).test(navigator.appVersion),
77 isIpad : (/ipad/gi).test(navigator.appVersion),
78 isAndroid : (/android/gi).test(navigator.appVersion)
81 Roo.apply(Roo.ux.Iscroll, {
82 isTouch : Roo.ux.Iscroll.isIphone || Roo.ux.Iscroll.isIpad || Roo.ux.Iscroll.isAndroid,
84 START_EVENT : Roo.ux.Iscroll.isTouch ? 'touchstart' : 'mousedown',
85 MOVE_EVENT : Roo.ux.Iscroll.isTouch ? 'touchmove' : 'mousemove',
86 END_EVENT : Roo.ux.Iscroll.isTouch ? 'touchend' : 'mouseup',
88 translateOpen : 'translate' + (Roo.ux.Iscroll.has3d ? '3d(' : '('),
89 translateClose : Roo.ux.Iscroll.has3d ? ',0)' : ')',
95 Roo.ux.Iscroll.prototype = {
100 handleEvent: function (e) {
104 case Roo.ux.Iscroll.START_EVENT:
107 case Roo.ux.Iscroll.MOVE_EVENT:
110 case Roo.ux.Iscroll.END_EVENT:
113 case 'webkitTransitionEnd':
114 that.transitionEnd();
116 case 'orientationchange':
120 case 'DOMSubtreeModified':
121 that.onDOMModified(e);
126 onDOMModified: function (e) {
129 // (Hopefully) execute onDOMModified only once
130 if (e.target.parentNode != that.element) {
134 setTimeout(function () { that.refresh(); }, 0);
136 if (that.options.topOnDOMChanges && (that.x!=0 || that.y!=0)) {
137 that.scrollTo(0,0,'0');
141 refresh: function () {
143 resetX = this.x, resetY = this.y,
146 that.scrollWidth = that.wrapper.clientWidth;
147 that.scrollHeight = that.wrapper.clientHeight;
148 that.scrollerWidth = that.element.offsetWidth;
149 that.scrollerHeight = that.element.offsetHeight;
150 that.maxScrollX = that.scrollWidth - that.scrollerWidth;
151 that.maxScrollY = that.scrollHeight - that.scrollerHeight;
156 if (that.maxScrollX >= 0) {
158 } else if (that.x < that.maxScrollX) {
159 resetX = that.maxScrollX;
163 if (that.maxScrollY >= 0) {
165 } else if (that.y < that.maxScrollY) {
166 resetY = that.maxScrollY;
170 if (that.options.snap) {
171 that.maxPageX = -Math.floor(that.maxScrollX/that.scrollWidth);
172 that.maxPageY = -Math.floor(that.maxScrollY/that.scrollHeight);
174 snap = that.snap(resetX, resetY);
179 if (resetX!=that.x || resetY!=that.y) {
180 that.setTransitionTime('0');
181 that.setPosition(resetX, resetY, true);
184 that.scrollX = that.scrollerWidth > that.scrollWidth;
185 that.scrollY = !that.scrollX || that.scrollerHeight > that.scrollHeight;
187 // Update horizontal scrollbar
188 if (that.options.hScrollbar && that.scrollX) {
189 that.scrollBarX = that.scrollBarX || new scrollbar('horizontal', that.wrapper, that.options.fadeScrollbar, that.options.shrinkScrollbar);
190 that.scrollBarX.init(that.scrollWidth, that.scrollerWidth);
191 } else if (that.scrollBarX) {
192 that.scrollBarX = that.scrollBarX.remove();
195 // Update vertical scrollbar
196 if (that.options.vScrollbar && that.scrollY && that.scrollerHeight > that.scrollHeight) {
197 that.scrollBarY = that.scrollBarY || new scrollbar('vertical', that.wrapper, that.options.fadeScrollbar, that.options.shrinkScrollbar);
198 that.scrollBarY.init(that.scrollHeight, that.scrollerHeight);
199 } else if (that.scrollBarY) {
200 that.scrollBarY = that.scrollBarY.remove();
204 setPosition: function (x, y, hideScrollBars) {
210 that.element.style.webkitTransform = Roo.ux.Iscroll.translateOpen + that.x + 'px,' + that.y + 'px' + Roo.ux.Iscroll.translateClose;
212 // Move the scrollbars
213 if (!hideScrollBars) {
214 if (that.scrollBarX) {
215 that.scrollBarX.setPosition(that.x);
217 if (that.scrollBarY) {
218 that.scrollBarY.setPosition(that.y);
223 setTransitionTime: function(time) {
227 that.element.style.webkitTransitionDuration = time;
229 if (that.scrollBarX) {
230 that.scrollBarX.bar.style.webkitTransitionDuration = time;
231 that.scrollBarX.wrapper.style.webkitTransitionDuration = Roo.ux.Iscroll.has3d && that.options.fadeScrollbar ? '300ms' : '0';
233 if (that.scrollBarY) {
234 that.scrollBarY.bar.style.webkitTransitionDuration = time;
235 that.scrollBarY.wrapper.style.webkitTransitionDuration = Roo.ux.Iscroll.has3d && that.options.fadeScrollbar ? '300ms' : '0';
239 touchStart: function(e) {
250 that.scrolling = true; // This is probably not needed, but may be useful if iScroll is used in conjuction with other frameworks
255 that.setTransitionTime('0');
257 // Check if the scroller is really where it should be
258 if (that.options.momentum || that.options.snap) {
259 matrix = new WebKitCSSMatrix(window.getComputedStyle(that.element).webkitTransform);
260 if (matrix.e != that.x || matrix.f != that.y) {
261 document.removeEventListener('webkitTransitionEnd', that, false);
262 that.setPosition(matrix.e, matrix.f);
267 that.touchStartX = Roo.ux.Iscroll.isTouch ? e.changedTouches[0].pageX : e.pageX;
268 that.scrollStartX = that.x;
270 that.touchStartY = Roo.ux.Iscroll.isTouch ? e.changedTouches[0].pageY : e.pageY;
271 that.scrollStartY = that.y;
273 that.scrollStartTime = e.timeStamp;
279 touchMove: function(e) {
281 pageX = Roo.ux.Iscroll.isTouch ? e.changedTouches[0].pageX : e.pageX,
282 pageY = Roo.ux.Iscroll.isTouch ? e.changedTouches[0].pageY : e.pageY,
283 leftDelta = that.scrollX ? pageX - that.touchStartX : 0,
284 topDelta = that.scrollY ? pageY - that.touchStartY : 0,
285 newX = that.x + leftDelta,
286 newY = that.y + topDelta;
288 if (!that.scrolling) {
292 //e.preventDefault();
293 e.stopPropagation(); // Stopping propagation just saves some cpu cycles (I presume)
295 that.touchStartX = pageX;
296 that.touchStartY = pageY;
298 // Slow down if outside of the boundaries
299 if (newX >= 0 || newX < that.maxScrollX) {
300 newX = that.options.bounce ? Math.round(that.x + leftDelta / 3) : (newX >= 0 || that.maxScrollX>=0) ? 0 : that.maxScrollX;
302 if (newY >= 0 || newY < that.maxScrollY) {
303 newY = that.options.bounce ? Math.round(that.y + topDelta / 3) : (newY >= 0 || that.maxScrollY>=0) ? 0 : that.maxScrollY;
306 if (that.dist > 5) { // 5 pixels threshold is needed on Android, but also on iPhone looks more natural
307 that.setPosition(newX, newY);
309 that.directionX = leftDelta > 0 ? -1 : 1;
310 that.directionY = topDelta > 0 ? -1 : 1;
312 that.dist+= Math.abs(leftDelta) + Math.abs(topDelta);
316 touchEnd: function(e) {
318 time = e.timeStamp - that.scrollStartTime,
319 point = Roo.ux.Iscroll.isTouch ? e.changedTouches[0] : e,
321 momentumX, momentumY,
323 newPositionX = that.x, newPositionY = that.y,
326 if (!that.scrolling) {
329 that.scrolling = false;
332 that.resetPosition();
334 if (Roo.ux.Iscroll.isTouch) {
335 // Find the last touched element
336 target = point.target;
337 while (target.nodeType != 1) {
338 target = target.parentNode;
341 // Create the fake event
342 target.style.pointerEvents = 'auto';
343 ev = document.createEvent('MouseEvents');
344 ev.initMouseEvent('click', true, true, e.view, 1,
345 point.screenX, point.screenY, point.clientX, point.clientY,
346 e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,
349 target.dispatchEvent(ev);
355 if (!that.options.snap && time > 250) { // Prevent slingshot effect
356 that.resetPosition();
360 if (that.options.momentum) {
361 momentumX = that.scrollX === true
362 ? that.momentum(that.x - that.scrollStartX,
364 that.options.bounce ? -that.x + that.scrollWidth/5 : -that.x,
365 that.options.bounce ? that.x + that.scrollerWidth - that.scrollWidth + that.scrollWidth/5 : that.x + that.scrollerWidth - that.scrollWidth)
366 : { dist: 0, time: 0 };
368 momentumY = that.scrollY === true
369 ? that.momentum(that.y - that.scrollStartY,
371 that.options.bounce ? -that.y + that.scrollHeight/5 : -that.y,
372 that.options.bounce ? (that.maxScrollY < 0 ? that.y + that.scrollerHeight - that.scrollHeight : 0) + that.scrollHeight/5 : that.y + that.scrollerHeight - that.scrollHeight)
373 : { dist: 0, time: 0 };
375 newDuration = Math.max(Math.max(momentumX.time, momentumY.time), 1); // The minimum animation length must be 1ms
376 newPositionX = that.x + momentumX.dist;
377 newPositionY = that.y + momentumY.dist;
380 if (that.options.snap) {
381 snap = that.snap(newPositionX, newPositionY);
382 newPositionX = snap.x;
383 newPositionY = snap.y;
384 newDuration = Math.max(snap.time, newDuration);
387 that.scrollTo(newPositionX, newPositionY, newDuration + 'ms');
390 transitionEnd: function () {
392 document.removeEventListener('webkitTransitionEnd', that, false);
393 that.resetPosition();
396 resetPosition: function () {
403 } else if (that.x < that.maxScrollX) {
404 resetX = that.maxScrollX;
407 if (that.y >= 0 || that.maxScrollY > 0) {
409 } else if (that.y < that.maxScrollY) {
410 resetY = that.maxScrollY;
413 if (resetX != that.x || resetY != that.y) {
414 that.scrollTo(resetX, resetY);
417 that.onScrollEnd(); // Execute custom code on scroll end
421 // Hide the scrollbars
422 if (that.scrollBarX) {
423 that.scrollBarX.hide();
425 if (that.scrollBarY) {
426 that.scrollBarY.hide();
431 snap: function (x, y) {
432 var that = this, time;
434 if (that.directionX > 0) {
435 x = Math.floor(x/that.scrollWidth);
436 } else if (that.directionX < 0) {
437 x = Math.ceil(x/that.scrollWidth);
439 x = Math.round(x/that.scrollWidth);
442 x = x * that.scrollWidth;
445 } else if (x < that.maxScrollX) {
446 that.pageX = that.maxPageX;
450 if (that.directionY > 0) {
451 y = Math.floor(y/that.scrollHeight);
452 } else if (that.directionY < 0) {
453 y = Math.ceil(y/that.scrollHeight);
455 y = Math.round(y/that.scrollHeight);
458 y = y * that.scrollHeight;
461 } else if (y < that.maxScrollY) {
462 that.pageY = that.maxPageY;
466 // Snap with constant speed (proportional duration)
467 time = Math.round(Math.max(
468 Math.abs(that.x - x) / that.scrollWidth * 500,
469 Math.abs(that.y - y) / that.scrollHeight * 500
472 return { x: x, y: y, time: time };
475 scrollTo: function (destX, destY, runtime) {
478 if (that.x == destX && that.y == destY) {
479 that.resetPosition();
484 that.setTransitionTime(runtime || '350ms');
485 that.setPosition(destX, destY);
487 if (runtime==='0' || runtime=='0s' || runtime=='0ms') {
488 that.resetPosition();
490 document.addEventListener('webkitTransitionEnd', that, false); // At the end of the transition check if we are still inside of the boundaries
494 scrollToPage: function (pageX, pageY, runtime) {
495 var that = this, snap;
497 if (!that.options.snap) {
498 that.pageX = -Math.round(that.x / that.scrollWidth);
499 that.pageY = -Math.round(that.y / that.scrollHeight);
502 if (pageX == 'next') {
503 pageX = ++that.pageX;
504 } else if (pageX == 'prev') {
505 pageX = --that.pageX;
508 if (pageY == 'next') {
509 pageY = ++that.pageY;
510 } else if (pageY == 'prev') {
511 pageY = --that.pageY;
514 pageX = -pageX*that.scrollWidth;
515 pageY = -pageY*that.scrollHeight;
517 snap = that.snap(pageX, pageY);
521 that.scrollTo(pageX, pageY, runtime || '500ms');
524 scrollToElement: function (el, runtime) {
525 el = typeof el == 'object' ? el : this.element.querySelector(el);
532 x = that.scrollX ? -el.offsetLeft : 0,
533 y = that.scrollY ? -el.offsetTop : 0;
537 } else if (x < that.maxScrollX) {
543 } else if (y < that.maxScrollY) {
547 that.scrollTo(x, y, runtime);
550 momentum: function (dist, time, maxDistUpper, maxDistLower) {
553 speed = Math.abs(dist) / time * 1000,
554 newDist = speed * speed / friction / 1000,
557 // Proportinally reduce speed if we are outside of the boundaries
558 if (dist > 0 && newDist > maxDistUpper) {
559 speed = speed * maxDistUpper / newDist / friction;
560 newDist = maxDistUpper;
561 } else if (dist < 0 && newDist > maxDistLower) {
562 speed = speed * maxDistLower / newDist / friction;
563 newDist = maxDistLower;
566 newDist = newDist * (dist < 0 ? -1 : 1);
567 newTime = speed / deceleration;
569 return { dist: Math.round(newDist), time: Math.round(newTime) };
572 onScrollEnd: function () {},
574 destroy: function (full) {
577 window.removeEventListener('onorientationchange' in window ? 'orientationchange' : 'resize', that, false);
578 that.element.removeEventListener(START_EVENT, that, false);
579 that.element.removeEventListener(MOVE_EVENT, that, false);
580 that.element.removeEventListener(END_EVENT, that, false);
581 document.removeEventListener('webkitTransitionEnd', that, false);
583 if (that.options.checkDOMChanges) {
584 that.element.removeEventListener('DOMSubtreeModified', that, false);
587 if (that.scrollBarX) {
588 that.scrollBarX = that.scrollBarX.remove();
591 if (that.scrollBarY) {
592 that.scrollBarY = that.scrollBarY.remove();
596 that.wrapper.parentNode.removeChild(that.wrapper);
603 Roo.ux.Iscroll.scrollbar = function (dir, wrapper, fade, shrink) {
604 var that = this, style;
608 that.shrink = shrink;
609 that.uid = ++Roo.ux.Iscroll.uid;
611 // Create main scrollbar
612 that.bar = document.createElement('div');
614 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);' +
615 '-webkit-transform:' + Roo.ux.Iscroll.translateOpen + '0,0' + Roo.ux.Iscroll.translateClose + ';' +
616 (dir == 'horizontal' ? '-webkit-border-radius:3px 2px;min-width:6px;min-height:5px' : '-webkit-border-radius:2px 3px;min-width:5px;min-height:6px');
618 that.bar.setAttribute('style', style);
620 // Create scrollbar wrapper
621 that.wrapper = document.createElement('div');
622 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;' +
623 (that.dir == 'horizontal' ? 'bottom:2px;left:2px;right:7px;height:5px' : 'top:2px;right:2px;bottom:7px;width:5px;');
624 that.wrapper.setAttribute('style', style);
626 // Add scrollbar to the DOM
627 that.wrapper.appendChild(that.bar);
628 wrapper.appendChild(that.wrapper);
631 Roo.ux.Iscroll.scrollbar.prototype = {
632 init: function (scroll, size) {
636 // Create scrollbar mask
637 if (that.dir == 'horizontal') {
638 if (that.maxSize != that.wrapper.offsetWidth) {
639 that.maxSize = that.wrapper.offsetWidth;
640 ctx = document.getCSSCanvasContext("2d", "scrollbar" + that.uid + that.dir, that.maxSize, 5);
641 ctx.fillStyle = "rgb(0,0,0)";
643 ctx.arc(2.5, 2.5, 2.5, Math.PI/2, -Math.PI/2, false);
644 ctx.lineTo(that.maxSize-2.5, 0);
645 ctx.arc(that.maxSize-2.5, 2.5, 2.5, -Math.PI/2, Math.PI/2, false);
650 if (that.maxSize != that.wrapper.offsetHeight) {
651 that.maxSize = that.wrapper.offsetHeight;
652 ctx = document.getCSSCanvasContext("2d", "scrollbar" + that.uid + that.dir, 5, that.maxSize);
653 ctx.fillStyle = "rgb(0,0,0)";
655 ctx.arc(2.5, 2.5, 2.5, Math.PI, 0, false);
656 ctx.lineTo(5, that.maxSize-2.5);
657 ctx.arc(2.5, that.maxSize-2.5, 2.5, 0, Math.PI, false);
663 that.size = Math.max(Math.round(that.maxSize * that.maxSize / size), 6);
664 that.maxScroll = that.maxSize - that.size;
665 that.toWrapperProp = that.maxScroll / (scroll - size);
666 that.bar.style[that.dir == 'horizontal' ? 'width' : 'height'] = that.size + 'px';
669 setPosition: function (pos) {
672 if (that.wrapper.style.opacity != '1') {
676 pos = Math.round(that.toWrapperProp * pos);
679 pos = that.shrink ? pos + pos*3 : 0;
680 if (that.size + pos < 7) {
681 pos = -that.size + 6;
683 } else if (pos > that.maxScroll) {
684 pos = that.shrink ? pos + (pos-that.maxScroll)*3 : that.maxScroll;
685 if (that.size + that.maxScroll - pos < 7) {
686 pos = that.size + that.maxScroll - 6;
690 pos = that.dir == 'horizontal'
691 ? Roo.ux.Iscroll.translateOpen + pos + 'px,0' + Roo.ux.Iscroll.translateClose
692 : Roo.ux.Iscroll.translateOpen + '0,' + pos + 'px' + Roo.ux.Iscroll.translateClose;
694 that.bar.style.webkitTransform = pos;
698 if (Roo.ux.Iscroll.has3d) {
699 this.wrapper.style.webkitTransitionDelay = '0';
701 this.wrapper.style.opacity = '1';
705 if (Roo.ux.Iscroll.has3d) {
706 this.wrapper.style.webkitTransitionDelay = '350ms';
708 this.wrapper.style.opacity = '0';
711 remove: function () {
712 this.wrapper.parentNode.removeChild(this.wrapper);
717 // Is translate3d compatible?