2 Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
8 YUI.add('scrollview-base', function(Y) {
11 * The scrollview-base module provides a basic ScrollView Widget, without scrollbar indicators
13 * @module scrollview-base
16 var getClassName = Y.ClassNameManager.getClassName,
17 SCROLLVIEW = 'scrollview',
19 vertical: getClassName(SCROLLVIEW, 'vert'),
20 horizontal: getClassName(SCROLLVIEW, 'horiz')
22 EV_SCROLL_END = 'scrollEnd',
23 EV_SCROLL_FLICK = 'flick',
25 FLICK = EV_SCROLL_FLICK,
41 BOUNDING_BOX = "boundingBox",
42 CONTENT_BOX = "contentBox",
49 NATIVE_TRANSITIONS = Y.Transition.useNative,
51 _constrain = function (val, min, max) {
52 return Math.min(Math.max(val, min), max);
55 Y.Node.DOM_EVENTS.DOMSubtreeModified = true;
58 * ScrollView provides a scrollable widget, supporting flick gestures, across both touch and mouse based devices.
62 * @param config {Object} Object literal with initial attribute values
66 function ScrollView() {
67 ScrollView.superclass.constructor.apply(this, arguments);
70 Y.ScrollView = Y.extend(ScrollView, Y.Widget, {
72 // Y.ScrollView prototype
75 * Designated initializer
79 initializer: function() {
81 * Notification event fired at the end of a scroll transition
84 * @param e {EventFacade} The default event facade.
88 * Notification event fired at the end of a flick gesture (the flick animation may still be in progress)
91 * @param e {EventFacade} The default event facade.
95 // Cache - they're write once, and not going to change
96 sv._cb = sv.get(CONTENT_BOX);
97 sv._bb = sv.get(BOUNDING_BOX);
101 * Override the contentBox sizing method, since the contentBox height
102 * should not be that of the boundingBox.
107 _uiSizeCB: function() {},
110 * Content box transition callback
112 * @method _onTransEnd
113 * @param {Event.Facade} e The event facade
116 _onTransEnd: function(e) {
117 this.fire(EV_SCROLL_END);
121 * bindUI implementation
123 * Hooks up events for the widget
128 var sv = this, // kweight
131 scrollChangeHandler = sv._afterScrollChange,
132 dimChangeHandler = sv._afterDimChange,
133 flick = sv.get(FLICK);
135 bb.on('gesturemovestart', Y.bind(sv._onGestureMoveStart, sv));
137 // IE SELECT HACK. See if we can do this non-natively and in the gesture for a future release.
139 sv._fixIESelect(bb, cb);
142 // TODO: Fires way to often when using non-native transitions, due to property change
143 if (NATIVE_TRANSITIONS) {
144 cb.on('DOMSubtreeModified', Y.bind(sv._uiDimensionsChange, sv));
148 cb.on("flick", Y.bind(sv._flick, sv), flick);
152 'scrollYChange' : scrollChangeHandler,
153 'scrollXChange' : scrollChangeHandler,
154 'heightChange' : dimChangeHandler,
155 'widthChange' : dimChangeHandler
160 * syncUI implementation
162 * Update the scroll position, based on the current value of scrollY
166 this._uiDimensionsChange();
167 this.scrollTo(this.get(SCROLL_X), this.get(SCROLL_Y));
171 * Scroll the element to a given y coordinate
174 * @param x {Number} The x-position to scroll to
175 * @param y {Number} The y-position to scroll to
176 * @param duration {Number} Duration, in ms, of the scroll animation (default is 0)
177 * @param easing {String} An easing equation if duration is set
179 scrollTo: function(x, y, duration, easing) {
183 xMove = (xSet) ? x * -1 : 0,
184 yMove = (ySet) ? y * -1 : 0,
186 TRANS = ScrollView._TRANSITION,
187 callback = this._transEndCB;
189 duration = duration || 0;
190 easing = easing || ScrollView.EASING;
193 this.set(SCROLL_X, x, { src: UI });
197 this.set(SCROLL_Y, y, { src: UI });
200 if (NATIVE_TRANSITIONS) {
201 // ANDROID WORKAROUND - try and stop existing transition, before kicking off new one.
202 cb.setStyle(TRANS.DURATION, ZERO).setStyle(TRANS.PROPERTY, EMPTY);
205 if (duration !== 0) {
209 duration : duration/1000
212 if (NATIVE_TRANSITIONS) {
213 transition.transform = 'translate3D('+ xMove +'px,'+ yMove +'px, 0px)';
215 if (xSet) { transition.left = xMove + PX; }
216 if (ySet) { transition.top = yMove + PX; }
221 callback = this._transEndCB = Y.bind(this._onTransEnd, this);
224 cb.transition(transition, callback);
227 if (NATIVE_TRANSITIONS) {
228 cb.setStyle('transform', 'translate3D('+ xMove +'px,'+ yMove +'px, 0px)');
230 if (xSet) { cb.setStyle(LEFT, xMove + PX); }
231 if (ySet) { cb.setStyle(TOP, yMove + PX); }
237 * <p>Used to control whether or not ScrollView's internal
238 * gesturemovestart, gesturemove and gesturemoveend
239 * event listeners should preventDefault. The value is an
240 * object, with "start", "move" and "end" properties used to
241 * specify which events should preventDefault and which shouldn't:</p>
251 * <p>The default values are set up in order to prevent panning,
252 * on touch devices, while allowing click listeners on elements inside
253 * the ScrollView to be notified as expected.</p>
266 * gesturemovestart event handler
268 * @method _onGestureMoveStart
269 * @param e {Event.Facade} The gesturemovestart event facade
272 _onGestureMoveStart: function(e) {
277 if (sv._prevent.start) {
283 sv._hm = bb.on('gesturemove', Y.bind(sv._onGestureMove, sv));
284 sv._hme = bb.on('gesturemoveend', Y.bind(sv._onGestureMoveEnd, sv));
286 sv._startY = e.clientY + sv.get(SCROLL_Y);
287 sv._startX = e.clientX + sv.get(SCROLL_X);
288 sv._startClientY = sv._endClientY = e.clientY;
289 sv._startClientX = sv._endClientX = e.clientX;
292 * Internal state, defines whether or not the scrollview is currently being dragged
294 * @property _isDragging
298 sv._isDragging = false;
301 * Internal state, defines whether or not the scrollview is currently animating a flick
303 * @property _flicking
307 sv._flicking = false;
310 * Internal state, defines whether or not the scrollview needs to snap to a boundary edge
312 * @property _snapToEdge
316 sv._snapToEdge = false;
320 * gesturemove event handler
322 * @method _onGestureMove
323 * @param e {Event.Facade} The gesturemove event facade
326 _onGestureMove: function(e) {
330 if (sv._prevent.move) {
334 sv._isDragging = true;
335 sv._endClientY = e.clientY;
336 sv._endClientX = e.clientX;
338 if (sv._scrollsVertical) {
339 sv.set(SCROLL_Y, -(e.clientY - sv._startY));
342 if(sv._scrollsHorizontal) {
343 sv.set(SCROLL_X, -(e.clientX - sv._startX));
348 * gestureend event handler
350 * @method _onGestureMoveEnd
351 * @param e {Event.Facade} The gesturemoveend event facade
354 _onGestureMoveEnd: function(e) {
356 if (this._prevent.end) {
360 var sv = this, // kweight
361 minY = sv._minScrollY,
362 maxY = sv._maxScrollY,
363 minX = sv._minScrollX,
364 maxX = sv._maxScrollX,
365 vert = sv._scrollsVertical,
366 horiz = sv._scrollsHorizontal,
367 startPoint = vert ? sv._startClientY : sv._startClientX,
368 endPoint = vert ? sv._endClientY : sv._endClientX,
369 distance = startPoint - endPoint,
370 absDistance = Math.abs(distance),
378 * Internal state, defines whether or not the scrollview has been scrolled half it's width/height
380 * @property _scrolledHalfway
384 sv._scrolledHalfway = sv._snapToEdge = sv._isDragging = false;
387 * Contains the distance (postive or negative) in pixels by which the scrollview was last scrolled. This is useful when
388 * setting up click listeners on the scrollview content, which on mouse based devices are always fired, even after a
391 * <p>Touch based devices don't currently fire a click event, if the finger has been moved (beyond a threshold) so this check isn't required,
392 * if working in a purely touch based environment</p>
394 * @property lastScrolledAmt
398 sv.lastScrolledAmt = distance;
401 if((horiz && absDistance > bb.get('offsetWidth')/2) || (vert && absDistance > bb.get('offsetHeight')/2)) {
402 sv._scrolledHalfway = true;
405 * Internal state, defines whether or not the scrollview has been scrolled in the forward (distance > 0), or backward (distance < 0) direction
407 * @property _scrolledForward
411 sv._scrolledForward = distance > 0;
416 yOrig = sv.get(SCROLL_Y);
417 y = _constrain(yOrig, minY, maxY);
421 xOrig = sv.get(SCROLL_X);
422 x = _constrain(xOrig, minX, maxX);
425 if (x !== xOrig || y !== yOrig) {
426 this._snapToEdge = true;
440 sv.fire(EV_SCROLL_END, {
441 onGestureMoveEnd: true
448 * After listener for changes to the scrollX or scrollY attribute
450 * @method _afterScrollChange
451 * @param e {Event.Facade} The event facade
454 _afterScrollChange : function(e) {
455 var duration = e.duration,
460 if (e.attrName == SCROLL_X) {
461 this._uiScrollTo(val, null, duration, easing);
463 this._uiScrollTo(null, val, duration, easing);
469 * Used to move the ScrollView content
471 * @method _uiScrollTo
474 * @param duration {Number}
475 * @param easing {String}
479 _uiScrollTo : function(x, y, duration, easing) {
481 // TODO: This doesn't seem right. This is not UI logic.
482 duration = duration || this._snapToEdge ? 400 : 0;
483 easing = easing || this._snapToEdge ? ScrollView.SNAP_EASING : null;
485 this.scrollTo(x, y, duration, easing);
489 * After listener for the height or width attribute
491 * @method _afterDimChange
492 * @param e {Event.Facade} The event facade
495 _afterDimChange: function() {
496 this._uiDimensionsChange();
500 * This method gets invoked whenever the height or width attributes change,
501 * allowing us to determine which scrolling axes need to be enabled.
503 * @method _uiDimensionsChange
506 _uiDimensionsChange: function() {
511 CLASS_NAMES = ScrollView.CLASS_NAMES,
513 height = sv.get('height'),
514 width = sv.get('width'),
516 // Use bb instead of cb. cb doesn't gives us the right results
517 // in FF (due to overflow:hidden)
518 scrollHeight = bb.get('scrollHeight'),
519 scrollWidth = bb.get('scrollWidth');
521 if (height && scrollHeight > height) {
522 sv._scrollsVertical = true;
523 sv._maxScrollY = scrollHeight - height;
525 sv._scrollHeight = scrollHeight;
526 bb.addClass(CLASS_NAMES.vertical);
529 if (width && scrollWidth > width) {
530 sv._scrollsHorizontal = true;
531 sv._maxScrollX = scrollWidth - width;
533 sv._scrollWidth = scrollWidth;
534 bb.addClass(CLASS_NAMES.horizontal);
538 * Internal state, defines whether or not the scrollview can scroll vertically
540 * @property _scrollsVertical
546 * Internal state, defines the maximum amount that the scrollview can be scrolled along the Y axis
548 * @property _maxScrollY
554 * Internal state, defines the minimum amount that the scrollview can be scrolled along the Y axis
556 * @property _minScrollY
562 * Internal state, cached scrollHeight, for performance
564 * @property _scrollHeight
570 * Internal state, defines whether or not the scrollview can scroll horizontally
572 * @property _scrollsHorizontal
578 * Internal state, defines the maximum amount that the scrollview can be scrolled along the X axis
580 * @property _maxScrollX
586 * Internal state, defines the minimum amount that the scrollview can be scrolled along the X axis
588 * @property _minScrollX
594 * Internal state, cached scrollWidth, for performance
596 * @property _scrollWidth
603 * Execute a flick at the end of a scroll action
606 * @param distance {Number} The distance (in px) the user scrolled before the flick
607 * @param time {Number} The number of ms the scroll event lasted before the flick
610 _flick: function(e) {
615 * Internal state, currently calculated velocity from the flick
617 * @property _currentVelocity
621 sv._currentVelocity = flick.velocity;
624 sv._cDecel = sv.get('deceleration');
625 sv._cBounce = sv.get('bounce');
627 sv._pastYEdge = false;
628 sv._pastXEdge = false;
632 sv.fire(EV_SCROLL_FLICK);
636 * Execute a single frame in the flick animation
638 * @method _flickFrame
641 _flickFrame: function() {
649 scrollsVertical = sv._scrollsVertical,
650 scrollsHorizontal = sv._scrollsHorizontal,
651 deceleration = sv._cDecel,
652 bounce = sv._cBounce,
653 vel = sv._currentVelocity,
654 step = ScrollView.FRAME_STEP;
656 if (scrollsVertical) {
657 maxY = sv._maxScrollY;
658 minY = sv._minScrollY;
659 newY = sv.get(SCROLL_Y) - (vel * step);
662 if (scrollsHorizontal) {
663 maxX = sv._maxScrollX;
664 minX = sv._minScrollX;
665 newX = sv.get(SCROLL_X) - (vel * step);
668 vel = sv._currentVelocity = (vel * deceleration);
670 if(Math.abs(vel).toFixed(4) <= 0.015) {
671 sv._flicking = false;
672 sv._killTimer(!(sv._pastYEdge || sv._pastXEdge));
674 if(scrollsVertical) {
676 sv._snapToEdge = true;
677 sv.set(SCROLL_Y, minY);
678 } else if(newY > maxY) {
679 sv._snapToEdge = true;
680 sv.set(SCROLL_Y, maxY);
684 if(scrollsHorizontal) {
686 sv._snapToEdge = true;
687 sv.set(SCROLL_X, minX);
688 } else if(newX > maxX) {
689 sv._snapToEdge = true;
690 sv.set(SCROLL_X, maxX);
697 if (scrollsVertical) {
698 if (newY < minY || newY > maxY) {
699 sv._pastYEdge = true;
700 sv._currentVelocity *= bounce;
703 sv.set(SCROLL_Y, newY);
706 if (scrollsHorizontal) {
707 if (newX < minX || newX > maxX) {
708 sv._pastXEdge = true;
709 sv._currentVelocity *= bounce;
712 sv.set(SCROLL_X, newX);
715 if (!sv._flickTimer) {
716 sv._flickTimer = Y.later(step, sv, '_flickFrame', null, true);
721 * Stop the animation timer
724 * @param fireEvent {Boolean} If true, fire the scrollEnd event
727 _killTimer: function(fireEvent) {
730 sv._flickTimer.cancel();
731 sv._flickTimer = null;
735 sv.fire(EV_SCROLL_END);
740 * The scrollX, scrollY setter implementation
744 * @param {Number} val
745 * @param {String} dim
747 * @return {Number} The constrained value, if it exceeds min/max range
749 _setScroll : function(val, dim) {
750 var bouncing = this._cachedBounce || this.get(BOUNCE),
751 range = ScrollView.BOUNCE_RANGE,
753 maxScroll = (dim == DIM_X) ? this._maxScrollX : this._maxScrollY,
755 min = bouncing ? -range : 0,
756 max = bouncing ? maxScroll + range : maxScroll;
758 if(!bouncing || !this._isDragging) {
761 } else if(val > max) {
770 * Setter for the scrollX attribute
772 * @method _setScrollX
773 * @param val {Number} The new scrollX value
774 * @return {Number} The normalized value
777 _setScrollX: function(val) {
778 return this._setScroll(val, DIM_X);
782 * Setter for the scrollY ATTR
784 * @method _setScrollY
785 * @param val {Number} The new scrollY value
786 * @return {Number} The normalized value
789 _setScrollY: function(val) {
790 return this._setScroll(val, DIM_Y);
795 // Y.ScrollView static properties
798 * The identity of the widget.
800 * @property ScrollView.NAME
802 * @default 'scrollview'
810 * Static property used to define the default attribute configuration of
813 * @property ScrollView.ATTRS
821 * The scroll position in the y-axis
829 setter: '_setScrollY'
833 * The scroll position in the x-axis
841 setter: '_setScrollX'
845 * Drag coefficent for inertial scrolling. The closer to 1 this
846 * value is, the less friction during scrolling.
848 * @attribute deceleration
856 * Drag coefficient for intertial scrolling at the upper
857 * and lower boundaries of the scrollview. Set to 0 to
858 * disable "rubber-banding".
869 * The minimum distance and/or velocity which define a flick
873 * @default Object with properties minDistance = 10, minVelocity = 0.3.
884 * List of class names used in the scrollview's DOM
886 * @property ScrollView.CLASS_NAMES
890 CLASS_NAMES: CLASS_NAMES,
893 * Flag used to source property changes initiated from the DOM
895 * @property ScrollView.UI_SRC
903 * The default bounce distance in pixels
905 * @property ScrollView.BOUNCE_RANGE
913 * The interval used when animating the flick
915 * @property ScrollView.FRAME_STEP
923 * The default easing used when animating the flick
925 * @property ScrollView.EASING
928 * @default 'cubic-bezier(0, 0.1, 0, 1.0)'
930 EASING : 'cubic-bezier(0, 0.1, 0, 1.0)',
933 * The default easing to use when animatiing the bounce snap back.
935 * @property ScrollView.SNAP_EASING
938 * @default 'ease-out'
940 SNAP_EASING : 'ease-out',
943 * Style property name to use to set transition property. Currently, Webkit specific (WebkitTransitionProperty)
945 * @property ScrollView._TRANSITION.PROPERTY
949 DURATION : "WebkitTransitionDuration",
950 PROPERTY : "WebkitTransitionProperty"
955 }, '3.3.0' ,{skinnable:true, requires:['widget', 'event-gestures', 'transition']});