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 function setElementTransform( $ele, x, y, duration ) {
13 var v = "translate(" + x + "," + y + ")",
16 if ( !duration || duration === undefined ) {
19 transition = "-webkit-transform " + duration / 1000 + "s";
24 "-webkit-transform": v,
28 "-webkit-transition": transition
32 function MomentumTracker( options ) {
33 this.options = $.extend( {}, options );
34 this.easing = "easeOutQuad";
45 function getCurrentTime() {
46 return ( new Date() ).getTime();
49 jQuery.widget( "tizen.scrollview", jQuery.mobile.widget, {
51 fps: 60, // Frames per second in msecs.
52 direction: null, // "x", "y", or null for both.
54 scrollDuration: 2000, // Duration of the scrolling animation in msecs.
55 overshootDuration: 250, // Duration of the overshoot animation in msecs.
56 snapbackDuration: 500, // Duration of the snapback animation in msecs.
58 moveThreshold: 50, // User must move this many pixels in any direction to trigger a scroll.
59 moveIntervalThreshold: 150, // Time between mousemoves must not exceed this threshold.
61 scrollMethod: "translate", // "translate", "position", "scroll"
62 startEventName: "scrollstart",
63 updateEventName: "scrollupdate",
64 stopEventName: "scrollstop",
66 eventType: $.support.touch ? "touch" : "mouse",
71 overshootEnable: false,
73 delayedClickSelector: "a,input,textarea,select,button,.ui-btn"
76 _makePositioned: function ( $ele ) {
77 if ( $ele.css("position") === "static" ) {
78 $ele.css( "position", "relative" );
82 _create: function () {
83 var $page = $('.ui-page'),
88 this._$clip = $( this.element ).addClass("ui-scrollview-clip");
90 $child = this._$clip.wrapInner("<div></div>").children();
92 this._$view = $child.addClass("ui-scrollview-view");
94 if ( this.options.scrollMethod === "translate" ) {
95 if ( this._$view.css("transform") === undefined ) {
96 this.options.scrollMethod = "position";
100 this._$clip.css( "overflow",
101 this.options.scrollMethod === "scroll" ? "scroll" : "hidden" );
103 this._makePositioned( this._$clip );
106 * Turn off our faux scrollbars if we are using native scrolling
107 * to position the view.
109 if ( this.options.scrollMethod === "scroll" ) {
110 this.options.showScrollBars = false;
114 * We really don't need this if we are using a translate transformation
115 * for scrolling. We set it just in case the user wants to switch methods
118 this._makePositioned( this._$view );
119 this._$view.css({ left: 0, top: 0 });
124 direction = this.options.direction;
126 this._hTracker = ( direction !== "y" ) ?
127 new MomentumTracker( this.options ) : null;
128 this._vTracker = ( direction !== "x" ) ?
129 new MomentumTracker( this.options ) : null;
131 this._timerInterval = 1000 / this.options.fps;
134 this._timerCB = function () {
135 self._handleMomentumScroll();
138 this._addBehaviors();
141 _startMScroll: function ( speedX, speedY ) {
143 this._showScrollBars();
145 var keepGoing = false,
146 duration = this.options.scrollDuration,
152 this._$clip.trigger( this.options.startEventName );
153 $( document ).trigger("scrollview_scroll");
156 c = this._$clip.width();
157 v = this._$view.width();
158 ht.start( this._sx, speedX,
159 duration, (v > c) ? -(v - c) : 0, 0 );
160 keepGoing = !ht.done();
164 c = this._$clip.height();
165 v = this._$view.height() +
166 parseFloat( this._$view.css("padding-top") );
168 vt.start( this._sy, speedY,
169 duration, (v > c) ? -(v - c) : 0, 0 );
170 keepGoing = keepGoing || !vt.done();
174 this._timerID = setTimeout( this._timerCB, this._timerInterval );
180 _stopMScroll: function () {
181 if ( this._timerID ) {
182 this._$clip.trigger( this.options.stopEventName );
183 clearTimeout( this._timerID );
187 if ( this._vTracker ) {
188 this._vTracker.reset();
191 if ( this._hTracker ) {
192 this._hTracker.reset();
195 this._hideScrollBars();
198 _handleMomentumScroll: function () {
199 var keepGoing = false,
207 vt.update( this.options.overshootEnable );
208 y = vt.getPosition();
209 keepGoing = !vt.done();
213 ht.update( this.options.overshootEnable );
214 x = ht.getPosition();
215 keepGoing = keepGoing || !ht.done();
218 this._setScrollPosition( x, y );
219 this._$clip.trigger( this.options.updateEventName,
220 [ { x: x, y: y } ] );
223 this._timerID = setTimeout( this._timerCB, this._timerInterval );
229 _setCalibration: function ( x, y ) {
230 if ( this.options.overshootEnable ) {
238 dirLock = this._directionLock,
241 if ( dirLock !== "y" && this._hTracker ) {
245 if ( dirLock !== "x" && this._vTracker ) {
246 scroll_height = v.height() - c.height() +
247 parseFloat( c.css("padding-top") ) +
248 parseFloat( c.css("padding-bottom") );
252 } else if ( y < -scroll_height ) {
253 this._sy = -scroll_height;
258 if ( scroll_height < 0 ) {
264 _setScrollPosition: function ( x, y, duration ) {
265 this._setCalibration( x, y );
270 var $v = this._$view,
271 sm = this.options.scrollMethod,
272 $vsb = this._$vScrollBar,
273 $hsb = this._$hScrollBar,
278 setElementTransform( $v, x + "px", y + "px", duration );
282 $v.css({left: x + "px", top: y + "px"});
286 this._$clip[0].scrollLeft = -x;
287 this._$clip[0].scrollTop = -y;
292 $sbt = $vsb.find(".ui-scrollbar-thumb");
294 if ( sm === "translate" ) {
295 setElementTransform( $sbt, "0px",
296 -y / $v.height() * $sbt.parent().height() + "px",
299 $sbt.css( "top", -y / $v.height() * 100 + "%" );
304 $sbt = $hsb.find(".ui-scrollbar-thumb");
306 if ( sm === "translate" ) {
307 setElementTransform( $sbt,
308 -x / $v.width() * $sbt.parent().width() + "px", "0px",
311 $sbt.css("left", -x / $v.width() * 100 + "%");
316 scrollTo: function ( x, y, duration ) {
320 * currently support only animation for translate
321 * Don't want to use setTimeout algorithm for animation.
323 if ( !duration || (duration && this.options.scrollMethod === "translate") ) {
324 return this._setScrollPosition( x, y, duration );
327 // follow jqm default animation when the scrollmethod is not translate.
333 start = getCurrentTime(),
334 efunc = $.easing.easeOutQuad,
341 tfunc = function () {
342 var elapsed = getCurrentTime() - start,
345 if ( elapsed >= duration ) {
347 self._setScrollPosition( x, y );
349 ec = efunc( elapsed / duration, elapsed, 0, 1, duration );
351 self._setScrollPosition( sx + (dx * ec), sy + (dy * ec) );
352 self._timerID = setTimeout( tfunc, self._timerInterval );
356 this._timerID = setTimeout( tfunc, this._timerInterval );
359 getScrollPosition: function () {
360 return { x: -this._sx, y: -this._sy };
363 _getScrollHierarchy: function () {
367 this._$clip.parents(".ui-scrollview-clip").each(function () {
368 d = $( this ).jqmData("scrollview");
376 _getAncestorByDirection: function ( dir ) {
377 var svh = this._getScrollHierarchy(),
384 svdir = sv.options.direction;
386 if (!svdir || svdir === dir) {
393 _handleDragStart: function ( e, ex, ey ) {
394 // Stop any scrolling of elements in our parent hierarcy.
395 $.each( this._getScrollHierarchy(), function (i, sv) {
400 this._didDrag = false;
402 var target = $( e.target ),
403 shouldBlockEvent = 1,
410 svdir = this.options.direction,
413 // should skip the dragging when click the button
414 this._skip_dragging = target.is('.ui-btn-text') ||
415 target.is('.ui-btn-inner');
417 if ( this._skip_dragging ) {
422 * If we're using mouse events, we need to prevent the default
423 * behavior to suppress accidental selection of text, etc. We
424 * can't do this on touch devices because it will disable the
425 * generation of "click" events.
428 this._shouldBlockEvent = !( target.is(':input') ||
429 target.parents(':input').length > 0 );
431 if ( this._shouldBlockEvent ) {
438 this._doSnapBackX = false;
439 this._doSnapBackY = false;
443 this._directionLock = "";
445 if ( this._hTracker ) {
446 cw = parseInt( c.css("width"), 10 );
447 vw = parseInt( v.css("width"), 10 );
448 this._maxX = cw - vw;
450 if ( this._maxX > 0 ) {
453 if ( this._$hScrollBar && vw ) {
454 thumb = this._$hScrollBar.find(".ui-scrollbar-thumb");
455 thumb.css( "width", (cw >= vw ? "100%" :
456 (Math.floor(cw / vw * 100) || 1) + "%") );
460 if ( this._vTracker ) {
461 ch = parseInt( c.css("height"), 10 );
462 vh = parseInt( v.css("height"), 10 ) +
463 parseFloat( v.css("padding-top") );
464 this._maxY = ch - vh;
466 if ( this._maxY > 0 ) {
469 if ( this._$vScrollBar && vh ) {
470 thumb = this._$vScrollBar.find(".ui-scrollbar-thumb");
471 thumb.css( "height", (ch >= vh ? "100%" :
472 (Math.floor(ch / vh * 100) || 1) + "%") );
480 if ( this.options.pagingEnabled && (svdir === "x" || svdir === "y") ) {
481 this._pageSize = (svdir === "x") ? cw : ch;
482 this._pagePos = (svdir === "x") ? this._sx : this._sy;
483 this._pagePos -= this._pagePos % this._pageSize;
487 this._enableTracking();
490 _propagateDragMove: function ( sv, e, ex, ey, dir ) {
491 this._hideScrollBars();
492 this._disableTracking();
493 sv._handleDragStart( e, ex, ey );
494 sv._directionLock = dir;
495 sv._didDrag = this._didDrag;
498 _handleDragMove: function ( e, ex, ey ) {
499 if ( this._skip_dragging ) {
503 if ( !this._dragging ) {
507 if ( this._shouldBlockEvent ) {
511 var mt = this.options.moveThreshold,
513 dx = ex - this._lastX,
514 dy = ey - this._lastY,
515 svdir = this.options.direction,
528 if ( Math.abs( this._startY - ey ) < mt && !this._didDrag ) {
532 this._lastMove = getCurrentTime();
533 if ( !this._directionLock ) {
537 if ( x < mt && y < mt ) {
541 if ( x < y && (x / y) < 0.5 ) {
543 } else if ( x > y && (y / x) < 0.5 ) {
547 if ( svdir && dir && svdir !== dir ) {
549 * This scrollview can't handle the direction the user
550 * is attempting to scroll. Find an ancestor scrollview
551 * that can handle the request.
554 sv = this._getAncestorByDirection( dir );
556 this._propagateDragMove( sv, e, ex, ey, dir );
561 //this._directionLock = svdir ? svdir : (dir ? dir : "none");
562 this._directionLock = svdir || (dir || "none");
567 dirLock = this._directionLock;
569 if ( dirLock !== "y" && this._hTracker ) {
574 // Simulate resistance.
576 this._doSnapBackX = false;
578 scope = (newX > 0 || newX < this._maxX);
579 if ( scope && dirLock === "x" ) {
580 sv = this._getAncestorByDirection("x");
582 this._setScrollPosition( newX > 0 ?
583 0 : this._maxX, newY );
584 this._propagateDragMove( sv, e, ex, ey, dir );
589 this._doSnapBackX = true;
593 if ( dirLock !== "x" && this._vTracker ) {
598 // Simulate resistance.
600 this._doSnapBackY = false;
602 scope = (newY > 0 || newY < this._maxY);
603 if ( scope && dirLock === "y" ) {
604 sv = this._getAncestorByDirection("y");
606 this._setScrollPosition( newX,
607 newY > 0 ? 0 : this._maxY );
608 this._propagateDragMove( sv, e, ex, ey, dir );
613 this._doSnapBackY = true;
617 if ( this.options.overshootEnable === false ) {
618 this._doSnapBackX = false;
619 this._doSnapBackY = false;
622 if ( this.options.pagingEnabled && (svdir === "x" || svdir === "y") ) {
623 if ( this._doSnapBackX || this._doSnapBackY ) {
626 opos = this._pagePos;
627 cpos = svdir === "x" ? newX : newY;
628 delta = svdir === "x" ? dx : dy;
630 if ( opos > cpos && delta < 0 ) {
631 this._pageDelta = this._pageSize;
632 } else if ( opos < cpos && delta > 0 ) {
633 this._pageDelta = -this._pageSize;
640 this._didDrag = true;
644 this._setScrollPosition( newX, newY );
646 this._showScrollBars();
649 _handleDragStop: function ( e ) {
650 if ( this._skip_dragging ) {
654 var l = this._lastMove,
655 t = getCurrentTime(),
656 doScroll = (l && (t - l) <= this.options.moveIntervalThreshold),
657 sx = ( this._hTracker && this._speedX && doScroll ) ?
658 this._speedX : ( this._doSnapBackX ? 1 : 0 ),
659 sy = ( this._vTracker && this._speedY && doScroll ) ?
660 this._speedY : ( this._doSnapBackY ? 1 : 0 ),
661 svdir = this.options.direction,
665 if ( this.options.pagingEnabled && (svdir === "x" || svdir === "y") &&
666 !this._doSnapBackX && !this._doSnapBackY ) {
670 if ( svdir === "x" ) {
671 x = -this._pagePos + this._pageDelta;
673 y = -this._pagePos + this._pageDelta;
676 this.scrollTo( x, y, this.options.snapbackDuration );
677 } else if ( sx || sy ) {
678 this._startMScroll( sx, sy );
680 this._hideScrollBars();
683 this._disableTracking();
685 if ( !this._didDrag && this.options.eventType === "touch" ) {
686 $(e.target).closest(this.options.delayedClickSelector).trigger("click");
690 * If a view scrolled, then we need to absorb
691 * the event so that links etc, underneath our
692 * cursor/finger don't fire.
695 return !this._didDrag;
698 _enableTracking: function () {
699 this._dragging = true;
702 _disableTracking: function () {
703 this._dragging = false;
706 _showScrollBars: function () {
707 var vclass = "ui-scrollbar-visible";
708 if ( this._$vScrollBar ) {
709 this._$vScrollBar.addClass( vclass );
711 if ( this._$hScrollBar ) {
712 this._$hScrollBar.addClass( vclass );
716 _hideScrollBars: function () {
717 var vclass = "ui-scrollbar-visible";
718 if ( this._$vScrollBar ) {
719 this._$vScrollBar.removeClass( vclass );
721 if ( this._$hScrollBar ) {
722 this._$hScrollBar.removeClass( vclass );
726 _addBehaviors: function () {
729 prefix = "<div class=\"ui-scrollbar ui-scrollbar-",
730 suffix = "\"><div class=\"ui-scrollbar-track\"><div class=\"ui-scrollbar-thumb\"></div></div></div>";
732 if ( this.options.eventType === "mouse" ) {
733 this._dragEvt = "mousedown mousemove mouseup click";
734 this._dragCB = function ( e ) {
737 return self._handleDragStart( e,
738 e.clientX, e.clientY );
741 return self._handleDragMove( e,
742 e.clientX, e.clientY );
745 return self._handleDragStop( e );
748 return !self._didDrag;
752 this._dragEvt = "touchstart touchmove touchend vclick";
753 this._dragCB = function ( e ) {
758 t = e.originalEvent.targetTouches[0];
759 return self._handleDragStart( e,
763 t = e.originalEvent.targetTouches[0];
764 return self._handleDragMove( e,
768 return self._handleDragStop( e );
771 return !self._didDrag;
776 this._$view.bind( this._dragEvt, this._dragCB );
778 if ( this.options.showScrollBars ) {
779 if ( this._vTracker ) {
780 $c.append( prefix + "y" + suffix );
781 this._$vScrollBar = $c.children(".ui-scrollbar-y");
783 if ( this._hTracker ) {
784 $c.append( prefix + "x" + suffix );
785 this._$hScrollBar = $c.children(".ui-scrollbar-x");
791 $.extend( MomentumTracker.prototype, {
792 start: function ( pos, speed, duration, minPos, maxPos ) {
793 var tstate = (pos < minPos || pos > maxPos) ?
794 tstates.snapback : tstates.scrolling,
797 this.state = (speed !== 0) ? tstate : tstates.done;
800 this.duration = (this.state === tstates.snapback) ?
801 this.options.snapbackDuration : duration;
802 this.minPos = minPos;
803 this.maxPos = maxPos;
805 this.fromPos = (this.state === tstates.snapback) ? this.pos : 0;
806 pos_temp = (this.pos < this.minPos) ? this.minPos : this.maxPos;
807 this.toPos = (this.state === tstates.snapback) ? pos_temp : 0;
809 this.startTime = getCurrentTime();
813 this.state = tstates.done;
821 update: function ( overshootEnable ) {
822 var state = this.state,
823 cur_time = getCurrentTime(),
824 duration = this.duration,
825 elapsed = cur_time - this.startTime,
830 if ( state === tstates.done ) {
834 elapsed = elapsed > duration ? duration : elapsed;
836 if ( state === tstates.scrolling || state === tstates.overshot ) {
838 (1 - $.easing[this.easing]( elapsed / duration,
839 elapsed, 0, 1, duration ));
843 didOverShoot = (state === tstates.scrolling) &&
844 (x < this.minPos || x > this.maxPos);
846 if ( didOverShoot ) {
847 x = (x < this.minPos) ? this.minPos : this.maxPos;
852 if ( state === tstates.overshot ) {
853 if ( elapsed >= duration ) {
854 this.state = tstates.snapback;
855 this.fromPos = this.pos;
856 this.toPos = (x < this.minPos) ?
857 this.minPos : this.maxPos;
858 this.duration = this.options.snapbackDuration;
859 this.startTime = cur_time;
862 } else if ( state === tstates.scrolling ) {
863 if ( didOverShoot && overshootEnable ) {
864 this.state = tstates.overshot;
866 this.duration = this.options.overshootDuration;
867 this.startTime = cur_time;
868 } else if ( elapsed >= duration ) {
869 this.state = tstates.done;
872 } else if ( state === tstates.snapback ) {
873 if ( elapsed >= duration ) {
874 this.pos = this.toPos;
875 this.state = tstates.done;
877 this.pos = this.fromPos + ((this.toPos - this.fromPos) *
878 $.easing[this.easing]( elapsed / duration,
879 elapsed, 0, 1, duration ));
887 return this.state === tstates.done;
890 getPosition: function () {
895 function resizePageContentHeight( page ) {
896 var $page = $( page ),
897 $content = $page.children(".ui-content"),
898 hh = $page.children(".ui-header").outerHeight() || 0,
899 fh = $page.children(".ui-footer").outerHeight() || 0,
900 pt = parseFloat( $content.css("padding-top") ),
901 pb = parseFloat( $content.css("padding-bottom") ),
902 wh = $(window).height();
904 $content.height( wh - (hh + fh) - (pt + pb) );
907 // auto-init scrollview and scrolllistview widgets
908 $( document ).bind( 'pagecreate create', function ( e ) {
909 var $page = $( e.target ),
910 scroll = $page.find(".ui-content").attr("data-scroll");
912 if ( scroll === "none" ) {
916 if ( $.support.scrollview === undefined ) {
917 // set as default value
918 $.support.scrollview = true;
921 if ( $.support.scrollview === true && scroll === undefined ) {
922 $page.find(".ui-content").attr( "data-scroll", "y" );
925 $page.find(":jqmData(scroll):not(.ui-scrollview-clip)").each( function () {
926 if ( $( this ).hasClass("ui-scrolllistview") ) {
927 $( this ).scrolllistview();
929 var st = $( this ).jqmData("scroll"),
930 paging = st && (st.search(/^[xy]p$/) !== -1),
931 dir = st && (st.search(/^[xy]/) !== -1) ? st.charAt(0) : null,
935 direction: dir || undefined,
936 paging: paging || undefined,
937 scrollMethod: $( this ).jqmData("scroll-method") || undefined
940 $( this ).scrollview( opts );
945 $( document ).bind( 'pageshow', function ( e ) {
946 var $page = $( e.target ),
947 scroll = $page.find(".ui-content").attr("data-scroll");
949 if ( scroll === "y" ) {
950 setTimeout( function () {
951 resizePageContentHeight( e.target );
956 $( window ).bind( "orientationchange", function ( e ) {
957 resizePageContentHeight( $(".ui-page") );
960 }( jQuery, window, document ) );