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 resizePageContentHeight( page ) {
13 var $page = $( page ),
14 $content = $page.children(".ui-content"),
15 hh = $page.children(".ui-header").outerHeight() || 0,
16 fh = $page.children(".ui-footer").outerHeight() || 0,
17 pt = parseFloat( $content.css("padding-top") ),
18 pb = parseFloat( $content.css("padding-bottom") ),
19 wh = $( window ).height();
21 $content.height( wh - (hh + fh) - (pt + pb) );
24 function MomentumTracker( options ) {
25 this.options = $.extend( {}, options );
26 this.easing = "easeOutQuad";
37 function getCurrentTime() {
41 jQuery.widget( "tizen.scrollview", jQuery.mobile.widget, {
43 fps: 60, // Frames per second in msecs.
44 direction: null, // "x", "y", or null for both.
46 scrollDuration: 2000, // Duration of the scrolling animation in msecs.
47 overshootDuration: 250, // Duration of the overshoot animation in msecs.
48 snapbackDuration: 500, // Duration of the snapback animation in msecs.
50 moveThreshold: 30, // User must move this many pixels in any direction to trigger a scroll.
51 moveIntervalThreshold: 150, // Time between mousemoves must not exceed this threshold.
53 scrollMethod: "translate", // "translate", "position"
54 startEventName: "scrollstart",
55 updateEventName: "scrollupdate",
56 stopEventName: "scrollstop",
58 eventType: $.support.touch ? "touch" : "mouse",
61 overshootEnable: false,
65 _makePositioned: function ( $ele ) {
66 if ( $ele.css("position") === "static" ) {
67 $ele.css( "position", "relative" );
71 _create: function () {
72 var $page = $('.ui-page'),
76 this._$clip = $( this.element ).addClass("ui-scrollview-clip");
77 this._$view = this._$clip.wrapInner("<div></div>").children()
78 .addClass("ui-scrollview-view");
80 if ( this.options.scrollMethod === "translate" ) {
81 if ( this._$view.css("transform") === undefined ) {
82 this.options.scrollMethod = "position";
86 this._$clip.css( "overflow", "hidden" );
87 this._makePositioned( this._$clip );
89 this._makePositioned( this._$view );
90 this._$view.css( { left: 0, top: 0 } );
91 this._view_height = this._$view.height();
96 direction = this.options.direction;
98 this._hTracker = ( direction !== "y" ) ?
99 new MomentumTracker( this.options ) : null;
100 this._vTracker = ( direction !== "x" ) ?
101 new MomentumTracker( this.options ) : null;
103 this._timerInterval = 1000 / this.options.fps;
106 this._timerCB = function () {
107 self._handleMomentumScroll();
111 this._add_scrollbar();
112 this._add_scroll_jump();
115 _startMScroll: function ( speedX, speedY ) {
116 var keepGoing = false,
117 duration = this.options.scrollDuration,
124 this._showScrollBars();
126 this._$clip.trigger( this.options.startEventName );
129 c = this._$clip.width();
130 v = this._$view.width();
132 ht.start( this._sx, speedX,
133 duration, (v > c) ? -(v - c) : 0, 0 );
134 keepGoing = !ht.done();
138 c = this._$clip.height();
139 v = this._$view.height() +
140 parseFloat( this._$view.css("padding-top") );
142 vt.start( this._sy, speedY,
143 duration, (v > c) ? -(v - c) : 0, 0 );
144 keepGoing = keepGoing || !vt.done();
148 this._timerID = setTimeout( this._timerCB, this._timerInterval );
154 _stopMScroll: function () {
155 if ( this._timerID ) {
156 this._$clip.trigger( this.options.stopEventName );
157 clearTimeout( this._timerID );
161 if ( this._vTracker ) {
162 this._vTracker.reset();
165 if ( this._hTracker ) {
166 this._hTracker.reset();
169 this._hideScrollBars();
172 _handleMomentumScroll: function () {
173 var keepGoing = false,
179 if ( this._outerScrolling ) {
184 vt.update( this.options.overshootEnable );
185 y = vt.getPosition();
186 keepGoing = !vt.done();
190 ht.update( this.options.overshootEnable );
191 x = ht.getPosition();
192 keepGoing = keepGoing || !ht.done();
195 this._setScrollPosition( x, y );
196 this._$clip.trigger( this.options.updateEventName,
197 [ { x: x, y: y } ] );
200 this._timerID = setTimeout( this._timerCB, this._timerInterval );
206 _setElementTransform: function ( $ele, x, y, duration ) {
210 if ( !duration || duration === undefined ) {
213 transition = "-webkit-transform " + duration / 1000 + "s";
216 if ( $.support.cssTransform3d ) {
217 translate = "translate3d(" + x + "," + y + ", 0px)";
219 translate = "translate(" + x + "," + y + ")";
223 "-moz-transform": translate,
224 "-webkit-transform": translate,
225 "-ms-transform": translate,
226 "-o-transform": translate,
227 "transform": translate,
228 "-webkit-transition": transition
232 _setCalibration: function ( x, y ) {
233 if ( this.options.overshootEnable ) {
239 var $v = this._$view,
241 dirLock = this._directionLock,
245 if ( dirLock !== "y" && this._hTracker ) {
246 scroll_width = $v.width() - $c.width();
250 } else if ( x < -scroll_width ) {
251 this._sx = -scroll_width;
256 if ( scroll_width < 0 ) {
261 if ( dirLock !== "x" && this._vTracker ) {
262 scroll_height = $v.height() - $c.height() +
263 parseFloat( $c.css("padding-top") ) +
264 parseFloat( $c.css("padding-bottom") );
266 this._outerScroll( y, scroll_height );
270 } else if ( y < -scroll_height ) {
271 this._sy = -scroll_height;
276 if ( scroll_height < 0 ) {
282 _setScrollPosition: function ( x, y, duration ) {
283 var $v = this._$view,
284 sm = this.options.scrollMethod,
285 $vsb = this._$vScrollBar,
286 $hsb = this._$hScrollBar,
289 this._setCalibration( x, y );
291 if ( this._outerScrolling ) {
298 if ( sm === "translate" ) {
299 this._setElementTransform( $v, x + "px", y + "px", duration );
301 $v.css( {left: x + "px", top: y + "px"} );
305 $sbt = $vsb.find(".ui-scrollbar-thumb");
307 if ( sm === "translate" ) {
308 this._setElementTransform( $sbt, "0px",
309 -y / $v.height() * $sbt.parent().height() + "px",
312 $sbt.css( "top", -y / $v.height() * 100 + "%" );
317 $sbt = $hsb.find(".ui-scrollbar-thumb");
319 if ( sm === "translate" ) {
320 this._setElementTransform( $sbt,
321 -x / $v.width() * $sbt.parent().width() + "px", "0px",
324 $sbt.css("left", -x / $v.width() * 100 + "%");
329 _outerScroll: function ( y, scroll_height ) {
331 top = $( window ).scrollTop(),
333 duration = this.options.snapbackDuration,
334 start = getCurrentTime(),
337 if ( this._$clip.jqmData("scroll") !== "y" ) {
341 if ( this._outerScrolling ) {
345 if ( !this._dragging ) {
349 if ( scroll_height < 0 ) {
355 } else if ( y < -scroll_height ) {
356 sy = -y - scroll_height;
363 tfunc = function () {
364 var elapsed = getCurrentTime() - start;
366 if ( elapsed >= duration ) {
367 window.scrollTo( 0, top + sy );
368 self._outerScrolling = undefined;
370 ec = $.easing.easeOutQuad( elapsed / duration, elapsed, 0, 1, duration );
372 window.scrollTo( 0, top + ( sy * ec ) );
373 self._outerScrolling = setTimeout( tfunc, self._timerInterval );
376 this._outerScrolling = setTimeout( tfunc, self._timerInterval );
378 /* skip the srollview dragging */
379 this._skip_dragging = true;
382 _scrollTo: function ( x, y, duration ) {
384 start = getCurrentTime(),
385 efunc = $.easing.easeOutQuad,
395 tfunc = function () {
396 var elapsed = getCurrentTime() - start,
399 if ( elapsed >= duration ) {
401 self._setScrollPosition( x, y );
403 ec = efunc( elapsed / duration, elapsed, 0, 1, duration );
405 self._setScrollPosition( sx + ( dx * ec ), sy + ( dy * ec ) );
406 self._timerID = setTimeout( tfunc, self._timerInterval );
410 this._timerID = setTimeout( tfunc, this._timerInterval );
413 scrollTo: function ( x, y, duration ) {
416 if ( !duration || this.options.scrollMethod === "translate" ) {
417 this._setScrollPosition( x, y, duration );
419 this._scrollTo( x, y, duration );
423 getScrollPosition: function () {
424 return { x: -this._sx, y: -this._sy };
427 _getScrollHierarchy: function () {
431 this._$clip.parents( ".ui-scrollview-clip").each( function () {
432 d = $( this ).jqmData("scrollview");
440 _getAncestorByDirection: function ( dir ) {
441 var svh = this._getScrollHierarchy(),
448 svdir = sv.options.direction;
450 if (!svdir || svdir === dir) {
457 _handleDragStart: function ( e, ex, ey ) {
460 this._didDrag = false;
461 this._skip_dragging = false;
463 var target = $( e.target ),
466 svdir = this.options.direction;
468 /* should prevent the default behavior when click the button */
469 this._is_button = target.is( '.ui-btn-text' ) ||
470 target.is( '.ui-btn-inner' ) ||
471 target.is( '.ui-btn-inner .ui-icon' );
473 if ( this._is_button ) {
474 if ( target.parents('.ui-slider-handle') ) {
475 this._skip_dragging = true;
481 * We need to prevent the default behavior to
482 * suppress accidental selection of text, etc.
484 this._is_inputbox = target.is(':input') ||
485 target.parents(':input').length > 0;
487 if ( this._is_inputbox ) {
488 target.one( "resize.scrollview", function () {
489 if ( ey > $c.height() ) {
490 self.scrollTo( -ex, self._sy - ey + $c.height(),
491 self.options.snapbackDuration );
499 this._doSnapBackX = false;
500 this._doSnapBackY = false;
503 this._directionLock = "";
506 this._enableTracking();
508 this._set_scrollbar_size();
511 _propagateDragMove: function ( sv, e, ex, ey, dir ) {
512 this._hideScrollBars();
513 this._disableTracking();
514 sv._handleDragStart( e, ex, ey );
515 sv._directionLock = dir;
516 sv._didDrag = this._didDrag;
519 _handleDragMove: function ( e, ex, ey ) {
520 if ( this._skip_dragging ) {
524 if ( !this._dragging ) {
528 if ( !this._is_inputbox && !this._is_button ) {
532 var mt = this.options.moveThreshold,
533 dx = ex - this._lastX,
534 dy = ey - this._lastY,
535 svdir = this.options.direction,
545 if ( Math.abs( this._startY - ey ) < mt && !this._didDrag ) {
549 this._lastMove = getCurrentTime();
551 if ( !this._directionLock ) {
555 if ( x < mt && y < mt ) {
559 if ( x < y && (x / y) < 0.5 ) {
561 } else if ( x > y && (y / x) < 0.5 ) {
565 if ( svdir && dir && svdir !== dir ) {
567 * This scrollview can't handle the direction the user
568 * is attempting to scroll. Find an ancestor scrollview
569 * that can handle the request.
572 sv = this._getAncestorByDirection( dir );
574 this._propagateDragMove( sv, e, ex, ey, dir );
579 this._directionLock = svdir || (dir || "none");
584 dirLock = this._directionLock;
586 if ( dirLock !== "y" && this._hTracker ) {
591 this._doSnapBackX = false;
593 scope = ( newX > 0 || newX < this._maxX );
595 if ( scope && dirLock === "x" ) {
596 sv = this._getAncestorByDirection("x");
598 this._setScrollPosition( newX > 0 ?
599 0 : this._maxX, newY );
600 this._propagateDragMove( sv, e, ex, ey, dir );
604 newX = x + ( dx / 2 );
605 this._doSnapBackX = true;
609 if ( dirLock !== "x" && this._vTracker ) {
614 this._doSnapBackY = false;
616 scope = ( newY > 0 || newY < this._maxY );
618 if ( scope && dirLock === "y" ) {
619 sv = this._getAncestorByDirection("y");
621 this._setScrollPosition( newX,
622 newY > 0 ? 0 : this._maxY );
623 this._propagateDragMove( sv, e, ex, ey, dir );
627 newY = y + ( dy / 2 );
628 this._doSnapBackY = true;
632 if ( this.options.overshootEnable === false ) {
633 this._doSnapBackX = false;
634 this._doSnapBackY = false;
637 this._didDrag = true;
641 this._setScrollPosition( newX, newY );
643 this._showScrollBars();
646 _handleDragStop: function ( e ) {
647 if ( this._skip_dragging ) {
651 var l = this._lastMove,
652 t = getCurrentTime(),
653 doScroll = (l && (t - l) <= this.options.moveIntervalThreshold),
654 sx = ( this._hTracker && this._speedX && doScroll ) ?
655 this._speedX : ( this._doSnapBackX ? 1 : 0 ),
656 sy = ( this._vTracker && this._speedY && doScroll ) ?
657 this._speedY : ( this._doSnapBackY ? 1 : 0 ),
658 svdir = this.options.direction,
663 this._startMScroll( sx, sy );
665 this._hideScrollBars();
668 this._disableTracking();
670 return !this._didDrag;
673 _enableTracking: function () {
674 this._dragging = true;
677 _disableTracking: function () {
678 this._dragging = false;
681 _showScrollBars: function () {
682 var vclass = "ui-scrollbar-visible";
684 if ( !this.options.showScrollBars ) {
687 if ( this._scrollbar_showed ) {
691 if ( this._$vScrollBar ) {
692 this._$vScrollBar.addClass( vclass );
694 if ( this._$hScrollBar ) {
695 this._$hScrollBar.addClass( vclass );
698 this._scrollbar_showed = true;
701 _hideScrollBars: function () {
702 var vclass = "ui-scrollbar-visible";
704 if ( !this.options.showScrollBars ) {
707 if ( !this._scrollbar_showed ) {
711 if ( this._$vScrollBar ) {
712 this._$vScrollBar.removeClass( vclass );
714 if ( this._$hScrollBar ) {
715 this._$hScrollBar.removeClass( vclass );
718 this._scrollbar_showed = false;
721 _add_event: function () {
726 if ( this.options.eventType === "mouse" ) {
727 this._dragEvt = "mousedown mousemove mouseup click mousewheel";
729 this._dragCB = function ( e ) {
732 return self._handleDragStart( e,
733 e.clientX, e.clientY );
736 return self._handleDragMove( e,
737 e.clientX, e.clientY );
740 return self._handleDragStop( e );
743 return !self._didDrag;
746 var old = self.getScrollPosition();
747 self.scrollTo( -old.x,
748 -(old.y - e.originalEvent.wheelDelta) );
753 this._dragEvt = "touchstart touchmove touchend click";
755 this._dragCB = function ( e ) {
760 t = e.originalEvent.targetTouches[0];
761 return self._handleDragStart( e,
765 t = e.originalEvent.targetTouches[0];
766 return self._handleDragMove( e,
770 return self._handleDragStop( e );
773 return !self._didDrag;
778 $v.bind( this._dragEvt, this._dragCB );
780 if ( $c.jqmData("scroll") !== "y" ) {
784 $c.bind( "updatelayout", function ( e ) {
785 var $page = $c.parentsUntil("ui-page"),
789 if ( !$c.height() || !$v.height() ) {
790 self.scrollTo( 0, 0, 0 );
794 sy = $c.height() - $v.height();
795 vh = $v.height() - self._view_height;
797 self._view_height = $v.height();
799 if ( vh == 0 || vh > $c.height() / 2 ) {
803 if ( self._sy - sy <= -vh ) {
804 self.scrollTo( 0, self._sy,
805 self.options.snapbackDuration );
806 } else if ( self._sy - sy <= vh + self.options.moveThreshold ) {
807 self.scrollTo( 0, sy,
808 self.options.snapbackDuration );
812 $( window ).bind( "resize", function ( e ) {
813 var $page = $c.parentsUntil("ui-page"),
816 if ( !$c.height() || !$v.height() ) {
820 focused = $c.find(".ui-focus");
823 focused.trigger("resize.scrollview");
826 /* calibration - after triggered throttledresize */
827 setTimeout( function () {
828 if ( self._sy < $c.height() - $v.height() ) {
829 self.scrollTo( 0, self._sy,
830 self.options.snapbackDuration );
834 self._view_height = $v.height();
838 _add_scrollbar: function () {
839 var $c = this._$clip,
840 prefix = "<div class=\"ui-scrollbar ui-scrollbar-",
841 suffix = "\"><div class=\"ui-scrollbar-track\"><div class=\"ui-scrollbar-thumb\"></div></div></div>";
843 if ( !this.options.showScrollBars ) {
847 if ( this._vTracker ) {
848 $c.append( prefix + "y" + suffix );
849 this._$vScrollBar = $c.children(".ui-scrollbar-y");
851 if ( this._hTracker ) {
852 $c.append( prefix + "x" + suffix );
853 this._$hScrollBar = $c.children(".ui-scrollbar-x");
856 this._scrollbar_showed = false;
859 _add_scroll_jump: function () {
860 var $c = this._$clip,
865 if ( !this.options.scrollJump ) {
869 if ( this._vTracker ) {
870 top_btn = $( '<div class="ui-scroll-jump-top-bg ui-btn" data-theme="s">' +
871 '<div class="ui-scroll-jump-top"></div></div>' );
872 $c.append( top_btn );
874 top_btn.bind( "vclick", function () {
875 self.scrollTo( 0, 0, self.options.overshootDuration );
879 if ( this._hTracker ) {
880 left_btn = $( '<div class="ui-scroll-jump-left-bg ui-btn" data-theme="s">' +
881 '<div class="ui-scroll-jump-left"></div></div>' );
882 $c.append( left_btn );
884 left_btn.bind( "vclick", function () {
885 self.scrollTo( 0, 0, self.options.overshootDuration );
890 _set_scrollbar_size: function () {
891 var $c = this._$clip,
899 if ( !this.options.showScrollBars ) {
903 if ( this._hTracker ) {
906 this._maxX = cw - vw;
908 if ( this._maxX > 0 ) {
911 if ( this._$hScrollBar && vw ) {
912 thumb = this._$hScrollBar.find(".ui-scrollbar-thumb");
913 thumb.css( "width", (cw >= vw ? "100%" :
914 (Math.floor(cw / vw * 100) || 1) + "%") );
918 if ( this._vTracker ) {
921 this._maxY = ch - vh;
923 if ( this._maxY > 0 ) {
926 if ( this._$vScrollBar && vh ) {
927 thumb = this._$vScrollBar.find(".ui-scrollbar-thumb");
928 thumb.css( "height", (ch >= vh ? "100%" :
929 (Math.floor(ch / vh * 100) || 1) + "%") );
935 $.extend( MomentumTracker.prototype, {
936 start: function ( pos, speed, duration, minPos, maxPos ) {
937 var tstate = ( pos < minPos || pos > maxPos ) ?
938 tstates.snapback : tstates.scrolling,
941 this.state = ( speed !== 0 ) ? tstate : tstates.done;
944 this.duration = ( this.state === tstates.snapback ) ?
945 this.options.snapbackDuration : duration;
946 this.minPos = minPos;
947 this.maxPos = maxPos;
949 this.fromPos = ( this.state === tstates.snapback ) ? this.pos : 0;
950 pos_temp = ( this.pos < this.minPos ) ? this.minPos : this.maxPos;
951 this.toPos = ( this.state === tstates.snapback ) ? pos_temp : 0;
953 this.startTime = getCurrentTime();
957 this.state = tstates.done;
965 update: function ( overshootEnable ) {
966 var state = this.state,
967 cur_time = getCurrentTime(),
968 duration = this.duration,
969 elapsed = cur_time - this.startTime,
974 if ( state === tstates.done ) {
978 elapsed = elapsed > duration ? duration : elapsed;
980 if ( state === tstates.scrolling || state === tstates.overshot ) {
982 ( 1 - $.easing[this.easing]( elapsed / duration,
983 elapsed, 0, 1, duration ) );
987 didOverShoot = ( state === tstates.scrolling ) &&
988 ( x < this.minPos || x > this.maxPos );
990 if ( didOverShoot ) {
991 x = ( x < this.minPos ) ? this.minPos : this.maxPos;
996 if ( state === tstates.overshot ) {
997 if ( elapsed >= duration ) {
998 this.state = tstates.snapback;
999 this.fromPos = this.pos;
1000 this.toPos = ( x < this.minPos ) ?
1001 this.minPos : this.maxPos;
1002 this.duration = this.options.snapbackDuration;
1003 this.startTime = cur_time;
1006 } else if ( state === tstates.scrolling ) {
1007 if ( didOverShoot && overshootEnable ) {
1008 this.state = tstates.overshot;
1009 this.speed = dx / 2;
1010 this.duration = this.options.overshootDuration;
1011 this.startTime = cur_time;
1012 } else if ( elapsed >= duration ) {
1013 this.state = tstates.done;
1016 } else if ( state === tstates.snapback ) {
1017 if ( elapsed >= duration ) {
1018 this.pos = this.toPos;
1019 this.state = tstates.done;
1021 this.pos = this.fromPos + (( this.toPos - this.fromPos ) *
1022 $.easing[this.easing]( elapsed / duration,
1023 elapsed, 0, 1, duration ));
1031 return this.state === tstates.done;
1034 getPosition: function () {
1039 $( document ).bind( 'pagecreate create', function ( e ) {
1040 var $page = $( e.target ),
1041 content_scroll = $page.find(".ui-content").jqmData("scroll");
1043 /* content scroll */
1044 if ( $.support.scrollview === undefined ) {
1045 $.support.scrollview = true;
1048 if ( $.support.scrollview === true && content_scroll === undefined ) {
1049 content_scroll = "y";
1052 if ( content_scroll !== "y" ) {
1053 content_scroll = "none";
1056 $page.find(".ui-content").attr( "data-scroll", content_scroll );
1058 $page.find(":jqmData(scroll):not(.ui-scrollview-clip)").each( function () {
1059 if ( $( this ).hasClass("ui-scrolllistview") ) {
1060 $( this ).scrolllistview();
1062 var st = $( this ).jqmData("scroll"),
1063 dir = st && ( st.search(/^[xy]/) !== -1 ) ? st.charAt(0) : null,
1066 if ( st === "none" ) {
1071 direction: dir || undefined,
1072 scrollMethod: $( this ).jqmData("scroll-method") || undefined,
1073 scrollJump: $( this ).jqmData("scroll-jump") || undefined
1076 $( this ).scrollview( opts );
1081 $( document ).bind( 'pageshow', function ( e ) {
1082 var $page = $( e.target ),
1083 scroll = $page.find(".ui-content").jqmData("scroll");
1085 if ( scroll === "y" ) {
1086 resizePageContentHeight( e.target );
1090 }( jQuery, window, document ) );