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: false,
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");
81 this._$view = this._$clip.wrapInner("<div></div>").children()
82 .addClass("ui-scrollview-view");
84 if ( this.options.scrollMethod === "translate" ) {
85 if ( this._$view.css("transform") === undefined ) {
86 this.options.scrollMethod = "position";
90 this._$clip.css( "overflow", "hidden" );
91 this._makePositioned( this._$clip );
93 this._makePositioned( this._$view );
94 this._$view.css( { left: 0, top: 0 } );
96 this._view_offset = this._$view.offset().top - this._$clip.offset().top;
97 this._view_height = this._getViewHeight();
102 direction = this.options.direction;
104 this._hTracker = ( direction !== "y" ) ?
105 new MomentumTracker( this.options ) : null;
106 this._vTracker = ( direction !== "x" ) ?
107 new MomentumTracker( this.options ) : null;
109 this._timerInterval = this.options.timerInterval;
112 this._timerCB = function () {
113 self._handleMomentumScroll();
117 this._add_scrollbar();
118 this._add_scroll_jump();
121 _startMScroll: function ( speedX, speedY ) {
122 var keepGoing = false,
123 duration = this.options.scrollDuration,
130 this._showScrollBars();
132 this._$clip.trigger( this.options.startEventName );
135 c = this._$clip.width();
136 v = this._$view.width();
138 ht.start( this._sx, speedX,
139 duration, (v > c) ? -(v - c) : 0, 0 );
140 keepGoing = !ht.done();
144 c = this._$clip.height();
145 v = this._getViewHeight();
147 vt.start( this._sy, speedY,
148 duration, (v > c) ? -(v - c) : 0, 0 );
149 keepGoing = keepGoing || !vt.done();
153 this._timerID = setTimeout( this._timerCB, this._timerInterval );
159 _stopMScroll: function () {
160 if ( this._timerID ) {
161 this._$clip.trigger( this.options.stopEventName );
162 clearTimeout( this._timerID );
166 if ( this._vTracker ) {
167 this._vTracker.reset();
170 if ( this._hTracker ) {
171 this._hTracker.reset();
174 this._hideScrollBars();
177 _handleMomentumScroll: function () {
178 var keepGoing = false,
184 if ( this._outerScrolling ) {
189 vt.update( this.options.overshootEnable );
190 y = vt.getPosition();
191 keepGoing = !vt.done();
195 ht.update( this.options.overshootEnable );
196 x = ht.getPosition();
197 keepGoing = keepGoing || !ht.done();
200 this._setScrollPosition( x, y );
201 this._$clip.trigger( this.options.updateEventName,
202 [ { x: x, y: y } ] );
205 this._timerID = setTimeout( this._timerCB, this._timerInterval );
211 _setElementTransform: function ( $ele, x, y, duration ) {
215 if ( !duration || duration === undefined ) {
218 transition = "-webkit-transform " + duration / 1000 + "s ease-out";
221 if ( $.support.cssTransform3d ) {
222 translate = "translate3d(" + x + "," + y + ", 0px)";
224 translate = "translate(" + x + "," + y + ")";
228 "-moz-transform": translate,
229 "-webkit-transform": translate,
230 "-ms-transform": translate,
231 "-o-transform": translate,
232 "transform": translate,
233 "-webkit-transition": transition
237 _setCalibration: function ( x, y ) {
238 if ( this.options.overshootEnable ) {
244 var $v = this._$view,
246 dirLock = this._directionLock,
250 if ( dirLock !== "y" && this._hTracker ) {
251 scroll_width = $v.width() - $c.width();
255 } else if ( x < -scroll_width ) {
256 this._sx = -scroll_width;
261 if ( scroll_width < 0 ) {
266 if ( dirLock !== "x" && this._vTracker ) {
267 scroll_height = this._getViewHeight() - $c.height();
269 this._outerScroll( y, scroll_height );
273 } else if ( y < -scroll_height ) {
274 this._sy = -scroll_height;
279 if ( scroll_height < 0 ) {
285 _setScrollPosition: function ( x, y, duration ) {
286 var $v = this._$view,
287 sm = this.options.scrollMethod,
288 $vsb = this._$vScrollBar,
289 $hsb = this._$hScrollBar,
292 this._setCalibration( x, y );
297 if ( sm === "translate" ) {
298 this._setElementTransform( $v, x + "px", y + "px", duration );
300 $v.css( {left: x + "px", top: y + "px"} );
304 $sbt = $vsb.find(".ui-scrollbar-thumb");
306 if ( sm === "translate" ) {
307 this._setElementTransform( $sbt, "0px",
308 -y / this._getViewHeight() * $sbt.parent().height() + "px",
311 $sbt.css( "top", -y / this._getViewHeight() * 100 + "%" );
316 $sbt = $hsb.find(".ui-scrollbar-thumb");
318 if ( sm === "translate" ) {
319 this._setElementTransform( $sbt,
320 -x / $v.width() * $sbt.parent().width() + "px", "0px",
323 $sbt.css("left", -x / $v.width() * 100 + "%");
328 _outerScroll: function ( y, scroll_height ) {
330 top = $( window ).scrollTop(),
332 duration = this.options.snapbackDuration,
333 start = getCurrentTime(),
336 if ( !this.options.outerScrollEnable ) {
340 if ( this._$clip.jqmData("scroll") !== "y" ) {
344 if ( this._outerScrolling ) {
348 if ( !this._dragging ) {
352 if ( scroll_height < 0 ) {
358 } else if ( y < -scroll_height ) {
359 sy = -y - scroll_height;
366 tfunc = function () {
367 var elapsed = getCurrentTime() - start;
369 if ( elapsed >= duration ) {
370 window.scrollTo( 0, top + sy );
371 self._outerScrolling = undefined;
373 ec = $.easing.easeOutQuad( elapsed / duration, elapsed, 0, 1, duration );
375 window.scrollTo( 0, top + ( sy * ec ) );
376 self._outerScrolling = setTimeout( tfunc, self._timerInterval );
379 this._outerScrolling = setTimeout( tfunc, self._timerInterval );
381 /* skip the srollview dragging */
382 this._skip_dragging = true;
385 _scrollTo: function ( x, y, duration ) {
387 start = getCurrentTime(),
388 efunc = $.easing.easeOutQuad,
398 tfunc = function () {
399 var elapsed = getCurrentTime() - start,
402 if ( elapsed >= duration ) {
404 self._setScrollPosition( x, y );
406 ec = efunc( elapsed / duration, elapsed, 0, 1, duration );
408 self._setScrollPosition( sx + ( dx * ec ), sy + ( dy * ec ) );
409 self._timerID = setTimeout( tfunc, self._timerInterval );
413 this._timerID = setTimeout( tfunc, this._timerInterval );
416 scrollTo: function ( x, y, duration ) {
419 if ( !duration || this.options.scrollMethod === "translate" ) {
420 this._setScrollPosition( x, y, duration );
422 this._scrollTo( x, y, duration );
426 getScrollPosition: function () {
427 return { x: -this._sx, y: -this._sy };
430 _getScrollHierarchy: function () {
434 this._$clip.parents( ".ui-scrollview-clip").each( function () {
435 d = $( this ).jqmData("scrollview");
443 _getAncestorByDirection: function ( dir ) {
444 var svh = this._getScrollHierarchy(),
451 svdir = sv.options.direction;
453 if (!svdir || svdir === dir) {
460 _handleDragStart: function ( e, ex, ey ) {
463 this._didDrag = false;
464 this._skip_dragging = false;
466 var target = $( e.target ),
469 svdir = this.options.direction;
471 /* should prevent the default behavior when click the button */
472 this._is_button = target.is( '.ui-btn-text' ) ||
473 target.is( '.ui-btn-inner' ) ||
474 target.is( '.ui-btn-inner .ui-icon' );
476 if ( this._is_button ) {
477 if ( target.parents('.ui-slider-handle') ) {
478 this._skip_dragging = true;
484 * We need to prevent the default behavior to
485 * suppress accidental selection of text, etc.
487 this._is_inputbox = target.is(':input') ||
488 target.parents(':input').length > 0;
490 if ( this._is_inputbox ) {
491 target.one( "resize.scrollview", function () {
492 if ( ey > $c.height() ) {
493 self.scrollTo( -ex, self._sy - ey + $c.height(),
494 self.options.snapbackDuration );
502 this._doSnapBackX = false;
503 this._doSnapBackY = false;
506 this._directionLock = "";
509 this._enableTracking();
511 this._set_scrollbar_size();
514 _propagateDragMove: function ( sv, e, ex, ey, dir ) {
515 this._hideScrollBars();
516 this._disableTracking();
517 sv._handleDragStart( e, ex, ey );
518 sv._directionLock = dir;
519 sv._didDrag = this._didDrag;
522 _handleDragMove: function ( e, ex, ey ) {
523 if ( this._skip_dragging ) {
527 if ( !this._dragging ) {
531 if ( !this._is_inputbox && !this._is_button ) {
535 var mt = this.options.moveThreshold,
536 dx = ex - this._lastX,
537 dy = ey - this._lastY,
538 svdir = this.options.direction,
548 if ( Math.abs( this._startY - ey ) < mt && !this._didDrag ) {
552 this._lastMove = getCurrentTime();
554 if ( !this._directionLock ) {
558 if ( x < mt && y < mt ) {
562 if ( x < y && (x / y) < 0.5 ) {
564 } else if ( x > y && (y / x) < 0.5 ) {
568 if ( svdir && dir && svdir !== dir ) {
570 * This scrollview can't handle the direction the user
571 * is attempting to scroll. Find an ancestor scrollview
572 * that can handle the request.
575 sv = this._getAncestorByDirection( dir );
577 this._propagateDragMove( sv, e, ex, ey, dir );
582 this._directionLock = svdir || (dir || "none");
587 dirLock = this._directionLock;
589 if ( dirLock !== "y" && this._hTracker ) {
594 this._doSnapBackX = false;
596 scope = ( newX > 0 || newX < this._maxX );
598 if ( scope && dirLock === "x" ) {
599 sv = this._getAncestorByDirection("x");
601 this._setScrollPosition( newX > 0 ?
602 0 : this._maxX, newY );
603 this._propagateDragMove( sv, e, ex, ey, dir );
607 newX = x + ( dx / 2 );
608 this._doSnapBackX = true;
612 if ( dirLock !== "x" && this._vTracker ) {
617 this._doSnapBackY = false;
619 scope = ( newY > 0 || newY < this._maxY );
621 if ( scope && dirLock === "y" ) {
622 sv = this._getAncestorByDirection("y");
624 this._setScrollPosition( newX,
625 newY > 0 ? 0 : this._maxY );
626 this._propagateDragMove( sv, e, ex, ey, dir );
630 newY = y + ( dy / 2 );
631 this._doSnapBackY = true;
635 if ( this.options.overshootEnable === false ) {
636 this._doSnapBackX = false;
637 this._doSnapBackY = false;
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,
666 this._startMScroll( sx, sy );
668 this._hideScrollBars();
671 this._disableTracking();
673 return !this._didDrag;
676 _enableTracking: function () {
677 this._dragging = true;
680 _disableTracking: function () {
681 this._dragging = false;
684 _showScrollBars: function ( interval ) {
685 var vclass = "ui-scrollbar-visible",
688 if ( !this.options.showScrollBars ) {
691 if ( this._scrollbar_showed ) {
695 if ( this._$vScrollBar ) {
696 this._$vScrollBar.addClass( vclass );
698 if ( this._$hScrollBar ) {
699 this._$hScrollBar.addClass( vclass );
702 this._scrollbar_showed = true;
705 setTimeout( function () {
706 self._hideScrollBars();
711 _hideScrollBars: function () {
712 var vclass = "ui-scrollbar-visible";
714 if ( !this.options.showScrollBars ) {
717 if ( !this._scrollbar_showed ) {
721 if ( this._$vScrollBar ) {
722 this._$vScrollBar.removeClass( vclass );
724 if ( this._$hScrollBar ) {
725 this._$hScrollBar.removeClass( vclass );
728 this._scrollbar_showed = false;
731 _add_event: function () {
736 if ( this.options.eventType === "mouse" ) {
737 this._dragEvt = "mousedown mousemove mouseup click mousewheel";
739 this._dragCB = function ( e ) {
742 return self._handleDragStart( e,
743 e.clientX, e.clientY );
746 return self._handleDragMove( e,
747 e.clientX, e.clientY );
750 return self._handleDragStop( e );
753 return !self._didDrag;
756 var old = self.getScrollPosition();
757 self.scrollTo( -old.x,
758 -(old.y - e.originalEvent.wheelDelta) );
763 this._dragEvt = "touchstart touchmove touchend click";
765 this._dragCB = function ( e ) {
770 t = e.originalEvent.targetTouches[0];
771 return self._handleDragStart( e,
775 t = e.originalEvent.targetTouches[0];
776 return self._handleDragMove( e,
780 return self._handleDragStop( e );
783 return !self._didDrag;
788 $v.bind( this._dragEvt, this._dragCB );
790 if ( $c.jqmData("scroll") !== "y" ) {
794 $c.bind( "updatelayout", function ( e ) {
797 view_h = self._getViewHeight();
799 if ( !$c.height() || !view_h ) {
800 self.scrollTo( 0, 0, 0 );
804 sy = $c.height() - view_h;
805 vh = view_h - self._view_height;
807 self._view_height = view_h;
809 if ( vh == 0 || vh > $c.height() / 2 ) {
813 if ( self._sy - sy <= -vh ) {
814 self.scrollTo( 0, self._sy,
815 self.options.snapbackDuration );
816 } else if ( self._sy - sy <= vh + self.options.moveThreshold ) {
817 self.scrollTo( 0, sy,
818 self.options.snapbackDuration );
822 $( window ).bind( "resize", function ( e ) {
824 view_h = self._getViewHeight();
826 if ( $(".ui-page-active").get(0) !== $c.closest(".ui-page").get(0) ) {
830 if ( !$c.height() || !view_h ) {
834 focused = $c.find(".ui-focus");
837 focused.trigger("resize.scrollview");
840 /* calibration - after triggered throttledresize */
841 setTimeout( function () {
842 if ( self._sy < $c.height() - self._getViewHeight() ) {
843 self.scrollTo( 0, self._sy,
844 self.options.snapbackDuration );
848 self._view_height = view_h;
851 $c.closest(".ui-page")
852 .one( "pageshow", function ( e ) {
853 self._view_offset = self._$view.offset().top - self._$clip.offset().top;
854 self._view_height = self._getViewHeight();
856 .bind( "pageshow", function ( e ) {
857 /* should be called after pagelayout */
858 setTimeout( function () {
859 self._set_scrollbar_size();
860 self._setScrollPosition( self._sx, self._sy );
861 self._showScrollBars( 2000 );
866 _add_scrollbar: function () {
867 var $c = this._$clip,
868 prefix = "<div class=\"ui-scrollbar ui-scrollbar-",
869 suffix = "\"><div class=\"ui-scrollbar-track\"><div class=\"ui-scrollbar-thumb\"></div></div></div>";
871 if ( !this.options.showScrollBars ) {
875 if ( this._vTracker ) {
876 $c.append( prefix + "y" + suffix );
877 this._$vScrollBar = $c.children(".ui-scrollbar-y");
879 if ( this._hTracker ) {
880 $c.append( prefix + "x" + suffix );
881 this._$hScrollBar = $c.children(".ui-scrollbar-x");
884 this._scrollbar_showed = false;
887 _add_scroll_jump: function () {
888 var $c = this._$clip,
893 if ( !this.options.scrollJump ) {
897 if ( this._vTracker ) {
898 top_btn = $( '<div class="ui-scroll-jump-top-bg ui-btn" data-theme="s">' +
899 '<div class="ui-scroll-jump-top"></div></div>' );
900 $c.append( top_btn );
902 top_btn.bind( "vclick", function () {
903 self.scrollTo( 0, 0, self.options.overshootDuration );
907 if ( this._hTracker ) {
908 left_btn = $( '<div class="ui-scroll-jump-left-bg ui-btn" data-theme="s">' +
909 '<div class="ui-scroll-jump-left"></div></div>' );
910 $c.append( left_btn );
912 left_btn.bind( "vclick", function () {
913 self.scrollTo( 0, 0, self.options.overshootDuration );
918 _set_scrollbar_size: function () {
919 var $c = this._$clip,
927 if ( !this.options.showScrollBars ) {
931 if ( this._hTracker ) {
934 this._maxX = cw - vw;
936 if ( this._maxX > 0 ) {
939 if ( this._$hScrollBar && vw ) {
940 thumb = this._$hScrollBar.find(".ui-scrollbar-thumb");
941 thumb.css( "width", (cw >= vw ? "0" :
942 (Math.floor(cw / vw * 100) || 1) + "%") );
946 if ( this._vTracker ) {
948 vh = this._getViewHeight();
949 this._maxY = ch - vh;
951 if ( this._maxY > 0 ) {
954 if ( this._$vScrollBar && vh ) {
955 thumb = this._$vScrollBar.find(".ui-scrollbar-thumb");
956 thumb.css( "height", (ch >= vh ? "0" :
957 (Math.floor(ch / vh * 100) || 1) + "%") );
963 $.extend( MomentumTracker.prototype, {
964 start: function ( pos, speed, duration, minPos, maxPos ) {
965 var tstate = ( pos < minPos || pos > maxPos ) ?
966 tstates.snapback : tstates.scrolling,
969 this.state = ( speed !== 0 ) ? tstate : tstates.done;
972 this.duration = ( this.state === tstates.snapback ) ?
973 this.options.snapbackDuration : duration;
974 this.minPos = minPos;
975 this.maxPos = maxPos;
977 this.fromPos = ( this.state === tstates.snapback ) ? this.pos : 0;
978 pos_temp = ( this.pos < this.minPos ) ? this.minPos : this.maxPos;
979 this.toPos = ( this.state === tstates.snapback ) ? pos_temp : 0;
981 this.startTime = getCurrentTime();
985 this.state = tstates.done;
993 update: function ( overshootEnable ) {
994 var state = this.state,
995 cur_time = getCurrentTime(),
996 duration = this.duration,
997 elapsed = cur_time - this.startTime,
1002 if ( state === tstates.done ) {
1006 elapsed = elapsed > duration ? duration : elapsed;
1008 if ( state === tstates.scrolling || state === tstates.overshot ) {
1010 ( 1 - $.easing[this.easing]( elapsed / duration,
1011 elapsed, 0, 1, duration ) );
1015 didOverShoot = ( state === tstates.scrolling ) &&
1016 ( x < this.minPos || x > this.maxPos );
1018 if ( didOverShoot ) {
1019 x = ( x < this.minPos ) ? this.minPos : this.maxPos;
1024 if ( state === tstates.overshot ) {
1025 if ( elapsed >= duration ) {
1026 this.state = tstates.snapback;
1027 this.fromPos = this.pos;
1028 this.toPos = ( x < this.minPos ) ?
1029 this.minPos : this.maxPos;
1030 this.duration = this.options.snapbackDuration;
1031 this.startTime = cur_time;
1034 } else if ( state === tstates.scrolling ) {
1035 if ( didOverShoot && overshootEnable ) {
1036 this.state = tstates.overshot;
1037 this.speed = dx / 2;
1038 this.duration = this.options.overshootDuration;
1039 this.startTime = cur_time;
1040 } else if ( elapsed >= duration ) {
1041 this.state = tstates.done;
1044 } else if ( state === tstates.snapback ) {
1045 if ( elapsed >= duration ) {
1046 this.pos = this.toPos;
1047 this.state = tstates.done;
1049 this.pos = this.fromPos + (( this.toPos - this.fromPos ) *
1050 $.easing[this.easing]( elapsed / duration,
1051 elapsed, 0, 1, duration ));
1059 return this.state === tstates.done;
1062 getPosition: function () {
1067 $( document ).bind( 'pagecreate create', function ( e ) {
1068 var $page = $( e.target ),
1069 content_scroll = $page.find(".ui-content").jqmData("scroll");
1071 /* content scroll */
1072 if ( $.support.scrollview === undefined ) {
1073 $.support.scrollview = true;
1076 if ( $.support.scrollview === true && content_scroll === undefined ) {
1077 content_scroll = "y";
1080 if ( content_scroll !== "y" ) {
1081 content_scroll = "none";
1084 $page.find(".ui-content").attr( "data-scroll", content_scroll );
1086 $page.find(":jqmData(scroll):not(.ui-scrollview-clip)").each( function () {
1087 if ( $( this ).hasClass("ui-scrolllistview") ) {
1088 $( this ).scrolllistview();
1090 var st = $( this ).jqmData("scroll"),
1091 dir = st && ( st.search(/^[xy]/) !== -1 ) ? st.charAt(0) : null,
1094 if ( st === "none" ) {
1099 direction: dir || undefined,
1100 scrollMethod: $( this ).jqmData("scroll-method") || undefined,
1101 scrollJump: $( this ).jqmData("scroll-jump") || undefined
1104 $( this ).scrollview( opts );
1109 $( document ).bind( 'pageshow', function ( e ) {
1110 var $page = $( e.target ),
1111 scroll = $page.find(".ui-content").jqmData("scroll");
1113 if ( scroll === "y" ) {
1114 resizePageContentHeight( e.target );
1118 }( jQuery, window, document ) );