3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
7 YUI.add('scrollview-base', function(Y) {
10 * The scrollview-base module provides a basic ScrollView Widget, without scrollbar indicators
12 * @module scrollview-base
15 var getClassName = Y.ClassNameManager.getClassName,
16 SCROLLVIEW = 'scrollview',
18 vertical: getClassName(SCROLLVIEW, 'vert'),
19 horizontal: getClassName(SCROLLVIEW, 'horiz')
21 EV_SCROLL_END = 'scrollEnd',
22 EV_SCROLL_FLICK = 'flick',
24 FLICK = EV_SCROLL_FLICK,
27 MOUSEWHEEL_ENABLED = true,
39 DISABLED = "disabled",
44 BOUNDING_BOX = "boundingBox",
45 CONTENT_BOX = "contentBox",
52 Transition = Y.Transition,
54 NATIVE_TRANSITIONS = Transition.useNative,
56 _constrain = function (val, min, max) {
57 return Math.min(Math.max(val, min), max);
61 * ScrollView provides a scrollable widget, supporting flick gestures, across both touch and mouse based devices.
64 * @param config {Object} Object literal with initial attribute values
68 function ScrollView() {
69 ScrollView.superclass.constructor.apply(this, arguments);
72 Y.ScrollView = Y.extend(ScrollView, Y.Widget, {
74 // Y.ScrollView prototype
77 * Designated initializer
81 initializer: function() {
83 * Notification event fired at the end of a scroll transition
86 * @param e {EventFacade} The default event facade.
90 * Notification event fired at the end of a flick gesture (the flick animation may still be in progress)
93 * @param e {EventFacade} The default event facade.
97 // Cache - they're write once, and not going to change
98 sv._cb = sv.get(CONTENT_BOX);
99 sv._bb = sv.get(BOUNDING_BOX);
103 * Override the contentBox sizing method, since the contentBox height
104 * should not be that of the boundingBox.
109 _uiSizeCB: function() {},
112 * Content box transition callback
114 * @method _onTransEnd
115 * @param {Event.Facade} e The event facade
118 _onTransEnd: function(e) {
119 this.fire(EV_SCROLL_END);
123 * bindUI implementation
125 * Hooks up events for the widget
131 sv._bindDrag(sv.get(DRAG));
132 sv._bindFlick(sv.get(FLICK));
133 // Note: You can find _bindMousewheel() inside syncUI(), becuase it depends on UI details
137 // IE SELECT HACK. See if we can do this non-natively and in the gesture for a future release.
139 sv._fixIESelect(sv._bb, sv._cb);
147 _bindAttrs : function() {
150 scrollChangeHandler = sv._afterScrollChange,
151 dimChangeHandler = sv._afterDimChange;
154 'disabledChange': sv._afterDisabledChange,
155 'flickChange' : sv._afterFlickChange,
156 'dragChange' : sv._afterDragChange,
157 'scrollYChange' : scrollChangeHandler,
158 'scrollXChange' : scrollChangeHandler,
159 'heightChange' : dimChangeHandler,
160 'widthChange' : dimChangeHandler
163 // Helps avoid potential CSS race where in the styles from
164 // scrollview-list-skin.css are applied after syncUI() fires.
165 // Without a _uiDimensionChange() call, the scrollview only
166 // scrolls partially due to the fact that styles added in the CSS
167 // altered the height/width of the bounding box.
170 this.after('renderedChange', function(e) {
171 //this._uiDimensionsChange();
177 * Bind (or unbind) gesture move listeners required for drag support
180 * @param drag {boolean} If true, the method binds listener to enable drag (gesturemovestart). If false, the method unbinds gesturemove listeners for drag support.
183 _bindDrag : function(drag) {
187 bb.on('drag|gesturemovestart', Y.bind(this._onGestureMoveStart, this));
194 * Bind (or unbind) flick listeners.
197 * @param flick {Object|boolean} If truthy, the method binds listeners for flick support. If false, the method unbinds flick listeners.
200 _bindFlick : function(flick) {
204 cb.on("flick|flick", Y.bind(this._flick, this), flick);
206 cb.detach('flick|*');
211 * Bind (or unbind) mousewheel listeners.
213 * @method _bindMousewheel
214 * @param mousewheel {Object|boolean} If truthy, the method binds listeners for mousewheel support. If false, the method unbinds mousewheel listeners.
217 _bindMousewheel : function(mousewheel) {
220 // Only enable for vertical scrollviews
221 if (this._scrollsVertical) {
223 cb.on("mousewheel", Y.bind(this._mousewheel, this), mousewheel);
225 cb.detach('mousewheel|*');
231 * syncUI implementation.
233 * Update the scroll position, based on the current value of scrollX/scrollY.
238 this._cDisabled = this.get(DISABLED);
239 this._uiDimensionsChange();
240 this._bindMousewheel(MOUSEWHEEL_ENABLED);
241 this.scrollTo(this.get(SCROLL_X), this.get(SCROLL_Y));
245 * Scroll the element to a given xy coordinate
248 * @param x {Number} The x-position to scroll to
249 * @param y {Number} The y-position to scroll to
250 * @param duration {Number} Duration, in ms, of the scroll animation (default is 0)
251 * @param easing {String} An easing equation if duration is set
253 scrollTo: function(x, y, duration, easing) {
255 // TODO: Figure out a better way to detect mousewheel events
256 if (easing === undefined) {
257 if ( y < this._minScrollY) {
258 y = this._minScrollY;
260 else if ( y > this._maxScrollY) {
261 y = this._maxScrollY;
265 if (!this._cDisabled) {
269 xMove = (xSet) ? x * -1 : 0,
270 yMove = (ySet) ? y * -1 : 0,
272 TRANS = ScrollView._TRANSITION,
273 callback = this._transEndCB;
275 duration = duration || 0;
276 easing = easing || ScrollView.EASING;
279 this.set(SCROLL_X, x, { src: UI });
283 this.set(SCROLL_Y, y, { src: UI });
286 if (NATIVE_TRANSITIONS) {
287 // ANDROID WORKAROUND - try and stop existing transition, before kicking off new one.
288 cb.setStyle(TRANS.DURATION, ZERO).setStyle(TRANS.PROPERTY, EMPTY);
291 if (duration !== 0) {
295 duration : duration/1000
298 if (NATIVE_TRANSITIONS) {
299 transition.transform = this._transform(xMove, yMove);
301 if (xSet) { transition.left = xMove + PX; }
302 if (ySet) { transition.top = yMove + PX; }
305 Y.log("Transition: duration, easing:" + [transition.duration, transition.easing], "scrollview");
308 callback = this._transEndCB = Y.bind(this._onTransEnd, this);
311 cb.transition(transition, callback);
314 if (NATIVE_TRANSITIONS) {
315 cb.setStyle('transform', this._transform(xMove, yMove));
317 if (xSet) { cb.setStyle(LEFT, xMove + PX); }
318 if (ySet) { cb.setStyle(TOP, yMove + PX); }
325 * Utility method, to create the translate transform string with the
326 * x, y translation amounts provided.
329 * @param {Number} x Number of pixels to translate along the x axis
330 * @param {Number} y Number of pixels to translate along the y axis
333 _transform : function(x, y) {
334 // TODO: Would we be better off using a Matrix for this?
335 return (this._forceHWTransforms) ? 'translate('+ x +'px,'+ y +'px) translateZ(0px)' : 'translate('+ x +'px,'+ y +'px)';
339 * Utility method, to move the given element to the given xy position
342 * @param node {Node} The node to move
343 * @param x {Number} The x-position to move to
344 * @param y {Number} The y-position to move to
347 _moveTo : function(node, x, y) {
348 if (NATIVE_TRANSITIONS) {
349 node.setStyle('transform', this._transform(x, y));
351 node.setStyle(LEFT, x + PX);
352 node.setStyle(TOP, y + PX);
357 * Flag driving whether or not we should try and force H/W acceleration when transforming. Currently enabled by default for Webkit.
358 * Used by the _transform method.
360 * @property _forceHWTransforms
364 _forceHWTransforms: Y.UA.webkit ? true : false,
367 * <p>Used to control whether or not ScrollView's internal
368 * gesturemovestart, gesturemove and gesturemoveend
369 * event listeners should preventDefault. The value is an
370 * object, with "start", "move" and "end" properties used to
371 * specify which events should preventDefault and which shouldn't:</p>
381 * <p>The default values are set up in order to prevent panning,
382 * on touch devices, while allowing click listeners on elements inside
383 * the ScrollView to be notified as expected.</p>
396 * gesturemovestart event handler
398 * @method _onGestureMoveStart
399 * @param e {Event.Facade} The gesturemovestart event facade
402 _onGestureMoveStart: function(e) {
407 if (!sv._cDisabled) {
409 if (sv._prevent.start) {
415 sv._hm = bb.on('drag|gesturemove', Y.bind(sv._onGestureMove, sv));
416 sv._hme = bb.on('drag|gesturemoveend', Y.bind(sv._onGestureMoveEnd, sv));
418 sv._startY = e.clientY + sv.get(SCROLL_Y);
419 sv._startX = e.clientX + sv.get(SCROLL_X);
420 sv._startClientY = sv._endClientY = e.clientY;
421 sv._startClientX = sv._endClientX = e.clientX;
424 * Internal state, defines whether or not the scrollview is currently being dragged
426 * @property _isDragging
430 sv._isDragging = false;
433 * Internal state, defines whether or not the scrollview is currently animating a flick
435 * @property _flicking
439 sv._flicking = false;
442 * Internal state, defines whether or not the scrollview needs to snap to a boundary edge
444 * @property _snapToEdge
448 sv._snapToEdge = false;
453 * gesturemove event handler
455 * @method _onGestureMove
456 * @param e {Event.Facade} The gesturemove event facade
459 _onGestureMove: function(e) {
463 if (sv._prevent.move) {
467 sv._isDragging = true;
468 sv._endClientY = e.clientY;
469 sv._endClientX = e.clientX;
471 if (sv._scrollsVertical) {
472 sv.set(SCROLL_Y, -(e.clientY - sv._startY));
475 if(sv._scrollsHorizontal) {
476 sv.set(SCROLL_X, -(e.clientX - sv._startX));
481 * gestureend event handler
483 * @method _onGestureMoveEnd
484 * @param e {Event.Facade} The gesturemoveend event facade
487 _onGestureMoveEnd: function(e) {
489 if (this._prevent.end) {
493 var sv = this, // kweight
494 minY = sv._minScrollY,
495 maxY = sv._maxScrollY,
496 minX = sv._minScrollX,
497 maxX = sv._maxScrollX,
498 vert = sv._scrollsVertical,
499 horiz = sv._scrollsHorizontal,
500 startPoint = vert ? sv._startClientY : sv._startClientX,
501 endPoint = vert ? sv._endClientY : sv._endClientX,
502 distance = startPoint - endPoint,
503 absDistance = Math.abs(distance),
511 * Internal state, defines whether or not the scrollview has been scrolled half it's width/height
513 * @property _scrolledHalfway
517 sv._scrolledHalfway = sv._snapToEdge = sv._isDragging = false;
520 * Contains the distance (postive or negative) in pixels by which the scrollview was last scrolled. This is useful when
521 * setting up click listeners on the scrollview content, which on mouse based devices are always fired, even after a
524 * <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,
525 * if working in a purely touch based environment</p>
527 * @property lastScrolledAmt
531 sv.lastScrolledAmt = distance;
534 if((horiz && absDistance > bb.get('offsetWidth')/2) || (vert && absDistance > bb.get('offsetHeight')/2)) {
535 sv._scrolledHalfway = true;
538 * Internal state, defines whether or not the scrollview has been scrolled in the forward (distance > 0), or backward (distance < 0) direction
540 * @property _scrolledForward
544 sv._scrolledForward = distance > 0;
549 yOrig = sv.get(SCROLL_Y);
550 y = _constrain(yOrig, minY, maxY);
554 xOrig = sv.get(SCROLL_X);
555 x = _constrain(xOrig, minX, maxX);
558 if (x !== xOrig || y !== yOrig) {
559 this._snapToEdge = true;
568 Y.log("half:" + sv._scrolledHalfway + ", fwd:" + sv._scrolledForward, "scrollview");
574 sv.fire(EV_SCROLL_END, {
575 onGestureMoveEnd: true
582 * After listener for changes to the scrollX or scrollY attribute
584 * @method _afterScrollChange
585 * @param e {Event.Facade} The event facade
588 _afterScrollChange : function(e) {
589 var duration = e.duration,
593 if (e.attrName == SCROLL_X) {
594 this._uiScrollTo(val, null, duration, easing);
596 this._uiScrollTo(null, val, duration, easing);
602 * After listener for changes to the flick attribute
604 * @method _afterFlickChange
605 * @param e {Event.Facade} The event facade
608 _afterFlickChange : function(e) {
609 this._bindFlick(e.newVal);
613 * After listener for changes to the disabled attribute
615 * @method _afterDisabledChange
616 * @param e {Event.Facade} The event facade
619 _afterDisabledChange : function(e) {
620 // Cache for performance - we check during move
621 this._cDisabled = e.newVal;
625 * After listener for changes to the drag attribute
627 * @method _afterDragChange
628 * @param e {Event.Facade} The event facade
631 _afterDragChange : function(e) {
632 this._bindDrag(e.newVal);
636 * Used to move the ScrollView content
638 * @method _uiScrollTo
641 * @param duration {Number}
642 * @param easing {String}
646 _uiScrollTo : function(x, y, duration, easing) {
647 // TODO: This doesn't seem right. This is not UI logic.
648 duration = duration || this._snapToEdge ? 400 : 0;
649 easing = easing || this._snapToEdge ? ScrollView.SNAP_EASING : null;
651 this.scrollTo(x, y, duration, easing);
655 * After listener for the height or width attribute
657 * @method _afterDimChange
658 * @param e {Event.Facade} The event facade
661 _afterDimChange: function() {
662 this._uiDimensionsChange();
666 * Utility method to obtain scrollWidth, scrollHeight,
667 * accounting for the impact of translate on scrollWidth, scrollHeight
668 * @method _getScrollDims
669 * @returns {Array} The offsetWidth, offsetHeight, scrollWidth and scrollHeight as an array: [offsetWidth, offsetHeight, scrollWidth, scrollHeight]
672 _getScrollDims: function() {
675 // Ideally using CSSMatrix - don't think we have it normalized yet though.
676 // origX = (new WebKitCSSMatrix(cb.getComputedStyle("transform"))).e;
677 // origY = (new WebKitCSSMatrix(cb.getComputedStyle("transform"))).f;
679 origX = this.get(SCROLL_X),
680 origY = this.get(SCROLL_Y),
682 cb = this.get(CONTENT_BOX),
683 bb = this.get(BOUNDING_BOX),
687 TRANS = ScrollView._TRANSITION;
689 // TODO: Is this OK? Just in case it's called 'during' a transition.
690 if (NATIVE_TRANSITIONS) {
691 cb.setStyle(TRANS.DURATION, ZERO);
692 cb.setStyle(TRANS.PROPERTY, EMPTY);
695 HWTransform = this._forceHWTransforms;
696 this._forceHWTransforms = false; // the z translation was causing issues with picking up accurate scrollWidths in Chrome/Mac.
698 this._moveTo(cb, 0, 0);
699 dims = [bb.get("offsetWidth"), bb.get("offsetHeight"), bb.get('scrollWidth'), bb.get('scrollHeight')];
700 this._moveTo(cb, -1*origX, -1*origY);
702 this._forceHWTransforms = HWTransform;
708 * This method gets invoked whenever the height or width attributes change,
709 * allowing us to determine which scrolling axes need to be enabled.
711 * @method _uiDimensionsChange
714 _uiDimensionsChange: function() {
717 CLASS_NAMES = ScrollView.CLASS_NAMES,
719 scrollDims = this._getScrollDims(),
721 width = scrollDims[0],
722 height = scrollDims[1],
723 scrollWidth = scrollDims[2],
724 scrollHeight = scrollDims[3];
726 if (height && scrollHeight > height) {
727 sv._scrollsVertical = true;
728 sv._maxScrollY = scrollHeight - height;
730 sv._scrollHeight = scrollHeight;
732 bb.addClass(CLASS_NAMES.vertical);
734 sv._scrollsVertical = false;
735 delete sv._maxScrollY;
736 delete sv._minScrollY;
737 delete sv._scrollHeight;
739 bb.removeClass(CLASS_NAMES.vertical);
742 if (width && scrollWidth > width) {
743 sv._scrollsHorizontal = true;
744 sv._maxScrollX = scrollWidth - width;
746 sv._scrollWidth = scrollWidth;
748 bb.addClass(CLASS_NAMES.horizontal);
750 sv._scrollsHorizontal = false;
751 delete sv._maxScrollX;
752 delete sv._minScrollX;
753 delete sv._scrollWidth;
755 bb.removeClass(CLASS_NAMES.horizontal);
759 * Internal state, defines whether or not the scrollview can scroll vertically
761 * @property _scrollsVertical
767 * Internal state, defines the maximum amount that the scrollview can be scrolled along the Y axis
769 * @property _maxScrollY
775 * Internal state, defines the minimum amount that the scrollview can be scrolled along the Y axis
777 * @property _minScrollY
783 * Internal state, cached scrollHeight, for performance
785 * @property _scrollHeight
791 * Internal state, defines whether or not the scrollview can scroll horizontally
793 * @property _scrollsHorizontal
799 * Internal state, defines the maximum amount that the scrollview can be scrolled along the X axis
801 * @property _maxScrollX
807 * Internal state, defines the minimum amount that the scrollview can be scrolled along the X axis
809 * @property _minScrollX
815 * Internal state, cached scrollWidth, for performance
817 * @property _scrollWidth
824 * Execute a flick at the end of a scroll action
827 * @param distance {Number} The distance (in px) the user scrolled before the flick
828 * @param time {Number} The number of ms the scroll event lasted before the flick
831 _flick: function(e) {
836 if (!sv._cDisabled) {
839 * Internal state, currently calculated velocity from the flick
841 * @property _currentVelocity
845 sv._currentVelocity = flick.velocity;
848 sv._cDecel = sv.get('deceleration');
849 sv._cBounce = sv.get('bounce');
851 sv._pastYEdge = false;
852 sv._pastXEdge = false;
856 sv.fire(EV_SCROLL_FLICK);
860 _mousewheel: function(e) {
861 var scrollY = this.get('scrollY'),
862 contentBox = this._cb,
863 scrollOffset = 10, // 10px
864 scrollToY = scrollY - (e.wheelDelta * scrollOffset);
866 this.scrollTo(0, scrollToY);
868 // if we have scrollbars plugin, update & set the flash timer on the scrollbar
869 if (this.scrollbars) {
870 // TODO: The scrollbars should handle this themselves
871 this.scrollbars._update();
872 this.scrollbars.flash();
874 // this.scrollbars._hostDimensionsChange();
877 // prevent browser default behavior on mouse scroll
882 * Execute a single frame in the flick animation
884 * @method _flickFrame
887 _flickFrame: function() {
895 scrollsVertical = sv._scrollsVertical,
896 scrollsHorizontal = sv._scrollsHorizontal,
897 deceleration = sv._cDecel,
898 bounce = sv._cBounce,
899 vel = sv._currentVelocity,
900 step = ScrollView.FRAME_STEP;
902 if (scrollsVertical) {
903 maxY = sv._maxScrollY;
904 minY = sv._minScrollY;
905 newY = sv.get(SCROLL_Y) - (vel * step);
908 if (scrollsHorizontal) {
909 maxX = sv._maxScrollX;
910 minX = sv._minScrollX;
911 newX = sv.get(SCROLL_X) - (vel * step);
914 vel = sv._currentVelocity = (vel * deceleration);
916 if(Math.abs(vel).toFixed(4) <= 0.015) {
917 sv._flicking = false;
918 sv._killTimer(!(sv._pastYEdge || sv._pastXEdge));
920 if(scrollsVertical) {
922 sv._snapToEdge = true;
923 sv.set(SCROLL_Y, minY);
924 } else if(newY > maxY) {
925 sv._snapToEdge = true;
926 sv.set(SCROLL_Y, maxY);
930 if(scrollsHorizontal) {
932 sv._snapToEdge = true;
933 sv.set(SCROLL_X, minX);
934 } else if(newX > maxX) {
935 sv._snapToEdge = true;
936 sv.set(SCROLL_X, maxX);
943 if (scrollsVertical) {
944 if (newY < minY || newY > maxY) {
945 sv._pastYEdge = true;
946 sv._currentVelocity *= bounce;
949 sv.set(SCROLL_Y, newY);
952 if (scrollsHorizontal) {
953 if (newX < minX || newX > maxX) {
954 sv._pastXEdge = true;
955 sv._currentVelocity *= bounce;
958 sv.set(SCROLL_X, newX);
961 if (!sv._flickTimer) {
962 sv._flickTimer = Y.later(step, sv, '_flickFrame', null, true);
967 * Stop the animation timer
970 * @param fireEvent {Boolean} If true, fire the scrollEnd event
973 _killTimer: function(fireEvent) {
976 sv._flickTimer.cancel();
977 sv._flickTimer = null;
981 sv.fire(EV_SCROLL_END);
986 * The scrollX, scrollY setter implementation
990 * @param {Number} val
991 * @param {String} dim
993 * @return {Number} The constrained value, if it exceeds min/max range
995 _setScroll : function(val, dim) {
996 if (this._cDisabled) {
997 val = Y.Attribute.INVALID_VALUE;
1000 var bouncing = this._cachedBounce || this.get(BOUNCE),
1001 range = ScrollView.BOUNCE_RANGE,
1003 maxScroll = (dim == DIM_X) ? this._maxScrollX : this._maxScrollY,
1005 min = bouncing ? -range : 0,
1006 max = bouncing ? maxScroll + range : maxScroll;
1008 if(!bouncing || !this._isDragging) {
1011 } else if(val > max) {
1021 * Setter for the scrollX attribute
1023 * @method _setScrollX
1024 * @param val {Number} The new scrollX value
1025 * @return {Number} The normalized value
1028 _setScrollX: function(val) {
1029 return this._setScroll(val, DIM_X);
1033 * Setter for the scrollY ATTR
1035 * @method _setScrollY
1036 * @param val {Number} The new scrollY value
1037 * @return {Number} The normalized value
1040 _setScrollY: function(val) {
1041 return this._setScroll(val, DIM_Y);
1046 // Y.ScrollView static properties
1049 * The identity of the widget.
1053 * @default 'scrollview'
1061 * Static property used to define the default attribute configuration of
1072 * The scroll position in the y-axis
1074 * @attribute scrollY
1080 setter: '_setScrollY'
1084 * The scroll position in the x-axis
1086 * @attribute scrollX
1092 setter: '_setScrollX'
1096 * Drag coefficent for inertial scrolling. The closer to 1 this
1097 * value is, the less friction during scrolling.
1099 * @attribute deceleration
1107 * Drag coefficient for intertial scrolling at the upper
1108 * and lower boundaries of the scrollview. Set to 0 to
1109 * disable "rubber-banding".
1120 * The minimum distance and/or velocity which define a flick. Can be set to false,
1121 * to disable flick support (note: drag support is enabled/disabled separately)
1125 * @default Object with properties minDistance = 10, minVelocity = 0.3.
1135 * Enable/Disable dragging the ScrollView content (note: flick support is enabled/disabled separately)
1146 * List of class names used in the scrollview's DOM
1148 * @property CLASS_NAMES
1152 CLASS_NAMES: CLASS_NAMES,
1155 * Flag used to source property changes initiated from the DOM
1165 * The default bounce distance in pixels
1167 * @property BOUNCE_RANGE
1175 * The interval used when animating the flick
1177 * @property FRAME_STEP
1185 * The default easing used when animating the flick
1190 * @default 'cubic-bezier(0, 0.1, 0, 1.0)'
1192 EASING : 'cubic-bezier(0, 0.1, 0, 1.0)',
1195 * The default easing to use when animating the bounce snap back.
1197 * @property SNAP_EASING
1200 * @default 'ease-out'
1202 SNAP_EASING : 'ease-out',
1205 * Object map of style property names used to set transition properties.
1206 * Defaults to the vendor prefix established by the Transition module.
1207 * The configured property names are `_TRANSITION.DURATION` (e.g. "WebkitTransitionDuration") and
1208 * `_TRANSITION.PROPERTY (e.g. "WebkitTransitionProperty").
1210 * @property _TRANSITION
1214 DURATION : Transition._VENDOR_PREFIX + "TransitionDuration",
1215 PROPERTY : Transition._VENDOR_PREFIX + "TransitionProperty"
1220 }, '3.5.0' ,{requires:['widget', 'event-gestures', 'event-mousewheel', 'transition'], skinnable:true});