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
6 * Modified by Koeun Choi <koeun.choi@samsung.com>
7 * Modified by Minkyu Kang <mk7.kang@samsung.com>
10 ( function ( $, window, document, undefined ) {
12 jQuery.widget( "tizen.scrollview", jQuery.mobile.widget, {
14 fps: 60, // Frames per second in msecs.
15 direction: null, // "x", "y", or null for both.
17 scrollDuration: 2000, // Duration of the scrolling animation in msecs.
18 overshootDuration: 250, // Duration of the overshoot animation in msecs.
19 snapbackDuration: 500, // Duration of the snapback animation in msecs.
21 moveThreshold: 50, // User must move this many pixels in any direction to trigger a scroll.
22 moveIntervalThreshold: 150, // Time between mousemoves must not exceed this threshold.
24 scrollMethod: "translate", // "translate", "position", "scroll"
25 startEventName: "scrollstart",
26 updateEventName: "scrollupdate",
27 stopEventName: "scrollstop",
29 eventType: $.support.touch ? "touch" : "mouse",
34 overshootEnable: false,
36 delayedClickSelector: "a,input,textarea,select,button,.ui-btn",
37 delayedClickEnabled: false
40 _makePositioned: function ( $ele ) {
41 if ( $ele.css("position") === "static" ) {
42 $ele.css( "position", "relative" );
46 _create: function () {
47 var $page = $('.ui-page');
48 this._$content = $page.children('.ui-content');
50 this._$clip = $( this.element ).addClass("ui-scrollview-clip");
52 var $child = this._$clip.children();
54 if ( $child.length > 1 ) {
55 $child = this._$clip.wrapInner("<div></div>").children();
58 this._$view = $child.addClass("ui-scrollview-view");
60 this._$clip.css( "overflow",
61 this.options.scrollMethod === "scroll" ? "scroll" : "hidden" );
63 this._makePositioned( this._$clip );
66 * Turn off our faux scrollbars if we are using native scrolling
67 * to position the view.
69 if ( this.options.scrollMethod === "scroll" ) {
70 this.options.showScrollBars = false;
74 * We really don't need this if we are using a translate transformation
75 * for scrolling. We set it just in case the user wants to switch methods
78 this._makePositioned( this._$view );
79 this._$view.css({ left: 0, top: 0 });
84 var direction = this.options.direction;
86 this._hTracker = ( direction !== "y" ) ?
87 new MomentumTracker( this.options ) : null;
88 this._vTracker = ( direction !== "x" ) ?
89 new MomentumTracker( this.options ) : null;
91 this._timerInterval = 1000 / this.options.fps;
95 this._timerCB = function () {
96 self._handleMomentumScroll();
102 _startMScroll: function ( speedX, speedY ) {
104 this._showScrollBars();
106 var keepGoing = false;
107 var duration = this.options.scrollDuration;
109 this._$clip.trigger( this.options.startEventName );
110 $( document ).trigger("scrollview_scroll");
112 var ht = this._hTracker;
114 var c = this._$clip.width();
115 var v = this._$view.width();
116 ht.start( this._sx, speedX,
117 duration, (v > c) ? -(v - c) : 0, 0 );
118 keepGoing = !ht.done();
121 var vt = this._vTracker;
123 var c = this._$clip.height();
124 var v = this._$view.height() +
125 parseFloat( this._$view.css("padding-top") );
127 vt.start( this._sy, speedY,
128 duration, (v > c) ? -(v - c) : 0, 0 );
129 keepGoing = keepGoing || !vt.done();
133 this._timerID = setTimeout( this._timerCB, this._timerInterval );
139 _stopMScroll: function () {
140 if ( this._timerID ) {
141 this._$clip.trigger( this.options.stopEventName );
142 clearTimeout( this._timerID );
146 if ( this._vTracker ) {
147 this._vTracker.reset();
150 if ( this._hTracker ) {
151 this._hTracker.reset();
154 this._hideScrollBars();
157 _handleMomentumScroll: function () {
158 var keepGoing = false;
163 var vt = this._vTracker;
165 vt.update( this.options.overshootEnable );
166 y = vt.getPosition();
167 keepGoing = !vt.done();
170 var ht = this._hTracker;
172 ht.update( this.options.overshootEnable );
173 x = ht.getPosition();
174 keepGoing = keepGoing || !ht.done();
177 this._setScrollPosition( x, y );
178 this._$clip.trigger( this.options.updateEventName,
179 [ { x: x, y: y } ] );
182 this._timerID = setTimeout( this._timerCB, this._timerInterval );
188 _setCalibration: function ( x, y ) {
189 if ( this.options.overshootEnable ) {
197 var dirLock = this._directionLock;
199 if ( dirLock !== "y" && this._hTracker ) {
203 if ( dirLock !== "x" && this._vTracker ) {
204 var scroll_height = v.height() - c.height() +
205 parseFloat( c.css("padding-top") ) +
206 parseFloat( c.css("padding-bottom") );
210 } else if ( y < -scroll_height ) {
211 this._sy = -scroll_height;
216 if ( scroll_height < 0 ) {
222 _setScrollPosition: function ( x, y, duration ) {
223 this._setCalibration( x, y );
228 var $v = this._$view;
230 var sm = this.options.scrollMethod;
234 setElementTransform( $v, x + "px", y + "px", duration );
238 $v.css({left: x + "px", top: y + "px"});
242 var c = this._$clip[0];
248 var $vsb = this._$vScrollBar;
249 var $hsb = this._$hScrollBar;
252 var $sbt = $vsb.find(".ui-scrollbar-thumb");
254 if ( sm === "translate" ) {
255 setElementTransform( $sbt, "0px",
256 -y / $v.height() * $sbt.parent().height() + "px",
259 $sbt.css( "top", -y / $v.height() * 100 + "%" );
264 var $sbt = $hsb.find(".ui-scrollbar-thumb");
266 if ( sm === "translate" ) {
267 setElementTransform( $sbt,
268 -x / $v.width() * $sbt.parent().width() + "px", "0px",
271 $sbt.css("left", -x/$v.width() * 100 + "%");
276 scrollTo: function ( x, y, duration ) {
278 var sm = this.options.scrollMethod;
281 * currently support only animation for translate
282 * Don't want to use setTimeout algorithm for animation.
284 if ( !duration || (duration && sm === "translate") ) {
285 return this._setScrollPosition( x, y, duration );
288 // follow jqm default animation when the scrollmethod is not translate.
294 var start = getCurrentTime();
295 var efunc = $.easing["easeOutQuad"];
301 var tfunc = function() {
302 var elapsed = getCurrentTime() - start;
304 if ( elapsed >= duration ) {
306 self._setScrollPosition( x, y );
308 var ec = efunc( elapsed / duration, elapsed, 0, 1, duration );
310 self._setScrollPosition( sx + (dx * ec), sy + (dy * ec) );
311 self._timerID = setTimeout( tfunc, self._timerInterval );
315 this._timerID = setTimeout( tfunc, this._timerInterval );
318 getScrollPosition: function () {
319 return { x: -this._sx, y: -this._sy };
322 _getScrollHierarchy: function () {
324 this._$clip.parents(".ui-scrollview-clip").each( function () {
325 var d = $(this).jqmData("scrollview");
333 _getAncestorByDirection: function ( dir ) {
334 var svh = this._getScrollHierarchy();
339 var svdir = sv.options.direction;
341 if (!svdir || svdir === dir) {
348 _handleDragStart: function ( e, ex, ey ) {
349 // Stop any scrolling of elements in our parent hierarcy.
350 $.each( this._getScrollHierarchy(),
356 this._didDrag = false;
357 var target = $( e.target );
359 // should skip the dragging when click the button
360 this._skip_dragging = target.is('.ui-btn-text') ||
361 target.is('.ui-btn-inner');
363 if ( this._skip_dragging ) {
368 * If we're using mouse events, we need to prevent the default
369 * behavior to suppress accidental selection of text, etc. We
370 * can't do this on touch devices because it will disable the
371 * generation of "click" events.
374 var shouldBlockEvent = 1;
376 if ( this.options.eventType === "mouse" ) {
377 shouldBlockEvent = !( target.is(':input') ||
378 target.parents(':input').length > 0 );
379 } else if ( this.options.eventType === "touch" ) {
380 shouldBlockEvent = !( target.is(':input') ||
381 target.parents(':input').length > 0 );
385 if ( shouldBlockEvent ) {
392 if ( this.options.delayedClickEnabled ) {
394 target.closest( this.options.delayedClickSelector );
400 this._doSnapBackX = false;
401 this._doSnapBackY = false;
405 this._directionLock = "";
412 if ( this._hTracker ) {
413 cw = parseInt( c.css("width"), 10 );
414 vw = parseInt( v.css("width"), 10 );
415 this._maxX = cw - vw;
417 if ( this._maxX > 0 ) {
420 if ( this._$hScrollBar ) {
421 var thumb = this._$hScrollBar.find(".ui-scrollbar-thumb");
422 thumb.css( "width", (cw >= vw ?
423 "100%" : Math.floor(cw / vw * 100) + "%") );
427 if ( this._vTracker ) {
428 ch = parseInt( c.css("height"), 10 );
429 vh = parseInt( v.css("height"), 10 ) +
430 parseFloat( v.css("padding-top") );
431 this._maxY = ch - vh;
433 if ( this._maxY > 0 ) {
436 if ( this._$vScrollBar ) {
437 var thumb = this._$vScrollBar.find(".ui-scrollbar-thumb");
438 thumb.css( "height", (ch >= vh ?
439 "100%" : Math.floor(ch / vh * 100) + "%") );
443 var svdir = this.options.direction;
449 if ( this.options.pagingEnabled && (svdir === "x" || svdir === "y") ) {
450 this._pageSize = (svdir === "x") ? cw : ch;
451 this._pagePos = (svdir === "x") ? this._sx : this._sy;
452 this._pagePos -= this._pagePos % this._pageSize;
456 this._enableTracking();
459 _propagateDragMove: function ( sv, e, ex, ey, dir ) {
460 this._hideScrollBars();
461 this._disableTracking();
462 sv._handleDragStart( e,ex,ey );
463 sv._directionLock = dir;
464 sv._didDrag = this._didDrag;
467 _handleDragMove: function ( e, ex, ey ) {
468 if ( this._skip_dragging ) {
472 if ( !this._dragging ) {
476 var mt = this.options.moveThreshold;
478 if ( Math.abs( this._startY - ey ) < mt && !this._didDrag ) {
482 this._lastMove = getCurrentTime();
486 var dx = ex - this._lastX;
487 var dy = ey - this._lastY;
488 var svdir = this.options.direction;
491 if ( !this._directionLock ) {
492 var x = Math.abs( dx );
493 var y = Math.abs( dy );
495 if ( x < mt && y < mt ) {
499 if ( x < y && (x / y) < 0.5 ) {
501 } else if ( x > y && (y / x) < 0.5 ) {
505 if ( svdir && dir && svdir !== dir ) {
507 * This scrollview can't handle the direction the user
508 * is attempting to scroll. Find an ancestor scrollview
509 * that can handle the request.
512 var sv = this._getAncestorByDirection( dir );
514 this._propagateDragMove( sv, e, ex, ey, dir );
519 this._directionLock = svdir ? svdir : (dir ? dir : "none");
524 var dirLock = this._directionLock;
526 if ( dirLock !== "y" && this._hTracker ) {
531 // Simulate resistance.
533 this._doSnapBackX = false;
535 var scope = (newX > 0 || newX < this._maxX);
536 if ( scope && dirLock === "x" ) {
537 var sv = this._getAncestorByDirection("x");
539 this._setScrollPosition( newX > 0 ?
540 0 : this._maxX, newY );
541 this._propagateDragMove( sv, e, ex, ey, dir );
546 this._doSnapBackX = true;
550 if ( dirLock !== "x" && this._vTracker ) {
555 // Simulate resistance.
557 this._doSnapBackY = false;
559 var scope = (newY > 0 || newY < this._maxY);
560 if ( scope && dirLock === "y" ) {
561 var sv = this._getAncestorByDirection("y");
563 this._setScrollPosition( newX,
564 newY > 0 ? 0 : this._maxY );
565 this._propagateDragMove( sv, e, ex, ey, dir );
570 this._doSnapBackY = true;
574 if ( this.options.overshootEnable === false ) {
575 this._doSnapBackX = false;
576 this._doSnapBackY = false;
579 if ( this.options.pagingEnabled && (svdir === "x" || svdir === "y") ) {
580 if ( this._doSnapBackX || this._doSnapBackY ) {
583 var opos = this._pagePos;
584 var cpos = svdir === "x" ? newX : newY;
585 var delta = svdir === "x" ? dx : dy;
587 if ( opos > cpos && delta < 0 ) {
588 this._pageDelta = this._pageSize;
589 } else if ( opos < cpos && delta > 0 ) {
590 this._pageDelta = -this._pageSize;
597 this._didDrag = true;
601 this._setScrollPosition( newX, newY );
603 this._showScrollBars();
608 _handleDragStop: function ( e ) {
609 if ( this._skip_dragging ) {
613 var l = this._lastMove;
614 var t = getCurrentTime();
615 var doScroll = (l && (t - l) <= this.options.moveIntervalThreshold);
617 var sx = ( this._hTracker && this._speedX && doScroll ) ?
618 this._speedX : ( this._doSnapBackX ? 1 : 0 );
619 var sy = ( this._vTracker && this._speedY && doScroll ) ?
620 this._speedY : ( this._doSnapBackY ? 1 : 0 );
622 var svdir = this.options.direction;
623 if ( this.options.pagingEnabled && (svdir === "x" || svdir === "y") &&
624 !this._doSnapBackX && !this._doSnapBackY ) {
628 if ( svdir === "x" ) {
629 x = -this._pagePos + this._pageDelta;
631 y = -this._pagePos + this._pageDelta;
634 this.scrollTo( x, y, this.options.snapbackDuration );
635 } else if ( sx || sy ) {
636 this._startMScroll( sx, sy );
638 this._hideScrollBars();
641 this._disableTracking();
643 if ( !this._didDrag &&
644 this.options.delayedClickEnabled &&
645 this._$clickEle.length ) {
647 .trigger("mousedown")
653 * If a view scrolled, then we need to absorb
654 * the event so that links etc, underneath our
655 * cursor/finger don't fire.
658 return !this._didDrag;
661 _enableTracking: function () {
662 this._dragging = true;
665 _disableTracking: function () {
666 this._dragging = false;
669 _showScrollBars: function () {
670 var vclass = "ui-scrollbar-visible";
671 if ( this._$vScrollBar ) {
672 this._$vScrollBar.addClass( vclass );
674 if ( this._$hScrollBar ) {
675 this._$hScrollBar.addClass( vclass );
679 _hideScrollBars: function () {
680 var vclass = "ui-scrollbar-visible";
681 if ( this._$vScrollBar ) {
682 this._$vScrollBar.removeClass( vclass );
684 if ( this._$hScrollBar ) {
685 this._$hScrollBar.removeClass( vclass );
689 _addBehaviors: function () {
692 if ( this.options.eventType === "mouse" ) {
693 this._dragEvt = "mousedown mousemove mouseup click";
694 this._dragCB = function ( e ) {
697 return self._handleDragStart( e,
698 e.clientX, e.clientY );
701 return self._handleDragMove( e,
702 e.clientX, e.clientY );
705 return self._handleDragStop( e );
708 return !self._didDrag;
712 this._dragEvt = "touchstart touchmove touchend vclick";
713 this._dragCB = function ( e ) {
718 t = e.originalEvent.targetTouches[0];
719 return self._handleDragStart( e,
723 t = e.originalEvent.targetTouches[0];
724 return self._handleDragMove( e,
728 return self._handleDragStop( e );
731 return !self._didDrag;
736 this._$view.bind( this._dragEvt, this._dragCB );
738 if ( this.options.showScrollBars ) {
739 var $c = this._$clip;
740 var prefix = "<div class=\"ui-scrollbar ui-scrollbar-";
741 var suffix = "\"><div class=\"ui-scrollbar-track\"><div class=\"ui-scrollbar-thumb\"></div></div></div>";
742 if ( this._vTracker ) {
743 $c.append( prefix + "y" + suffix );
744 this._$vScrollBar = $c.children(".ui-scrollbar-y");
746 if ( this._hTracker ) {
747 $c.append( prefix + "x" + suffix );
748 this._$hScrollBar = $c.children(".ui-scrollbar-x");
754 function setElementTransform( $ele, x, y, duration ) {
755 var v = "translate3d(" + x + "," + y + ", 0px)";
758 if ( !duration || duration === undefined ) {
761 transition = "-webkit-transform " + duration / 1000 + "s";
766 "-webkit-transform": v,
768 "-webkit-transition": transition
772 function MomentumTracker( options ) {
773 this.options = $.extend( {}, options );
774 this.easing = "easeOutQuad";
785 function getCurrentTime() {
786 return ( new Date() ).getTime();
789 $.extend( MomentumTracker.prototype, {
790 start: function ( pos, speed, duration, minPos, maxPos ) {
791 var tstate = (pos < minPos || pos > maxPos) ?
792 tstates.snapback : tstates.scrolling;
793 this.state = (speed !== 0) ? tstate : tstates.done;
796 this.duration = (this.state === tstates.snapback) ?
797 this.options.snapbackDuration : duration;
798 this.minPos = minPos;
799 this.maxPos = maxPos;
801 this.fromPos = (this.state === tstates.snapback) ? this.pos : 0;
802 var pos_temp = (this.pos < this.minPos) ? this.minPos : this.maxPos;
803 this.toPos = (this.state === tstates.snapback) ? pos_temp : 0;
805 this.startTime = getCurrentTime();
809 this.state = tstates.done;
817 update: function ( overshootEnable ) {
818 var state = this.state;
820 if ( state === tstates.done ) {
824 var cur_time = getCurrentTime();
825 var duration = this.duration;
826 var elapsed = cur_time - this.startTime;
827 elapsed = elapsed > duration ? duration : elapsed;
829 if ( state === tstates.scrolling || state === tstates.overshot ) {
830 var dx = this.speed *
831 (1 - $.easing[this.easing]( elapsed / duration,
832 elapsed, 0, 1, duration ));
834 var x = this.pos + dx;
836 var didOverShoot = (state === tstates.scrolling) &&
837 (x < this.minPos || x > this.maxPos);
839 if ( didOverShoot ) {
840 x = (x < this.minPos) ? this.minPos : this.maxPos;
845 if ( state === tstates.overshot ) {
846 if ( elapsed >= duration ) {
847 this.state = tstates.snapback;
848 this.fromPos = this.pos;
849 this.toPos = (x < this.minPos) ?
850 this.minPos : this.maxPos;
851 this.duration = this.options.snapbackDuration;
852 this.startTime = cur_time;
855 } else if ( state === tstates.scrolling ) {
856 if ( didOverShoot && overshootEnable ) {
857 this.state = tstates.overshot;
859 this.duration = this.options.overshootDuration;
860 this.startTime = cur_time;
861 } else if ( elapsed >= duration ) {
862 this.state = tstates.done;
865 } else if ( state === tstates.snapback ) {
866 if ( elapsed >= duration ) {
867 this.pos = this.toPos;
868 this.state = tstates.done;
870 this.pos = this.fromPos + ((this.toPos - this.fromPos) *
871 $.easing[this.easing]( elapsed/duration,
872 elapsed, 0, 1, duration ));
880 return this.state === tstates.done;
883 getPosition: function () {
888 function ResizePageContentHeight( page ) {
889 var $page = $( page ),
890 $content = $page.children(".ui-content"),
891 hh = $page.children(".ui-header").outerHeight() || 0,
892 fh = $page.children(".ui-footer").outerHeight() || 0,
893 pt = parseFloat( $content.css("padding-top") ),
894 pb = parseFloat( $content.css("padding-bottom") ),
895 wh = window.innerHeight;
897 $content.height( wh - (hh + fh) - (pt + pb) );
900 // auto-init scrollview and scrolllistview widgets
901 $( document ).bind( 'pagecreate create', function ( e ) {
902 var $page = $( e.target );
903 var scroll = $page.find(".ui-content").attr("data-scroll");
905 if ( scroll === "none" ) {
909 if ( $.support.scrollview === true && scroll === undefined ) {
910 $page.find(".ui-content").attr( "data-scroll", "y" );
913 $page.find(":jqmData(scroll):not(.ui-scrollview-clip)").each( function () {
914 var $this = $( this );
916 if ( $this.hasClass("ui-scrolllistview") ) {
917 $this.scrolllistview();
919 var st = $this.jqmData("scroll") + "",
920 paging = st && (st.search(/^[xy]p$/) !== -1),
921 dir = st && (st.search(/^[xy]/) !== -1) ? st.charAt(0) : null;
924 direction: dir || undefined,
925 paging: paging || undefined,
926 scrollMethod: $this.jqmData("scroll-method") || undefined
929 $this.scrollview( opts );
934 $( document ).bind( 'pageshow', function ( e ) {
935 var $page = $( e.target );
936 var scroll = $page.find(".ui-content").attr("data-scroll");
938 if ( scroll === "y" ) {
939 setTimeout( function () {
940 ResizePageContentHeight( e.target );
945 $( window ).bind( "orientationchange", function( e ) {
946 ResizePageContentHeight( $(".ui-page") );
949 }( jQuery, window, document ) );