2 * jQuery Mobile Framework : scrollview plugin
3 * Copyright (c) 2010 Adobe Systems Incorporated - Kin Blas (jblas@adobe.com)
4 * Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.
5 * Note: Code is in draft form and is subject to change
7 (function($,window,document,undefined){
9 jQuery.widget( "mobile.scrollview", jQuery.mobile.widget, {
11 fps: 60, // Frames per second in msecs.
12 direction: null, // "x", "y", or null for both.
14 scrollDuration: 2000, // Duration of the scrolling animation in msecs.
15 overshootDuration: 250, // Duration of the overshoot animation in msecs.
16 snapbackDuration: 500, // Duration of the snapback animation in msecs.
18 moveThreshold: 10, // User must move this many pixels in any direction to trigger a scroll.
19 moveIntervalThreshold: 150, // Time between mousemoves must not exceed this threshold.
21 scrollMethod: "translate", // "translate", "position", "scroll"
23 startEventName: "scrollstart",
24 updateEventName: "scrollupdate",
25 stopEventName: "scrollstop",
27 eventType: $.support.touch ? "touch" : "mouse",
32 delayedClickSelector: "a,input,textarea,select,button,.ui-btn",
33 delayedClickEnabled: false
36 _makePositioned: function($ele)
38 if ($ele.css("position") == "static")
39 $ele.css("position", "relative");
44 this._$clip = $(this.element).addClass("ui-scrollview-clip");
45 var $child = this._$clip.children();
46 if ($child.length > 1) {
47 $child = this._$clip.wrapInner("<div></div>").children();
49 this._$view = $child.addClass("ui-scrollview-view");
51 this._$clip.css("overflow", this.options.scrollMethod === "scroll" ? "scroll" : "hidden");
52 this._makePositioned(this._$clip);
54 this._$view.css("overflow", "hidden");
56 // Turn off our faux scrollbars if we are using native scrolling
57 // to position the view.
59 this.options.showScrollBars = this.options.scrollMethod === "scroll" ? false : this.options.showScrollBars;
61 // We really don't need this if we are using a translate transformation
62 // for scrolling. We set it just in case the user wants to switch methods
65 this._makePositioned(this._$view);
66 this._$view.css({ left: 0, top: 0 });
71 var direction = this.options.direction;
72 this._hTracker = (direction !== "y") ? new MomentumTracker(this.options) : null;
73 this._vTracker = (direction !== "x") ? new MomentumTracker(this.options) : null;
75 this._timerInterval = 1000/this.options.fps;
79 this._timerCB = function(){ self._handleMomentumScroll(); };
84 _startMScroll: function(speedX, speedY)
87 this._showScrollBars();
89 var keepGoing = false;
90 var duration = this.options.scrollDuration;
92 this._$clip.trigger(this.options.startEventName);
94 var ht = this._hTracker;
97 var c = this._$clip.width();
98 var v = this._$view.width();
99 ht.start(this._sx, speedX, duration, (v > c) ? -(v - c) : 0, 0);
100 keepGoing = !ht.done();
103 var vt = this._vTracker;
106 var c = this._$clip.height();
107 var v = this._$view.height();
108 vt.start(this._sy, speedY, duration, (v > c) ? -(v - c) : 0, 0);
109 keepGoing = keepGoing || !vt.done();
113 this._timerID = setTimeout(this._timerCB, this._timerInterval);
118 _stopMScroll: function()
122 this._$clip.trigger(this.options.stopEventName);
123 clearTimeout(this._timerID);
128 this._vTracker.reset();
131 this._hTracker.reset();
133 this._hideScrollBars();
136 _handleMomentumScroll: function()
138 var keepGoing = false;
143 var vt = this._vTracker;
147 y = vt.getPosition();
148 keepGoing = !vt.done();
151 var ht = this._hTracker;
155 x = ht.getPosition();
156 keepGoing = keepGoing || !ht.done();
159 this._setScrollPosition(x, y);
160 this._$clip.trigger(this.options.updateEventName, [ { x: x, y: y } ]);
163 this._timerID = setTimeout(this._timerCB, this._timerInterval);
168 _setScrollPosition: function(x, y)
173 var $v = this._$view;
175 var sm = this.options.scrollMethod;
180 setElementTransform($v, x + "px", y + "px");
183 $v.css({left: x + "px", top: y + "px"});
186 var c = this._$clip[0];
192 var $vsb = this._$vScrollBar;
193 var $hsb = this._$hScrollBar;
197 var $sbt = $vsb.find(".ui-scrollbar-thumb");
198 if (sm === "translate")
199 setElementTransform($sbt, "0px", -y/$v.height() * $sbt.parent().height() + "px");
201 $sbt.css("top", -y/$v.height()*100 + "%");
206 var $sbt = $hsb.find(".ui-scrollbar-thumb");
207 if (sm === "translate")
208 setElementTransform($sbt, -x/$v.width() * $sbt.parent().width() + "px", "0px");
210 $sbt.css("left", -x/$v.width()*100 + "%");
214 scrollTo: function(x, y, duration)
218 return this._setScrollPosition(x, y);
224 var start = getCurrentTime();
225 var efunc = $.easing["easeOutQuad"];
230 var tfunc = function(){
231 var elapsed = getCurrentTime() - start;
232 if (elapsed >= duration)
235 self._setScrollPosition(x, y);
239 var ec = efunc(elapsed/duration, elapsed, 0, 1, duration);
240 self._setScrollPosition(sx + (dx * ec), sy + (dy * ec));
241 self._timerID = setTimeout(tfunc, self._timerInterval);
245 this._timerID = setTimeout(tfunc, this._timerInterval);
248 getScrollPosition: function()
250 return { x: -this._sx, y: -this._sy };
253 _getScrollHierarchy: function()
256 this._$clip.parents(".ui-scrollview-clip").each(function(){
257 var d = $(this).jqmData("scrollview");
258 if (d) svh.unshift(d);
263 _getAncestorByDirection: function(dir)
265 var svh = this._getScrollHierarchy();
270 var svdir = sv.options.direction;
272 if (!svdir || svdir == dir)
278 _handleDragStart: function(e, ex, ey)
280 // Stop any scrolling of elements in our parent hierarcy.
281 $.each(this._getScrollHierarchy(),function(i,sv){ sv._stopMScroll(); });
287 if (this.options.delayedClickEnabled) {
288 this._$clickEle = $(e.target).closest(this.options.delayedClickSelector);
292 this._doSnapBackX = false;
293 this._doSnapBackY = false;
296 this._directionLock = "";
297 this._didDrag = false;
301 var cw = parseInt(c.css("width"), 10);
302 var vw = parseInt(v.css("width"), 10);
303 this._maxX = cw - vw;
304 if (this._maxX > 0) this._maxX = 0;
305 if (this._$hScrollBar)
306 this._$hScrollBar.find(".ui-scrollbar-thumb").css("width", (cw >= vw ? "100%" : Math.floor(cw/vw*100)+ "%"));
311 var ch = parseInt(c.css("height"), 10);
312 var vh = parseInt(v.css("height"), 10);
313 this._maxY = ch - vh;
314 if (this._maxY > 0) this._maxY = 0;
315 if (this._$vScrollBar)
316 this._$vScrollBar.find(".ui-scrollbar-thumb").css("height", (ch >= vh ? "100%" : Math.floor(ch/vh*100)+ "%"));
319 var svdir = this.options.direction;
325 if (this.options.pagingEnabled && (svdir === "x" || svdir === "y"))
327 this._pageSize = svdir === "x" ? cw : ch;
328 this._pagePos = svdir === "x" ? this._sx : this._sy;
329 this._pagePos -= this._pagePos % this._pageSize;
332 this._enableTracking();
334 // If we're using mouse events, we need to prevent the default
335 // behavior to suppress accidental selection of text, etc. We
336 // can't do this on touch devices because it will disable the
337 // generation of "click" events.
339 // XXX: We should test if this has an effect on links! - kin
341 if (this.options.eventType == "mouse" || this.options.delayedClickEnabled)
346 _propagateDragMove: function(sv, e, ex, ey, dir)
348 this._hideScrollBars();
349 this._disableTracking();
350 sv._handleDragStart(e,ex,ey);
351 sv._directionLock = dir;
352 sv._didDrag = this._didDrag;
355 _handleDragMove: function(e, ex, ey)
357 this._lastMove = getCurrentTime();
361 var dx = ex - this._lastX;
362 var dy = ey - this._lastY;
363 var svdir = this.options.direction;
365 if (!this._directionLock)
367 var x = Math.abs(dx);
368 var y = Math.abs(dy);
369 var mt = this.options.moveThreshold;
371 if (x < mt && y < mt) {
377 if (x < y && (x/y) < 0.5) {
380 else if (x > y && (y/x) < 0.5) {
384 if (svdir && dir && svdir != dir)
386 // This scrollview can't handle the direction the user
387 // is attempting to scroll. Find an ancestor scrollview
388 // that can handle the request.
390 var sv = this._getAncestorByDirection(dir);
393 this._propagateDragMove(sv, e, ex, ey, dir);
398 this._directionLock = svdir ? svdir : (dir ? dir : "none");
404 if (this._directionLock !== "y" && this._hTracker)
410 // Simulate resistance.
412 this._doSnapBackX = false;
413 if (newX > 0 || newX < this._maxX)
415 if (this._directionLock === "x")
417 var sv = this._getAncestorByDirection("x");
420 this._setScrollPosition(newX > 0 ? 0 : this._maxX, newY);
421 this._propagateDragMove(sv, e, ex, ey, dir);
426 this._doSnapBackX = true;
430 if (this._directionLock !== "x" && this._vTracker)
436 // Simulate resistance.
438 this._doSnapBackY = false;
439 if (newY > 0 || newY < this._maxY)
441 if (this._directionLock === "y")
443 var sv = this._getAncestorByDirection("y");
446 this._setScrollPosition(newX, newY > 0 ? 0 : this._maxY);
447 this._propagateDragMove(sv, e, ex, ey, dir);
453 this._doSnapBackY = true;
458 if (this.options.pagingEnabled && (svdir === "x" || svdir === "y"))
460 if (this._doSnapBackX || this._doSnapBackY)
464 var opos = this._pagePos;
465 var cpos = svdir === "x" ? newX : newY;
466 var delta = svdir === "x" ? dx : dy;
468 this._pageDelta = (opos > cpos && delta < 0) ? this._pageSize : ((opos < cpos && delta > 0) ? -this._pageSize : 0);
472 this._didDrag = true;
476 this._setScrollPosition(newX, newY);
478 this._showScrollBars();
480 // Call preventDefault() to prevent touch devices from
481 // scrolling the main window.
483 // e.preventDefault();
488 _handleDragStop: function(e)
490 var l = this._lastMove;
491 var t = getCurrentTime();
492 var doScroll = l && (t - l) <= this.options.moveIntervalThreshold;
494 var sx = (this._hTracker && this._speedX && doScroll) ? this._speedX : (this._doSnapBackX ? 1 : 0);
495 var sy = (this._vTracker && this._speedY && doScroll) ? this._speedY : (this._doSnapBackY ? 1 : 0);
497 var svdir = this.options.direction;
498 if (this.options.pagingEnabled && (svdir === "x" || svdir === "y") && !this._doSnapBackX && !this._doSnapBackY)
503 x = -this._pagePos + this._pageDelta;
505 y = -this._pagePos + this._pageDelta;
507 this.scrollTo(x, y, this.options.snapbackDuration);
510 this._startMScroll(sx, sy);
512 this._hideScrollBars();
514 this._disableTracking();
516 if (!this._didDrag && this.options.delayedClickEnabled && this._$clickEle.length) {
518 .trigger("mousedown")
524 // If a view scrolled, then we need to absorb
525 // the event so that links etc, underneath our
526 // cursor/finger don't fire.
528 return this._didDrag ? false : undefined;
531 _enableTracking: function()
533 $(document).bind(this._dragMoveEvt, this._dragMoveCB);
534 $(document).bind(this._dragStopEvt, this._dragStopCB);
537 _disableTracking: function()
539 $(document).unbind(this._dragMoveEvt, this._dragMoveCB);
540 $(document).unbind(this._dragStopEvt, this._dragStopCB);
543 _showScrollBars: function()
545 var vclass = "ui-scrollbar-visible";
546 if (this._$vScrollBar) this._$vScrollBar.addClass(vclass);
547 if (this._$hScrollBar) this._$hScrollBar.addClass(vclass);
550 _hideScrollBars: function()
552 var vclass = "ui-scrollbar-visible";
553 if (this._$vScrollBar) this._$vScrollBar.removeClass(vclass);
554 if (this._$hScrollBar) this._$hScrollBar.removeClass(vclass);
557 _addBehaviors: function()
560 if (this.options.eventType === "mouse")
562 this._dragStartEvt = "mousedown";
563 this._dragStartCB = function(e){ return self._handleDragStart(e, e.clientX, e.clientY); };
565 this._dragMoveEvt = "mousemove";
566 this._dragMoveCB = function(e){ return self._handleDragMove(e, e.clientX, e.clientY); };
568 this._dragStopEvt = "mouseup";
569 this._dragStopCB = function(e){ return self._handleDragStop(e); };
573 this._dragStartEvt = "touchstart";
574 this._dragStartCB = function(e)
576 var t = e.originalEvent.targetTouches[0];
577 return self._handleDragStart(e, t.pageX, t.pageY);
580 this._dragMoveEvt = "touchmove";
581 this._dragMoveCB = function(e)
583 var t = e.originalEvent.targetTouches[0];
584 return self._handleDragMove(e, t.pageX, t.pageY);
587 this._dragStopEvt = "touchend";
588 this._dragStopCB = function(e){ return self._handleDragStop(e); };
591 this._$view.bind(this._dragStartEvt, this._dragStartCB);
593 if (this.options.showScrollBars)
595 var $c = this._$clip;
596 var prefix = "<div class=\"ui-scrollbar ui-scrollbar-";
597 var suffix = "\"><div class=\"ui-scrollbar-track\"><div class=\"ui-scrollbar-thumb\"></div></div></div>";
600 $c.append(prefix + "y" + suffix);
601 this._$vScrollBar = $c.children(".ui-scrollbar-y");
605 $c.append(prefix + "x" + suffix);
606 this._$hScrollBar = $c.children(".ui-scrollbar-x");
612 function setElementTransform($ele, x, y)
614 var v = "translate3d(" + x + "," + y + ", 0px)";
617 "-webkit-transform": v,
623 function MomentumTracker(options)
625 this.options = $.extend({}, options);
626 this.easing = "easeOutQuad";
637 function getCurrentTime() { return (new Date()).getTime(); }
639 $.extend(MomentumTracker.prototype, {
640 start: function(pos, speed, duration, minPos, maxPos)
642 this.state = (speed != 0) ? ((pos < minPos || pos > maxPos) ? tstates.snapback : tstates.scrolling) : tstates.done;
645 this.duration = (this.state == tstates.snapback) ? this.options.snapbackDuration : duration;
646 this.minPos = minPos;
647 this.maxPos = maxPos;
649 this.fromPos = (this.state == tstates.snapback) ? this.pos : 0;
650 this.toPos = (this.state == tstates.snapback) ? ((this.pos < this.minPos) ? this.minPos : this.maxPos) : 0;
652 this.startTime = getCurrentTime();
657 this.state = tstates.done;
667 var state = this.state;
668 if (state == tstates.done)
671 var duration = this.duration;
672 var elapsed = getCurrentTime() - this.startTime;
673 elapsed = elapsed > duration ? duration : elapsed;
675 if (state == tstates.scrolling || state == tstates.overshot)
677 var dx = this.speed * (1 - $.easing[this.easing](elapsed/duration, elapsed, 0, 1, duration));
679 var x = this.pos + dx;
681 var didOverShoot = (state == tstates.scrolling) && (x < this.minPos || x > this.maxPos);
683 x = (x < this.minPos) ? this.minPos : this.maxPos;
687 if (state == tstates.overshot)
689 if (elapsed >= duration)
691 this.state = tstates.snapback;
692 this.fromPos = this.pos;
693 this.toPos = (x < this.minPos) ? this.minPos : this.maxPos;
694 this.duration = this.options.snapbackDuration;
695 this.startTime = getCurrentTime();
699 else if (state == tstates.scrolling)
703 this.state = tstates.overshot;
705 this.duration = this.options.overshootDuration;
706 this.startTime = getCurrentTime();
708 else if (elapsed >= duration)
709 this.state = tstates.done;
712 else if (state == tstates.snapback)
714 if (elapsed >= duration)
716 this.pos = this.toPos;
717 this.state = tstates.done;
720 this.pos = this.fromPos + ((this.toPos - this.fromPos) * $.easing[this.easing](elapsed/duration, elapsed, 0, 1, duration));
726 done: function() { return this.state == tstates.done; },
727 getPosition: function(){ return this.pos; }
730 jQuery.widget( "mobile.scrolllistview", jQuery.mobile.scrollview, {
735 _create: function() {
736 $.mobile.scrollview.prototype._create.call(this);
738 // Cache the dividers so we don't have to search for them everytime the
741 // XXX: Note that we need to update this cache if we ever support lists
742 // that can dynamically update their content.
744 this._$dividers = this._$view.find(":jqmData(role='list-divider')");
745 this._lastDivider = null;
748 _setScrollPosition: function(x, y)
750 // Let the view scroll like it normally does.
752 $.mobile.scrollview.prototype._setScrollPosition.call(this, x, y);
756 // Find the dividers for the list.
758 var $divs = this._$dividers;
759 var cnt = $divs.length;
764 for (var i = 0; i < cnt; i++)
767 var t = nd.offsetTop;
777 // If we found a divider to move position it at the top of the
782 var h = d.offsetHeight;
783 var mxy = (d != nd) ? nd.offsetTop : (this._$view.get(0).offsetHeight);
789 // XXX: Need to convert this over to using $().css() and supporting the non-transform case.
791 var ld = this._lastDivider;
793 setElementTransform($(ld), 0, 0);
795 setElementTransform($(d), 0, y + "px");
796 this._lastDivider = d;
802 })(jQuery,window,document); // End Component