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 direction: null, // "x", "y", or null for both.
46 scrollDuration: 1000, // 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,
62 outerScrollEnable: true,
66 _getViewHeight: function () {
67 return this._$view.height() + this._view_offset;
70 _makePositioned: function ( $ele ) {
71 if ( $ele.css("position") === "static" ) {
72 $ele.css( "position", "relative" );
76 _create: function () {
80 this._$clip = $( this.element ).addClass("ui-scrollview-clip");
82 if ( this._$clip.children(".ui-scrollview-view").length ) {
83 this._$view = this._$clip.children(".ui-scrollview-view");
85 this._$view = this._$clip.wrapInner("<div></div>").children()
86 .addClass("ui-scrollview-view");
89 if ( this.options.scrollMethod === "translate" ) {
90 if ( this._$view.css("transform") === undefined ) {
91 this.options.scrollMethod = "position";
95 this._$clip.css( "overflow", "hidden" );
96 this._makePositioned( this._$clip );
98 this._makePositioned( this._$view );
99 this._$view.css( { left: 0, top: 0 } );
101 this._view_offset = this._$view.offset().top - this._$clip.offset().top;
102 this._view_height = this._getViewHeight();
107 direction = this.options.direction;
109 this._hTracker = ( direction !== "y" ) ?
110 new MomentumTracker( this.options ) : null;
111 this._vTracker = ( direction !== "x" ) ?
112 new MomentumTracker( this.options ) : null;
114 this._timerInterval = this.options.timerInterval;
117 this._timerCB = function () {
118 self._handleMomentumScroll();
122 this._add_scrollbar();
123 this._add_scroll_jump();
126 _startMScroll: function ( speedX, speedY ) {
127 var keepGoing = false,
128 duration = this.options.scrollDuration,
135 this._showScrollBars();
137 this._$clip.trigger( this.options.startEventName );
140 c = this._$clip.width();
141 v = this._$view.width();
143 ht.start( this._sx, speedX,
144 duration, (v > c) ? -(v - c) : 0, 0 );
145 keepGoing = !ht.done();
149 c = this._$clip.height();
150 v = this._getViewHeight();
152 vt.start( this._sy, speedY,
153 duration, (v > c) ? -(v - c) : 0, 0 );
154 keepGoing = keepGoing || !vt.done();
158 this._timerID = setTimeout( this._timerCB, this._timerInterval );
164 _stopMScroll: function () {
165 if ( this._timerID ) {
166 this._$clip.trigger( this.options.stopEventName );
167 clearTimeout( this._timerID );
171 if ( this._vTracker ) {
172 this._vTracker.reset();
175 if ( this._hTracker ) {
176 this._hTracker.reset();
179 this._hideScrollBars();
182 _handleMomentumScroll: function () {
183 var keepGoing = false,
190 if ( this._outerScrolling ) {
195 vt.update( this.options.overshootEnable );
196 y = vt.getPosition();
197 keepGoing = !vt.done();
199 if ( vt.getRemained() > this.options.overshootDuration ) {
200 scroll_height = this._getViewHeight() - this._$clip.height();
203 this._outerScroll( y - vt.getRemained() / 3, scroll_height );
204 } else if ( vt.isMax() ) {
205 this._outerScroll( vt.getRemained() / 3, scroll_height );
211 ht.update( this.options.overshootEnable );
212 x = ht.getPosition();
213 keepGoing = keepGoing || !ht.done();
216 this._setScrollPosition( x, y );
217 this._$clip.trigger( this.options.updateEventName,
218 [ { x: x, y: y } ] );
221 this._timerID = setTimeout( this._timerCB, this._timerInterval );
227 _setElementTransform: function ( $ele, x, y, duration ) {
231 if ( !duration || duration === undefined ) {
234 transition = "-webkit-transform " + duration / 1000 + "s ease-out";
237 if ( $.support.cssTransform3d ) {
238 translate = "translate3d(" + x + "," + y + ", 0px)";
240 translate = "translate(" + x + "," + y + ")";
244 "-moz-transform": translate,
245 "-webkit-transform": translate,
246 "-ms-transform": translate,
247 "-o-transform": translate,
248 "transform": translate,
249 "-webkit-transition": transition
253 _setCalibration: function ( x, y ) {
254 if ( this.options.overshootEnable ) {
260 var $v = this._$view,
262 dirLock = this._directionLock,
266 if ( dirLock !== "y" && this._hTracker ) {
267 scroll_width = $v.width() - $c.width();
271 } else if ( x < -scroll_width ) {
272 this._sx = -scroll_width;
277 if ( scroll_width < 0 ) {
282 if ( dirLock !== "x" && this._vTracker ) {
283 scroll_height = this._getViewHeight() - $c.height();
287 } else if ( y < -scroll_height ) {
288 this._sy = -scroll_height;
293 if ( scroll_height < 0 ) {
299 _setScrollPosition: function ( x, y, duration ) {
300 var $v = this._$view,
301 sm = this.options.scrollMethod,
302 $vsb = this._$vScrollBar,
303 $hsb = this._$hScrollBar,
306 this._setCalibration( x, y );
311 if ( sm === "translate" ) {
312 this._setElementTransform( $v, x + "px", y + "px", duration );
314 $v.css( {left: x + "px", top: y + "px"} );
318 $sbt = $vsb.find(".ui-scrollbar-thumb");
320 if ( sm === "translate" ) {
321 this._setElementTransform( $sbt, "0px",
322 -y / this._getViewHeight() * $sbt.parent().height() + "px",
325 $sbt.css( "top", -y / this._getViewHeight() * 100 + "%" );
330 $sbt = $hsb.find(".ui-scrollbar-thumb");
332 if ( sm === "translate" ) {
333 this._setElementTransform( $sbt,
334 -x / $v.width() * $sbt.parent().width() + "px", "0px",
337 $sbt.css("left", -x / $v.width() * 100 + "%");
342 _outerScroll: function ( y, scroll_height ) {
344 top = $( window ).scrollTop() - window.screenTop,
346 duration = this.options.snapbackDuration,
347 start = getCurrentTime(),
350 if ( !this.options.outerScrollEnable ) {
354 if ( this._$clip.jqmData("scroll") !== "y" ) {
358 if ( this._outerScrolling ) {
362 if ( scroll_height < 0 ) {
368 } else if ( y < -scroll_height ) {
369 sy = -y - scroll_height;
374 tfunc = function () {
375 var elapsed = getCurrentTime() - start;
377 if ( elapsed >= duration ) {
378 window.scrollTo( 0, top + sy );
379 self._outerScrolling = undefined;
383 ec = $.easing.easeOutQuad( elapsed / duration,
384 elapsed, 0, 1, duration );
386 window.scrollTo( 0, top + ( sy * ec ) );
387 self._outerScrolling = setTimeout( tfunc, self._timerInterval );
390 this._outerScrolling = setTimeout( tfunc, self._timerInterval );
393 _scrollTo: function ( x, y, duration ) {
395 start = getCurrentTime(),
396 efunc = $.easing.easeOutQuad,
406 tfunc = function () {
407 var elapsed = getCurrentTime() - start,
410 if ( elapsed >= duration ) {
412 self._setScrollPosition( x, y );
414 ec = efunc( elapsed / duration, elapsed, 0, 1, duration );
416 self._setScrollPosition( sx + ( dx * ec ), sy + ( dy * ec ) );
417 self._timerID = setTimeout( tfunc, self._timerInterval );
421 this._timerID = setTimeout( tfunc, this._timerInterval );
424 scrollTo: function ( x, y, duration ) {
427 if ( !duration || this.options.scrollMethod === "translate" ) {
428 this._setScrollPosition( x, y, duration );
430 this._scrollTo( x, y, duration );
434 getScrollPosition: function () {
435 return { x: -this._sx, y: -this._sy };
438 _getScrollHierarchy: function () {
442 this._$clip.parents( ".ui-scrollview-clip").each( function () {
443 d = $( this ).jqmData("scrollview");
451 _getAncestorByDirection: function ( dir ) {
452 var svh = this._getScrollHierarchy(),
459 svdir = sv.options.direction;
461 if (!svdir || svdir === dir) {
468 _handleDragStart: function ( e, ex, ey ) {
471 this._didDrag = false;
472 this._skip_dragging = false;
474 var target = $( e.target ),
477 svdir = this.options.direction;
479 /* should prevent the default behavior when click the button */
480 this._is_button = target.is( '.ui-btn-text' ) ||
481 target.is( '.ui-btn-inner' ) ||
482 target.is( '.ui-btn-inner .ui-icon' );
484 if ( this._is_button ) {
485 if ( target.parents('.ui-slider-handle').length ) {
486 this._skip_dragging = true;
492 * We need to prevent the default behavior to
493 * suppress accidental selection of text, etc.
495 this._is_inputbox = target.is(':input') ||
496 target.parents(':input').length > 0;
498 if ( this._is_inputbox ) {
499 target.one( "resize.scrollview", function () {
500 if ( ey > $c.height() ) {
501 self.scrollTo( -ex, self._sy - ey + $c.height(),
502 self.options.snapbackDuration );
507 if ( this.options.eventType === "mouse" && !this._is_inputbox && !this._is_button ) {
514 this._doSnapBackX = false;
515 this._doSnapBackY = false;
518 this._directionLock = "";
521 this._enableTracking();
523 this._set_scrollbar_size();
526 _propagateDragMove: function ( sv, e, ex, ey, dir ) {
527 this._hideScrollBars();
528 this._disableTracking();
529 sv._handleDragStart( e, ex, ey );
530 sv._directionLock = dir;
531 sv._didDrag = this._didDrag;
534 _handleDragMove: function ( e, ex, ey ) {
535 if ( this._skip_dragging ) {
539 if ( !this._dragging ) {
543 if ( !this._is_inputbox && !this._is_button ) {
547 var mt = this.options.moveThreshold,
548 dx = ex - this._lastX,
549 dy = ey - this._lastY,
550 svdir = this.options.direction,
560 this._lastMove = getCurrentTime();
562 if ( !this._directionLock ) {
566 if ( x < mt && y < mt ) {
570 if ( x < y && (x / y) < 0.5 ) {
572 } else if ( x > y && (y / x) < 0.5 ) {
576 if ( svdir && dir && svdir !== dir ) {
578 * This scrollview can't handle the direction the user
579 * is attempting to scroll. Find an ancestor scrollview
580 * that can handle the request.
583 sv = this._getAncestorByDirection( dir );
585 this._propagateDragMove( sv, e, ex, ey, dir );
590 this._directionLock = svdir || (dir || "none");
595 dirLock = this._directionLock;
597 if ( dirLock !== "y" && this._hTracker ) {
602 this._doSnapBackX = false;
604 scope = ( newX > 0 || newX < this._maxX );
606 if ( scope && dirLock === "x" ) {
607 sv = this._getAncestorByDirection("x");
609 this._setScrollPosition( newX > 0 ?
610 0 : this._maxX, newY );
611 this._propagateDragMove( sv, e, ex, ey, dir );
615 newX = x + ( dx / 2 );
616 this._doSnapBackX = true;
620 if ( dirLock !== "x" && this._vTracker ) {
621 if ( Math.abs( this._startY - ey ) < mt && dirLock !== "xy" ) {
629 this._doSnapBackY = false;
631 scope = ( newY > 0 || newY < this._maxY );
633 if ( scope && dirLock === "y" ) {
634 sv = this._getAncestorByDirection("y");
636 this._setScrollPosition( newX,
637 newY > 0 ? 0 : this._maxY );
638 this._propagateDragMove( sv, e, ex, ey, dir );
642 newY = y + ( dy / 2 );
643 this._doSnapBackY = true;
647 if ( this.options.overshootEnable === false ) {
648 this._doSnapBackX = false;
649 this._doSnapBackY = false;
652 this._didDrag = true;
656 this._setScrollPosition( newX, newY );
658 this._showScrollBars();
661 _handleDragStop: function ( e ) {
662 if ( this._skip_dragging ) {
666 var l = this._lastMove,
667 t = getCurrentTime(),
668 doScroll = (l && (t - l) <= this.options.moveIntervalThreshold),
669 sx = ( this._hTracker && this._speedX && doScroll ) ?
670 this._speedX : ( this._doSnapBackX ? 1 : 0 ),
671 sy = ( this._vTracker && this._speedY && doScroll ) ?
672 this._speedY : ( this._doSnapBackY ? 1 : 0 ),
673 svdir = this.options.direction,
678 this._startMScroll( sx, sy );
680 this._hideScrollBars();
683 this._disableTracking();
685 return !this._didDrag;
688 _enableTracking: function () {
689 this._dragging = true;
692 _disableTracking: function () {
693 this._dragging = false;
696 _showScrollBars: function ( interval ) {
697 var vclass = "ui-scrollbar-visible",
700 if ( !this.options.showScrollBars ) {
703 if ( this._scrollbar_showed ) {
707 if ( this._$vScrollBar ) {
708 this._$vScrollBar.addClass( vclass );
710 if ( this._$hScrollBar ) {
711 this._$hScrollBar.addClass( vclass );
714 this._scrollbar_showed = true;
717 setTimeout( function () {
718 self._hideScrollBars();
723 _hideScrollBars: function () {
724 var vclass = "ui-scrollbar-visible";
726 if ( !this.options.showScrollBars ) {
729 if ( !this._scrollbar_showed ) {
733 if ( this._$vScrollBar ) {
734 this._$vScrollBar.removeClass( vclass );
736 if ( this._$hScrollBar ) {
737 this._$hScrollBar.removeClass( vclass );
740 this._scrollbar_showed = false;
743 _add_event: function () {
748 if ( this.options.eventType === "mouse" ) {
749 this._dragEvt = "mousedown mousemove mouseup click mousewheel";
751 this._dragCB = function ( e ) {
754 return self._handleDragStart( e,
755 e.clientX, e.clientY );
758 return self._handleDragMove( e,
759 e.clientX, e.clientY );
762 return self._handleDragStop( e );
765 return !self._didDrag;
768 var old = self.getScrollPosition();
769 self.scrollTo( -old.x,
770 -(old.y - e.originalEvent.wheelDelta) );
775 this._dragEvt = "touchstart touchmove touchend click";
777 this._dragCB = function ( e ) {
782 t = e.originalEvent.targetTouches[0];
783 return self._handleDragStart( e,
787 t = e.originalEvent.targetTouches[0];
788 return self._handleDragMove( e,
792 return self._handleDragStop( e );
795 return !self._didDrag;
800 $v.bind( this._dragEvt, this._dragCB );
802 $c.bind( "updatelayout", function ( e ) {
805 view_h = self._getViewHeight();
807 if ( !$c.height() || !view_h ) {
808 self.scrollTo( 0, 0, 0 );
812 sy = $c.height() - view_h;
813 vh = view_h - self._view_height;
815 self._view_height = view_h;
817 if ( vh == 0 || vh > $c.height() / 2 ) {
821 if ( self._sy - sy <= -vh ) {
822 self.scrollTo( 0, self._sy,
823 self.options.snapbackDuration );
824 } else if ( self._sy - sy <= vh + self.options.moveThreshold ) {
825 self.scrollTo( 0, sy,
826 self.options.snapbackDuration );
830 $( window ).bind( "resize", function ( e ) {
832 view_h = self._getViewHeight();
834 if ( $(".ui-page-active").get(0) !== $c.closest(".ui-page").get(0) ) {
838 if ( !$c.height() || !view_h ) {
842 focused = $c.find(".ui-focus");
845 focused.trigger("resize.scrollview");
848 /* calibration - after triggered throttledresize */
849 setTimeout( function () {
850 if ( self._sy < $c.height() - self._getViewHeight() ) {
851 self.scrollTo( 0, self._sy,
852 self.options.snapbackDuration );
856 self._view_height = view_h;
859 $c.closest(".ui-page")
860 .one( "pageshow", function ( e ) {
861 self._view_offset = self._$view.offset().top - self._$clip.offset().top;
862 self._view_height = self._getViewHeight();
864 .bind( "pageshow", function ( e ) {
865 /* should be called after pagelayout */
866 setTimeout( function () {
867 self._set_scrollbar_size();
868 self._setScrollPosition( self._sx, self._sy );
869 self._showScrollBars( 2000 );
874 _add_scrollbar: function () {
875 var $c = this._$clip,
876 prefix = "<div class=\"ui-scrollbar ui-scrollbar-",
877 suffix = "\"><div class=\"ui-scrollbar-track\"><div class=\"ui-scrollbar-thumb\"></div></div></div>";
879 if ( !this.options.showScrollBars ) {
883 if ( this._vTracker ) {
884 $c.append( prefix + "y" + suffix );
885 this._$vScrollBar = $c.children(".ui-scrollbar-y");
887 if ( this._hTracker ) {
888 $c.append( prefix + "x" + suffix );
889 this._$hScrollBar = $c.children(".ui-scrollbar-x");
892 this._scrollbar_showed = false;
895 _add_scroll_jump: function () {
896 var $c = this._$clip,
901 if ( !this.options.scrollJump ) {
905 if ( this._vTracker ) {
906 top_btn = $( '<div class="ui-scroll-jump-top-bg ui-btn" data-theme="s">' +
907 '<div class="ui-scroll-jump-top"></div></div>' );
908 $c.append( top_btn );
910 top_btn.bind( "vclick", function () {
911 self.scrollTo( 0, 0, self.options.overshootDuration );
915 if ( this._hTracker ) {
916 left_btn = $( '<div class="ui-scroll-jump-left-bg ui-btn" data-theme="s">' +
917 '<div class="ui-scroll-jump-left"></div></div>' );
918 $c.append( left_btn );
920 left_btn.bind( "vclick", function () {
921 self.scrollTo( 0, 0, self.options.overshootDuration );
926 _set_scrollbar_size: function () {
927 var $c = this._$clip,
935 if ( !this.options.showScrollBars ) {
939 if ( this._hTracker ) {
942 this._maxX = cw - vw;
944 if ( this._maxX > 0 ) {
947 if ( this._$hScrollBar && vw ) {
948 thumb = this._$hScrollBar.find(".ui-scrollbar-thumb");
949 thumb.css( "width", (cw >= vw ? "0" :
950 (Math.floor(cw / vw * 100) || 1) + "%") );
954 if ( this._vTracker ) {
956 vh = this._getViewHeight();
957 this._maxY = ch - vh;
959 if ( this._maxY > 0 ) {
962 if ( this._$vScrollBar && vh ) {
963 thumb = this._$vScrollBar.find(".ui-scrollbar-thumb");
964 thumb.css( "height", (ch >= vh ? "0" :
965 (Math.floor(ch / vh * 100) || 1) + "%") );
971 $.extend( MomentumTracker.prototype, {
972 start: function ( pos, speed, duration, minPos, maxPos ) {
973 var tstate = ( pos < minPos || pos > maxPos ) ?
974 tstates.snapback : tstates.scrolling,
977 this.state = ( speed !== 0 ) ? tstate : tstates.done;
980 this.duration = ( this.state === tstates.snapback ) ?
981 this.options.snapbackDuration : duration;
982 this.minPos = minPos;
983 this.maxPos = maxPos;
985 this.fromPos = ( this.state === tstates.snapback ) ? this.pos : 0;
986 pos_temp = ( this.pos < this.minPos ) ? this.minPos : this.maxPos;
987 this.toPos = ( this.state === tstates.snapback ) ? pos_temp : 0;
989 this.startTime = getCurrentTime();
993 this.state = tstates.done;
1002 update: function ( overshootEnable ) {
1003 var state = this.state,
1004 cur_time = getCurrentTime(),
1005 duration = this.duration,
1006 elapsed = cur_time - this.startTime,
1011 if ( state === tstates.done ) {
1015 elapsed = elapsed > duration ? duration : elapsed;
1017 this.remained = duration - elapsed;
1019 if ( state === tstates.scrolling || state === tstates.overshot ) {
1021 ( 1 - $.easing[this.easing]( elapsed / duration,
1022 elapsed, 0, 1, duration ) );
1026 didOverShoot = ( state === tstates.scrolling ) &&
1027 ( x < this.minPos || x > this.maxPos );
1029 if ( didOverShoot ) {
1030 x = ( x < this.minPos ) ? this.minPos : this.maxPos;
1035 if ( state === tstates.overshot ) {
1036 if ( !overshootEnable ) {
1037 this.state = tstates.done;
1039 if ( elapsed >= duration ) {
1040 this.state = tstates.snapback;
1041 this.fromPos = this.pos;
1042 this.toPos = ( x < this.minPos ) ?
1043 this.minPos : this.maxPos;
1044 this.duration = this.options.snapbackDuration;
1045 this.startTime = cur_time;
1048 } else if ( state === tstates.scrolling ) {
1049 if ( didOverShoot && overshootEnable ) {
1050 this.state = tstates.overshot;
1051 this.speed = dx / 2;
1052 this.duration = this.options.overshootDuration;
1053 this.startTime = cur_time;
1054 } else if ( elapsed >= duration ) {
1055 this.state = tstates.done;
1058 } else if ( state === tstates.snapback ) {
1059 if ( elapsed >= duration ) {
1060 this.pos = this.toPos;
1061 this.state = tstates.done;
1063 this.pos = this.fromPos + (( this.toPos - this.fromPos ) *
1064 $.easing[this.easing]( elapsed / duration,
1065 elapsed, 0, 1, duration ));
1073 return this.state === tstates.done;
1076 isMin: function () {
1077 return this.pos === this.minPos;
1080 isMax: function () {
1081 return this.pos === this.maxPos;
1084 getRemained: function () {
1085 return this.remained;
1088 getPosition: function () {
1093 $( document ).bind( 'pagecreate create', function ( e ) {
1094 var $page = $( e.target ),
1095 content_scroll = $page.find(".ui-content").jqmData("scroll");
1097 /* content scroll */
1098 if ( $.support.scrollview === undefined ) {
1099 $.support.scrollview = true;
1102 if ( $.support.scrollview === true && content_scroll === undefined ) {
1103 content_scroll = "y";
1106 if ( content_scroll !== "y" ) {
1107 content_scroll = "none";
1110 $page.find(".ui-content").attr( "data-scroll", content_scroll );
1112 $page.find(":jqmData(scroll):not(.ui-scrollview-clip)").each( function () {
1113 if ( $( this ).hasClass("ui-scrolllistview") ) {
1114 $( this ).scrolllistview();
1116 var st = $( this ).jqmData("scroll"),
1117 dir = st && ( st.search(/^[xy]/) !== -1 ) ? st : null,
1120 if ( st === "none" ) {
1125 direction: dir || undefined,
1126 scrollMethod: $( this ).jqmData("scroll-method") || undefined,
1127 scrollJump: $( this ).jqmData("scroll-jump") || undefined
1130 $( this ).scrollview( opts );
1135 $( document ).bind( 'pageshow', function ( e ) {
1136 var $page = $( e.target ),
1137 scroll = $page.find(".ui-content").jqmData("scroll");
1139 if ( scroll === "y" ) {
1140 resizePageContentHeight( e.target );
1144 }( jQuery, window, document ) );