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 _getViewHeight: function () {
66 return this._$view.height() + this._view_offset;
69 _makePositioned: function ( $ele ) {
70 if ( $ele.css("position") === "static" ) {
71 $ele.css( "position", "relative" );
75 _create: function () {
79 this._$clip = $( this.element ).addClass("ui-scrollview-clip");
80 this._$view = this._$clip.wrapInner("<div></div>").children()
81 .addClass("ui-scrollview-view");
83 if ( this.options.scrollMethod === "translate" ) {
84 if ( this._$view.css("transform") === undefined ) {
85 this.options.scrollMethod = "position";
89 this._$clip.css( "overflow", "hidden" );
90 this._makePositioned( this._$clip );
92 this._makePositioned( this._$view );
93 this._$view.css( { left: 0, top: 0 } );
95 this._view_offset = this._$view.offset().top - this._$clip.offset().top;
96 this._view_height = this._getViewHeight();
101 direction = this.options.direction;
103 this._hTracker = ( direction !== "y" ) ?
104 new MomentumTracker( this.options ) : null;
105 this._vTracker = ( direction !== "x" ) ?
106 new MomentumTracker( this.options ) : null;
108 this._timerInterval = 1000 / this.options.fps;
111 this._timerCB = function () {
112 self._handleMomentumScroll();
116 this._add_scrollbar();
117 this._add_scroll_jump();
120 _startMScroll: function ( speedX, speedY ) {
121 var keepGoing = false,
122 duration = this.options.scrollDuration,
129 this._showScrollBars();
131 this._$clip.trigger( this.options.startEventName );
134 c = this._$clip.width();
135 v = this._$view.width();
137 ht.start( this._sx, speedX,
138 duration, (v > c) ? -(v - c) : 0, 0 );
139 keepGoing = !ht.done();
143 c = this._$clip.height();
144 v = this._getViewHeight();
146 vt.start( this._sy, speedY,
147 duration, (v > c) ? -(v - c) : 0, 0 );
148 keepGoing = keepGoing || !vt.done();
152 this._timerID = setTimeout( this._timerCB, this._timerInterval );
158 _stopMScroll: function () {
159 if ( this._timerID ) {
160 this._$clip.trigger( this.options.stopEventName );
161 clearTimeout( this._timerID );
165 if ( this._vTracker ) {
166 this._vTracker.reset();
169 if ( this._hTracker ) {
170 this._hTracker.reset();
173 this._hideScrollBars();
176 _handleMomentumScroll: function () {
177 var keepGoing = false,
183 if ( this._outerScrolling ) {
188 vt.update( this.options.overshootEnable );
189 y = vt.getPosition();
190 keepGoing = !vt.done();
194 ht.update( this.options.overshootEnable );
195 x = ht.getPosition();
196 keepGoing = keepGoing || !ht.done();
199 this._setScrollPosition( x, y );
200 this._$clip.trigger( this.options.updateEventName,
201 [ { x: x, y: y } ] );
204 this._timerID = setTimeout( this._timerCB, this._timerInterval );
210 _setElementTransform: function ( $ele, x, y, duration ) {
214 if ( !duration || duration === undefined ) {
217 transition = "-webkit-transform " + duration / 1000 + "s";
220 if ( $.support.cssTransform3d ) {
221 translate = "translate3d(" + x + "," + y + ", 0px)";
223 translate = "translate(" + x + "," + y + ")";
227 "-moz-transform": translate,
228 "-webkit-transform": translate,
229 "-ms-transform": translate,
230 "-o-transform": translate,
231 "transform": translate,
232 "-webkit-transition": transition
236 _setCalibration: function ( x, y ) {
237 if ( this.options.overshootEnable ) {
243 var $v = this._$view,
245 dirLock = this._directionLock,
249 if ( dirLock !== "y" && this._hTracker ) {
250 scroll_width = $v.width() - $c.width();
254 } else if ( x < -scroll_width ) {
255 this._sx = -scroll_width;
260 if ( scroll_width < 0 ) {
265 if ( dirLock !== "x" && this._vTracker ) {
266 scroll_height = this._getViewHeight() - $c.height();
268 this._outerScroll( y, scroll_height );
272 } else if ( y < -scroll_height ) {
273 this._sy = -scroll_height;
278 if ( scroll_height < 0 ) {
284 _setScrollPosition: function ( x, y, duration ) {
285 var $v = this._$view,
286 sm = this.options.scrollMethod,
287 $vsb = this._$vScrollBar,
288 $hsb = this._$hScrollBar,
291 this._setCalibration( x, y );
296 if ( sm === "translate" ) {
297 this._setElementTransform( $v, x + "px", y + "px", duration );
299 $v.css( {left: x + "px", top: y + "px"} );
303 $sbt = $vsb.find(".ui-scrollbar-thumb");
305 if ( sm === "translate" ) {
306 this._setElementTransform( $sbt, "0px",
307 -y / this._getViewHeight() * $sbt.parent().height() + "px",
310 $sbt.css( "top", -y / this._getViewHeight() * 100 + "%" );
315 $sbt = $hsb.find(".ui-scrollbar-thumb");
317 if ( sm === "translate" ) {
318 this._setElementTransform( $sbt,
319 -x / $v.width() * $sbt.parent().width() + "px", "0px",
322 $sbt.css("left", -x / $v.width() * 100 + "%");
327 _outerScroll: function ( y, scroll_height ) {
329 top = $( window ).scrollTop(),
331 duration = this.options.snapbackDuration,
332 start = getCurrentTime(),
335 if ( this._$clip.jqmData("scroll") !== "y" ) {
339 if ( this._outerScrolling ) {
343 if ( !this._dragging ) {
347 if ( scroll_height < 0 ) {
353 } else if ( y < -scroll_height ) {
354 sy = -y - scroll_height;
361 tfunc = function () {
362 var elapsed = getCurrentTime() - start;
364 if ( elapsed >= duration ) {
365 window.scrollTo( 0, top + sy );
366 self._outerScrolling = undefined;
368 ec = $.easing.easeOutQuad( elapsed / duration, elapsed, 0, 1, duration );
370 window.scrollTo( 0, top + ( sy * ec ) );
371 self._outerScrolling = setTimeout( tfunc, self._timerInterval );
374 this._outerScrolling = setTimeout( tfunc, self._timerInterval );
376 /* skip the srollview dragging */
377 this._skip_dragging = true;
380 _scrollTo: function ( x, y, duration ) {
382 start = getCurrentTime(),
383 efunc = $.easing.easeOutQuad,
393 tfunc = function () {
394 var elapsed = getCurrentTime() - start,
397 if ( elapsed >= duration ) {
399 self._setScrollPosition( x, y );
401 ec = efunc( elapsed / duration, elapsed, 0, 1, duration );
403 self._setScrollPosition( sx + ( dx * ec ), sy + ( dy * ec ) );
404 self._timerID = setTimeout( tfunc, self._timerInterval );
408 this._timerID = setTimeout( tfunc, this._timerInterval );
411 scrollTo: function ( x, y, duration ) {
414 if ( !duration || this.options.scrollMethod === "translate" ) {
415 this._setScrollPosition( x, y, duration );
417 this._scrollTo( x, y, duration );
421 getScrollPosition: function () {
422 return { x: -this._sx, y: -this._sy };
425 _getScrollHierarchy: function () {
429 this._$clip.parents( ".ui-scrollview-clip").each( function () {
430 d = $( this ).jqmData("scrollview");
438 _getAncestorByDirection: function ( dir ) {
439 var svh = this._getScrollHierarchy(),
446 svdir = sv.options.direction;
448 if (!svdir || svdir === dir) {
455 _handleDragStart: function ( e, ex, ey ) {
458 this._didDrag = false;
459 this._skip_dragging = false;
461 var target = $( e.target ),
464 svdir = this.options.direction;
466 /* should prevent the default behavior when click the button */
467 this._is_button = target.is( '.ui-btn-text' ) ||
468 target.is( '.ui-btn-inner' ) ||
469 target.is( '.ui-btn-inner .ui-icon' );
471 if ( this._is_button ) {
472 if ( target.parents('.ui-slider-handle') ) {
473 this._skip_dragging = true;
479 * We need to prevent the default behavior to
480 * suppress accidental selection of text, etc.
482 this._is_inputbox = target.is(':input') ||
483 target.parents(':input').length > 0;
485 if ( this._is_inputbox ) {
486 target.one( "resize.scrollview", function () {
487 if ( ey > $c.height() ) {
488 self.scrollTo( -ex, self._sy - ey + $c.height(),
489 self.options.snapbackDuration );
497 this._doSnapBackX = false;
498 this._doSnapBackY = false;
501 this._directionLock = "";
504 this._enableTracking();
506 this._set_scrollbar_size();
509 _propagateDragMove: function ( sv, e, ex, ey, dir ) {
510 this._hideScrollBars();
511 this._disableTracking();
512 sv._handleDragStart( e, ex, ey );
513 sv._directionLock = dir;
514 sv._didDrag = this._didDrag;
517 _handleDragMove: function ( e, ex, ey ) {
518 if ( this._skip_dragging ) {
522 if ( !this._dragging ) {
526 if ( !this._is_inputbox && !this._is_button ) {
530 var mt = this.options.moveThreshold,
531 dx = ex - this._lastX,
532 dy = ey - this._lastY,
533 svdir = this.options.direction,
543 if ( Math.abs( this._startY - ey ) < mt && !this._didDrag ) {
547 this._lastMove = getCurrentTime();
549 if ( !this._directionLock ) {
553 if ( x < mt && y < mt ) {
557 if ( x < y && (x / y) < 0.5 ) {
559 } else if ( x > y && (y / x) < 0.5 ) {
563 if ( svdir && dir && svdir !== dir ) {
565 * This scrollview can't handle the direction the user
566 * is attempting to scroll. Find an ancestor scrollview
567 * that can handle the request.
570 sv = this._getAncestorByDirection( dir );
572 this._propagateDragMove( sv, e, ex, ey, dir );
577 this._directionLock = svdir || (dir || "none");
582 dirLock = this._directionLock;
584 if ( dirLock !== "y" && this._hTracker ) {
589 this._doSnapBackX = false;
591 scope = ( newX > 0 || newX < this._maxX );
593 if ( scope && dirLock === "x" ) {
594 sv = this._getAncestorByDirection("x");
596 this._setScrollPosition( newX > 0 ?
597 0 : this._maxX, newY );
598 this._propagateDragMove( sv, e, ex, ey, dir );
602 newX = x + ( dx / 2 );
603 this._doSnapBackX = true;
607 if ( dirLock !== "x" && this._vTracker ) {
612 this._doSnapBackY = false;
614 scope = ( newY > 0 || newY < this._maxY );
616 if ( scope && dirLock === "y" ) {
617 sv = this._getAncestorByDirection("y");
619 this._setScrollPosition( newX,
620 newY > 0 ? 0 : this._maxY );
621 this._propagateDragMove( sv, e, ex, ey, dir );
625 newY = y + ( dy / 2 );
626 this._doSnapBackY = true;
630 if ( this.options.overshootEnable === false ) {
631 this._doSnapBackX = false;
632 this._doSnapBackY = false;
635 this._didDrag = true;
639 this._setScrollPosition( newX, newY );
641 this._showScrollBars();
644 _handleDragStop: function ( e ) {
645 if ( this._skip_dragging ) {
649 var l = this._lastMove,
650 t = getCurrentTime(),
651 doScroll = (l && (t - l) <= this.options.moveIntervalThreshold),
652 sx = ( this._hTracker && this._speedX && doScroll ) ?
653 this._speedX : ( this._doSnapBackX ? 1 : 0 ),
654 sy = ( this._vTracker && this._speedY && doScroll ) ?
655 this._speedY : ( this._doSnapBackY ? 1 : 0 ),
656 svdir = this.options.direction,
661 this._startMScroll( sx, sy );
663 this._hideScrollBars();
666 this._disableTracking();
668 return !this._didDrag;
671 _enableTracking: function () {
672 this._dragging = true;
675 _disableTracking: function () {
676 this._dragging = false;
679 _showScrollBars: function () {
680 var vclass = "ui-scrollbar-visible";
682 if ( !this.options.showScrollBars ) {
685 if ( this._scrollbar_showed ) {
689 if ( this._$vScrollBar ) {
690 this._$vScrollBar.addClass( vclass );
692 if ( this._$hScrollBar ) {
693 this._$hScrollBar.addClass( vclass );
696 this._scrollbar_showed = true;
699 _hideScrollBars: function () {
700 var vclass = "ui-scrollbar-visible";
702 if ( !this.options.showScrollBars ) {
705 if ( !this._scrollbar_showed ) {
709 if ( this._$vScrollBar ) {
710 this._$vScrollBar.removeClass( vclass );
712 if ( this._$hScrollBar ) {
713 this._$hScrollBar.removeClass( vclass );
716 this._scrollbar_showed = false;
719 _add_event: function () {
724 if ( this.options.eventType === "mouse" ) {
725 this._dragEvt = "mousedown mousemove mouseup click mousewheel";
727 this._dragCB = function ( e ) {
730 return self._handleDragStart( e,
731 e.clientX, e.clientY );
734 return self._handleDragMove( e,
735 e.clientX, e.clientY );
738 return self._handleDragStop( e );
741 return !self._didDrag;
744 var old = self.getScrollPosition();
745 self.scrollTo( -old.x,
746 -(old.y - e.originalEvent.wheelDelta) );
751 this._dragEvt = "touchstart touchmove touchend click";
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 $v.bind( this._dragEvt, this._dragCB );
778 if ( $c.jqmData("scroll") !== "y" ) {
782 $c.bind( "updatelayout", function ( e ) {
785 view_h = self._getViewHeight();
787 if ( !$c.height() || !view_h ) {
788 self.scrollTo( 0, 0, 0 );
792 sy = $c.height() - view_h;
793 vh = view_h - self._view_height;
795 self._view_height = view_h;
797 if ( vh == 0 || vh > $c.height() / 2 ) {
801 if ( self._sy - sy <= -vh ) {
802 self.scrollTo( 0, self._sy,
803 self.options.snapbackDuration );
804 } else if ( self._sy - sy <= vh + self.options.moveThreshold ) {
805 self.scrollTo( 0, sy,
806 self.options.snapbackDuration );
810 $( window ).bind( "resize", function ( e ) {
812 view_h = self._getViewHeight();
814 if ( $(".ui-page-active").get(0) !== self._page.get(0) ) {
818 if ( !$c.height() || !view_h ) {
822 focused = $c.find(".ui-focus");
825 focused.trigger("resize.scrollview");
828 /* calibration - after triggered throttledresize */
829 setTimeout( function () {
830 if ( self._sy < $c.height() - self._getViewHeight() ) {
831 self.scrollTo( 0, self._sy,
832 self.options.snapbackDuration );
836 self._view_height = view_h;
839 $( document ).one( "pageshow", function ( e ) {
840 self._page = $(".ui-page-active");
841 self._view_offset = self._$view.offset().top - self._$clip.offset().top;
842 self._view_height = self._getViewHeight();
846 _add_scrollbar: function () {
847 var $c = this._$clip,
848 prefix = "<div class=\"ui-scrollbar ui-scrollbar-",
849 suffix = "\"><div class=\"ui-scrollbar-track\"><div class=\"ui-scrollbar-thumb\"></div></div></div>";
851 if ( !this.options.showScrollBars ) {
855 if ( this._vTracker ) {
856 $c.append( prefix + "y" + suffix );
857 this._$vScrollBar = $c.children(".ui-scrollbar-y");
859 if ( this._hTracker ) {
860 $c.append( prefix + "x" + suffix );
861 this._$hScrollBar = $c.children(".ui-scrollbar-x");
864 this._scrollbar_showed = false;
867 _add_scroll_jump: function () {
868 var $c = this._$clip,
873 if ( !this.options.scrollJump ) {
877 if ( this._vTracker ) {
878 top_btn = $( '<div class="ui-scroll-jump-top-bg ui-btn" data-theme="s">' +
879 '<div class="ui-scroll-jump-top"></div></div>' );
880 $c.append( top_btn );
882 top_btn.bind( "vclick", function () {
883 self.scrollTo( 0, 0, self.options.overshootDuration );
887 if ( this._hTracker ) {
888 left_btn = $( '<div class="ui-scroll-jump-left-bg ui-btn" data-theme="s">' +
889 '<div class="ui-scroll-jump-left"></div></div>' );
890 $c.append( left_btn );
892 left_btn.bind( "vclick", function () {
893 self.scrollTo( 0, 0, self.options.overshootDuration );
898 _set_scrollbar_size: function () {
899 var $c = this._$clip,
907 if ( !this.options.showScrollBars ) {
911 if ( this._hTracker ) {
914 this._maxX = cw - vw;
916 if ( this._maxX > 0 ) {
919 if ( this._$hScrollBar && vw ) {
920 thumb = this._$hScrollBar.find(".ui-scrollbar-thumb");
921 thumb.css( "width", (cw >= vw ? "100%" :
922 (Math.floor(cw / vw * 100) || 1) + "%") );
926 if ( this._vTracker ) {
928 vh = this._getViewHeight();
929 this._maxY = ch - vh;
931 if ( this._maxY > 0 ) {
934 if ( this._$vScrollBar && vh ) {
935 thumb = this._$vScrollBar.find(".ui-scrollbar-thumb");
936 thumb.css( "height", (ch >= vh ? "100%" :
937 (Math.floor(ch / vh * 100) || 1) + "%") );
943 $.extend( MomentumTracker.prototype, {
944 start: function ( pos, speed, duration, minPos, maxPos ) {
945 var tstate = ( pos < minPos || pos > maxPos ) ?
946 tstates.snapback : tstates.scrolling,
949 this.state = ( speed !== 0 ) ? tstate : tstates.done;
952 this.duration = ( this.state === tstates.snapback ) ?
953 this.options.snapbackDuration : duration;
954 this.minPos = minPos;
955 this.maxPos = maxPos;
957 this.fromPos = ( this.state === tstates.snapback ) ? this.pos : 0;
958 pos_temp = ( this.pos < this.minPos ) ? this.minPos : this.maxPos;
959 this.toPos = ( this.state === tstates.snapback ) ? pos_temp : 0;
961 this.startTime = getCurrentTime();
965 this.state = tstates.done;
973 update: function ( overshootEnable ) {
974 var state = this.state,
975 cur_time = getCurrentTime(),
976 duration = this.duration,
977 elapsed = cur_time - this.startTime,
982 if ( state === tstates.done ) {
986 elapsed = elapsed > duration ? duration : elapsed;
988 if ( state === tstates.scrolling || state === tstates.overshot ) {
990 ( 1 - $.easing[this.easing]( elapsed / duration,
991 elapsed, 0, 1, duration ) );
995 didOverShoot = ( state === tstates.scrolling ) &&
996 ( x < this.minPos || x > this.maxPos );
998 if ( didOverShoot ) {
999 x = ( x < this.minPos ) ? this.minPos : this.maxPos;
1004 if ( state === tstates.overshot ) {
1005 if ( elapsed >= duration ) {
1006 this.state = tstates.snapback;
1007 this.fromPos = this.pos;
1008 this.toPos = ( x < this.minPos ) ?
1009 this.minPos : this.maxPos;
1010 this.duration = this.options.snapbackDuration;
1011 this.startTime = cur_time;
1014 } else if ( state === tstates.scrolling ) {
1015 if ( didOverShoot && overshootEnable ) {
1016 this.state = tstates.overshot;
1017 this.speed = dx / 2;
1018 this.duration = this.options.overshootDuration;
1019 this.startTime = cur_time;
1020 } else if ( elapsed >= duration ) {
1021 this.state = tstates.done;
1024 } else if ( state === tstates.snapback ) {
1025 if ( elapsed >= duration ) {
1026 this.pos = this.toPos;
1027 this.state = tstates.done;
1029 this.pos = this.fromPos + (( this.toPos - this.fromPos ) *
1030 $.easing[this.easing]( elapsed / duration,
1031 elapsed, 0, 1, duration ));
1039 return this.state === tstates.done;
1042 getPosition: function () {
1047 $( document ).bind( 'pagecreate create', function ( e ) {
1048 var $page = $( e.target ),
1049 content_scroll = $page.find(".ui-content").jqmData("scroll");
1051 /* content scroll */
1052 if ( $.support.scrollview === undefined ) {
1053 $.support.scrollview = true;
1056 if ( $.support.scrollview === true && content_scroll === undefined ) {
1057 content_scroll = "y";
1060 if ( content_scroll !== "y" ) {
1061 content_scroll = "none";
1064 $page.find(".ui-content").attr( "data-scroll", content_scroll );
1066 $page.find(":jqmData(scroll):not(.ui-scrollview-clip)").each( function () {
1067 if ( $( this ).hasClass("ui-scrolllistview") ) {
1068 $( this ).scrolllistview();
1070 var st = $( this ).jqmData("scroll"),
1071 dir = st && ( st.search(/^[xy]/) !== -1 ) ? st.charAt(0) : null,
1074 if ( st === "none" ) {
1079 direction: dir || undefined,
1080 scrollMethod: $( this ).jqmData("scroll-method") || undefined,
1081 scrollJump: $( this ).jqmData("scroll-jump") || undefined
1084 $( this ).scrollview( opts );
1089 $( document ).bind( 'pageshow', function ( e ) {
1090 var $page = $( e.target ),
1091 scroll = $page.find(".ui-content").jqmData("scroll");
1093 if ( scroll === "y" ) {
1094 resizePageContentHeight( e.target );
1098 }( jQuery, window, document ) );