diff -r d334a616c023 -r e16a97fb364a src/cm/media/js/lib/yui/yui3-3.15.0/build/scrollview-base/scrollview-base-debug.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/cm/media/js/lib/yui/yui3-3.15.0/build/scrollview-base/scrollview-base-debug.js Mon Mar 10 15:19:48 2014 +0100 @@ -0,0 +1,1457 @@ +YUI.add('scrollview-base', function (Y, NAME) { + +/** + * The scrollview-base module provides a basic ScrollView Widget, without scrollbar indicators + * + * @module scrollview + * @submodule scrollview-base + */ + + // Local vars +var getClassName = Y.ClassNameManager.getClassName, + DOCUMENT = Y.config.doc, + IE = Y.UA.ie, + NATIVE_TRANSITIONS = Y.Transition.useNative, + vendorPrefix = Y.Transition._VENDOR_PREFIX, // Todo: This is a private property, and alternative approaches should be investigated + SCROLLVIEW = 'scrollview', + CLASS_NAMES = { + vertical: getClassName(SCROLLVIEW, 'vert'), + horizontal: getClassName(SCROLLVIEW, 'horiz') + }, + EV_SCROLL_END = 'scrollEnd', + FLICK = 'flick', + DRAG = 'drag', + MOUSEWHEEL = 'mousewheel', + UI = 'ui', + TOP = 'top', + LEFT = 'left', + PX = 'px', + AXIS = 'axis', + SCROLL_Y = 'scrollY', + SCROLL_X = 'scrollX', + BOUNCE = 'bounce', + DISABLED = 'disabled', + DECELERATION = 'deceleration', + DIM_X = 'x', + DIM_Y = 'y', + BOUNDING_BOX = 'boundingBox', + CONTENT_BOX = 'contentBox', + GESTURE_MOVE = 'gesturemove', + START = 'start', + END = 'end', + EMPTY = '', + ZERO = '0s', + SNAP_DURATION = 'snapDuration', + SNAP_EASING = 'snapEasing', + EASING = 'easing', + FRAME_DURATION = 'frameDuration', + BOUNCE_RANGE = 'bounceRange', + _constrain = function (val, min, max) { + return Math.min(Math.max(val, min), max); + }; + +/** + * ScrollView provides a scrollable widget, supporting flick gestures, + * across both touch and mouse based devices. + * + * @class ScrollView + * @param config {Object} Object literal with initial attribute values + * @extends Widget + * @constructor + */ +function ScrollView() { + ScrollView.superclass.constructor.apply(this, arguments); +} + +Y.ScrollView = Y.extend(ScrollView, Y.Widget, { + + // *** Y.ScrollView prototype + + /** + * Flag driving whether or not we should try and force H/W acceleration when transforming. Currently enabled by default for Webkit. + * Used by the _transform method. + * + * @property _forceHWTransforms + * @type boolean + * @protected + */ + _forceHWTransforms: Y.UA.webkit ? true : false, + + /** + *

Used to control whether or not ScrollView's internal + * gesturemovestart, gesturemove and gesturemoveend + * event listeners should preventDefault. The value is an + * object, with "start", "move" and "end" properties used to + * specify which events should preventDefault and which shouldn't:

+ * + *
+     * {
+     *    start: false,
+     *    move: true,
+     *    end: false
+     * }
+     * 
+ * + *

The default values are set up in order to prevent panning, + * on touch devices, while allowing click listeners on elements inside + * the ScrollView to be notified as expected.

+ * + * @property _prevent + * @type Object + * @protected + */ + _prevent: { + start: false, + move: true, + end: false + }, + + /** + * Contains the distance (postive or negative) in pixels by which + * the scrollview was last scrolled. This is useful when setting up + * click listeners on the scrollview content, which on mouse based + * devices are always fired, even after a drag/flick. + * + *

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, if working in a purely touch based environment

+ * + * @property lastScrolledAmt + * @type Number + * @public + * @default 0 + */ + lastScrolledAmt: 0, + + /** + * Internal state, defines the minimum amount that the scrollview can be scrolled along the X axis + * + * @property _minScrollX + * @type number + * @protected + */ + _minScrollX: null, + + /** + * Internal state, defines the maximum amount that the scrollview can be scrolled along the X axis + * + * @property _maxScrollX + * @type number + * @protected + */ + _maxScrollX: null, + + /** + * Internal state, defines the minimum amount that the scrollview can be scrolled along the Y axis + * + * @property _minScrollY + * @type number + * @protected + */ + _minScrollY: null, + + /** + * Internal state, defines the maximum amount that the scrollview can be scrolled along the Y axis + * + * @property _maxScrollY + * @type number + * @protected + */ + _maxScrollY: null, + + /** + * Designated initializer + * + * @method initializer + * @param {Object} Configuration object for the plugin + */ + initializer: function () { + var sv = this; + + // Cache these values, since they aren't going to change. + sv._bb = sv.get(BOUNDING_BOX); + sv._cb = sv.get(CONTENT_BOX); + + // Cache some attributes + sv._cAxis = sv.get(AXIS); + sv._cBounce = sv.get(BOUNCE); + sv._cBounceRange = sv.get(BOUNCE_RANGE); + sv._cDeceleration = sv.get(DECELERATION); + sv._cFrameDuration = sv.get(FRAME_DURATION); + }, + + /** + * bindUI implementation + * + * Hooks up events for the widget + * @method bindUI + */ + bindUI: function () { + var sv = this; + + // Bind interaction listers + sv._bindFlick(sv.get(FLICK)); + sv._bindDrag(sv.get(DRAG)); + sv._bindMousewheel(true); + + // Bind change events + sv._bindAttrs(); + + // IE SELECT HACK. See if we can do this non-natively and in the gesture for a future release. + if (IE) { + sv._fixIESelect(sv._bb, sv._cb); + } + + // Set any deprecated static properties + if (ScrollView.SNAP_DURATION) { + sv.set(SNAP_DURATION, ScrollView.SNAP_DURATION); + } + + if (ScrollView.SNAP_EASING) { + sv.set(SNAP_EASING, ScrollView.SNAP_EASING); + } + + if (ScrollView.EASING) { + sv.set(EASING, ScrollView.EASING); + } + + if (ScrollView.FRAME_STEP) { + sv.set(FRAME_DURATION, ScrollView.FRAME_STEP); + } + + if (ScrollView.BOUNCE_RANGE) { + sv.set(BOUNCE_RANGE, ScrollView.BOUNCE_RANGE); + } + + // Recalculate dimension properties + // TODO: This should be throttled. + // Y.one(WINDOW).after('resize', sv._afterDimChange, sv); + }, + + /** + * Bind event listeners + * + * @method _bindAttrs + * @private + */ + _bindAttrs: function () { + var sv = this, + scrollChangeHandler = sv._afterScrollChange, + dimChangeHandler = sv._afterDimChange; + + // Bind any change event listeners + sv.after({ + 'scrollEnd': sv._afterScrollEnd, + 'disabledChange': sv._afterDisabledChange, + 'flickChange': sv._afterFlickChange, + 'dragChange': sv._afterDragChange, + 'axisChange': sv._afterAxisChange, + 'scrollYChange': scrollChangeHandler, + 'scrollXChange': scrollChangeHandler, + 'heightChange': dimChangeHandler, + 'widthChange': dimChangeHandler + }); + }, + + /** + * Bind (or unbind) gesture move listeners required for drag support + * + * @method _bindDrag + * @param drag {boolean} If true, the method binds listener to enable + * drag (gesturemovestart). If false, the method unbinds gesturemove + * listeners for drag support. + * @private + */ + _bindDrag: function (drag) { + var sv = this, + bb = sv._bb; + + // Unbind any previous 'drag' listeners + bb.detach(DRAG + '|*'); + + if (drag) { + bb.on(DRAG + '|' + GESTURE_MOVE + START, Y.bind(sv._onGestureMoveStart, sv)); + } + }, + + /** + * Bind (or unbind) flick listeners. + * + * @method _bindFlick + * @param flick {Object|boolean} If truthy, the method binds listeners for + * flick support. If false, the method unbinds flick listeners. + * @private + */ + _bindFlick: function (flick) { + var sv = this, + bb = sv._bb; + + // Unbind any previous 'flick' listeners + bb.detach(FLICK + '|*'); + + if (flick) { + bb.on(FLICK + '|' + FLICK, Y.bind(sv._flick, sv), flick); + + // Rebind Drag, becuase _onGestureMoveEnd always has to fire -after- _flick + sv._bindDrag(sv.get(DRAG)); + } + }, + + /** + * Bind (or unbind) mousewheel listeners. + * + * @method _bindMousewheel + * @param mousewheel {Object|boolean} If truthy, the method binds listeners for + * mousewheel support. If false, the method unbinds mousewheel listeners. + * @private + */ + _bindMousewheel: function (mousewheel) { + var sv = this, + bb = sv._bb; + + // Unbind any previous 'mousewheel' listeners + // TODO: This doesn't actually appear to work properly. Fix. #2532743 + bb.detach(MOUSEWHEEL + '|*'); + + // Only enable for vertical scrollviews + if (mousewheel) { + // Bound to document, because that's where mousewheel events fire off of. + Y.one(DOCUMENT).on(MOUSEWHEEL, Y.bind(sv._mousewheel, sv)); + } + }, + + /** + * syncUI implementation. + * + * Update the scroll position, based on the current value of scrollX/scrollY. + * + * @method syncUI + */ + syncUI: function () { + var sv = this, + scrollDims = sv._getScrollDims(), + width = scrollDims.offsetWidth, + height = scrollDims.offsetHeight, + scrollWidth = scrollDims.scrollWidth, + scrollHeight = scrollDims.scrollHeight; + + // If the axis is undefined, auto-calculate it + if (sv._cAxis === undefined) { + // This should only ever be run once (for now). + // In the future SV might post-load axis changes + sv._cAxis = { + x: (scrollWidth > width), + y: (scrollHeight > height) + }; + + sv._set(AXIS, sv._cAxis); + } + + // get text direction on or inherited by scrollview node + sv.rtl = (sv._cb.getComputedStyle('direction') === 'rtl'); + + // Cache the disabled value + sv._cDisabled = sv.get(DISABLED); + + // Run this to set initial values + sv._uiDimensionsChange(); + + // If we're out-of-bounds, snap back. + if (sv._isOutOfBounds()) { + sv._snapBack(); + } + }, + + /** + * Utility method to obtain widget dimensions + * + * @method _getScrollDims + * @return {Object} The offsetWidth, offsetHeight, scrollWidth and + * scrollHeight as an array: [offsetWidth, offsetHeight, scrollWidth, + * scrollHeight] + * @private + */ + _getScrollDims: function () { + var sv = this, + cb = sv._cb, + bb = sv._bb, + TRANS = ScrollView._TRANSITION, + // Ideally using CSSMatrix - don't think we have it normalized yet though. + // origX = (new WebKitCSSMatrix(cb.getComputedStyle("transform"))).e, + // origY = (new WebKitCSSMatrix(cb.getComputedStyle("transform"))).f, + origX = sv.get(SCROLL_X), + origY = sv.get(SCROLL_Y), + origHWTransform, + dims; + + // TODO: Is this OK? Just in case it's called 'during' a transition. + if (NATIVE_TRANSITIONS) { + cb.setStyle(TRANS.DURATION, ZERO); + cb.setStyle(TRANS.PROPERTY, EMPTY); + } + + origHWTransform = sv._forceHWTransforms; + sv._forceHWTransforms = false; // the z translation was causing issues with picking up accurate scrollWidths in Chrome/Mac. + + sv._moveTo(cb, 0, 0); + dims = { + 'offsetWidth': bb.get('offsetWidth'), + 'offsetHeight': bb.get('offsetHeight'), + 'scrollWidth': bb.get('scrollWidth'), + 'scrollHeight': bb.get('scrollHeight') + }; + sv._moveTo(cb, -(origX), -(origY)); + + sv._forceHWTransforms = origHWTransform; + + return dims; + }, + + /** + * This method gets invoked whenever the height or width attributes change, + * allowing us to determine which scrolling axes need to be enabled. + * + * @method _uiDimensionsChange + * @protected + */ + _uiDimensionsChange: function () { + var sv = this, + bb = sv._bb, + scrollDims = sv._getScrollDims(), + width = scrollDims.offsetWidth, + height = scrollDims.offsetHeight, + scrollWidth = scrollDims.scrollWidth, + scrollHeight = scrollDims.scrollHeight, + rtl = sv.rtl, + svAxis = sv._cAxis, + minScrollX = (rtl ? Math.min(0, -(scrollWidth - width)) : 0), + maxScrollX = (rtl ? 0 : Math.max(0, scrollWidth - width)), + minScrollY = 0, + maxScrollY = Math.max(0, scrollHeight - height); + + if (svAxis && svAxis.x) { + bb.addClass(CLASS_NAMES.horizontal); + } + + if (svAxis && svAxis.y) { + bb.addClass(CLASS_NAMES.vertical); + } + + sv._setBounds({ + minScrollX: minScrollX, + maxScrollX: maxScrollX, + minScrollY: minScrollY, + maxScrollY: maxScrollY + }); + }, + + /** + * Set the bounding dimensions of the ScrollView + * + * @method _setBounds + * @protected + * @param bounds {Object} [duration] ms of the scroll animation. (default is 0) + * @param {Number} [bounds.minScrollX] The minimum scroll X value + * @param {Number} [bounds.maxScrollX] The maximum scroll X value + * @param {Number} [bounds.minScrollY] The minimum scroll Y value + * @param {Number} [bounds.maxScrollY] The maximum scroll Y value + */ + _setBounds: function (bounds) { + var sv = this; + + // TODO: Do a check to log if the bounds are invalid + + sv._minScrollX = bounds.minScrollX; + sv._maxScrollX = bounds.maxScrollX; + sv._minScrollY = bounds.minScrollY; + sv._maxScrollY = bounds.maxScrollY; + }, + + /** + * Get the bounding dimensions of the ScrollView + * + * @method _getBounds + * @protected + */ + _getBounds: function () { + var sv = this; + + return { + minScrollX: sv._minScrollX, + maxScrollX: sv._maxScrollX, + minScrollY: sv._minScrollY, + maxScrollY: sv._maxScrollY + }; + + }, + + /** + * Scroll the element to a given xy coordinate + * + * @method scrollTo + * @param x {Number} The x-position to scroll to. (null for no movement) + * @param y {Number} The y-position to scroll to. (null for no movement) + * @param {Number} [duration] ms of the scroll animation. (default is 0) + * @param {String} [easing] An easing equation if duration is set. (default is `easing` attribute) + * @param {String} [node] The node to transform. Setting this can be useful in + * dual-axis paginated instances. (default is the instance's contentBox) + */ + scrollTo: function (x, y, duration, easing, node) { + // Check to see if widget is disabled + if (this._cDisabled) { + return; + } + + var sv = this, + cb = sv._cb, + TRANS = ScrollView._TRANSITION, + callback = Y.bind(sv._onTransEnd, sv), // @Todo : cache this + newX = 0, + newY = 0, + transition = {}, + transform; + + // default the optional arguments + duration = duration || 0; + easing = easing || sv.get(EASING); // @TODO: Cache this + node = node || cb; + + if (x !== null) { + sv.set(SCROLL_X, x, {src:UI}); + newX = -(x); + } + + if (y !== null) { + sv.set(SCROLL_Y, y, {src:UI}); + newY = -(y); + } + + transform = sv._transform(newX, newY); + + if (NATIVE_TRANSITIONS) { + // ANDROID WORKAROUND - try and stop existing transition, before kicking off new one. + node.setStyle(TRANS.DURATION, ZERO).setStyle(TRANS.PROPERTY, EMPTY); + } + + // Move + if (duration === 0) { + if (NATIVE_TRANSITIONS) { + node.setStyle('transform', transform); + } + else { + // TODO: If both set, batch them in the same update + // Update: Nope, setStyles() just loops through each property and applies it. + if (x !== null) { + node.setStyle(LEFT, newX + PX); + } + if (y !== null) { + node.setStyle(TOP, newY + PX); + } + } + } + + // Animate + else { + transition.easing = easing; + transition.duration = duration / 1000; + + if (NATIVE_TRANSITIONS) { + transition.transform = transform; + } + else { + transition.left = newX + PX; + transition.top = newY + PX; + } + + node.transition(transition, callback); + } + }, + + /** + * Utility method, to create the translate transform string with the + * x, y translation amounts provided. + * + * @method _transform + * @param {Number} x Number of pixels to translate along the x axis + * @param {Number} y Number of pixels to translate along the y axis + * @private + */ + _transform: function (x, y) { + // TODO: Would we be better off using a Matrix for this? + var prop = 'translate(' + x + 'px, ' + y + 'px)'; + + if (this._forceHWTransforms) { + prop += ' translateZ(0)'; + } + + return prop; + }, + + /** + * Utility method, to move the given element to the given xy position + * + * @method _moveTo + * @param node {Node} The node to move + * @param x {Number} The x-position to move to + * @param y {Number} The y-position to move to + * @private + */ + _moveTo : function(node, x, y) { + if (NATIVE_TRANSITIONS) { + node.setStyle('transform', this._transform(x, y)); + } else { + node.setStyle(LEFT, x + PX); + node.setStyle(TOP, y + PX); + } + }, + + + /** + * Content box transition callback + * + * @method _onTransEnd + * @param {EventFacade} e The event facade + * @private + */ + _onTransEnd: function () { + var sv = this; + + // If for some reason we're OOB, snapback + if (sv._isOutOfBounds()) { + sv._snapBack(); + } + else { + /** + * Notification event fired at the end of a scroll transition + * + * @event scrollEnd + * @param e {EventFacade} The default event facade. + */ + sv.fire(EV_SCROLL_END); + } + }, + + /** + * gesturemovestart event handler + * + * @method _onGestureMoveStart + * @param e {EventFacade} The gesturemovestart event facade + * @private + */ + _onGestureMoveStart: function (e) { + + if (this._cDisabled) { + return false; + } + + var sv = this, + bb = sv._bb, + currentX = sv.get(SCROLL_X), + currentY = sv.get(SCROLL_Y), + clientX = e.clientX, + clientY = e.clientY; + + if (sv._prevent.start) { + e.preventDefault(); + } + + // if a flick animation is in progress, cancel it + if (sv._flickAnim) { + sv._cancelFlick(); + sv._onTransEnd(); + } + + // Reset lastScrolledAmt + sv.lastScrolledAmt = 0; + + // Stores data for this gesture cycle. Cleaned up later + sv._gesture = { + + // Will hold the axis value + axis: null, + + // The current attribute values + startX: currentX, + startY: currentY, + + // The X/Y coordinates where the event began + startClientX: clientX, + startClientY: clientY, + + // The X/Y coordinates where the event will end + endClientX: null, + endClientY: null, + + // The current delta of the event + deltaX: null, + deltaY: null, + + // Will be populated for flicks + flick: null, + + // Create some listeners for the rest of the gesture cycle + onGestureMove: bb.on(DRAG + '|' + GESTURE_MOVE, Y.bind(sv._onGestureMove, sv)), + + // @TODO: Don't bind gestureMoveEnd if it's a Flick? + onGestureMoveEnd: bb.on(DRAG + '|' + GESTURE_MOVE + END, Y.bind(sv._onGestureMoveEnd, sv)) + }; + }, + + /** + * gesturemove event handler + * + * @method _onGestureMove + * @param e {EventFacade} The gesturemove event facade + * @private + */ + _onGestureMove: function (e) { + var sv = this, + gesture = sv._gesture, + svAxis = sv._cAxis, + svAxisX = svAxis.x, + svAxisY = svAxis.y, + startX = gesture.startX, + startY = gesture.startY, + startClientX = gesture.startClientX, + startClientY = gesture.startClientY, + clientX = e.clientX, + clientY = e.clientY; + + if (sv._prevent.move) { + e.preventDefault(); + } + + gesture.deltaX = startClientX - clientX; + gesture.deltaY = startClientY - clientY; + + // Determine if this is a vertical or horizontal movement + // @TODO: This is crude, but it works. Investigate more intelligent ways to detect intent + if (gesture.axis === null) { + gesture.axis = (Math.abs(gesture.deltaX) > Math.abs(gesture.deltaY)) ? DIM_X : DIM_Y; + } + + // Move X or Y. @TODO: Move both if dualaxis. + if (gesture.axis === DIM_X && svAxisX) { + sv.set(SCROLL_X, startX + gesture.deltaX); + } + else if (gesture.axis === DIM_Y && svAxisY) { + sv.set(SCROLL_Y, startY + gesture.deltaY); + } + }, + + /** + * gesturemoveend event handler + * + * @method _onGestureMoveEnd + * @param e {EventFacade} The gesturemoveend event facade + * @private + */ + _onGestureMoveEnd: function (e) { + var sv = this, + gesture = sv._gesture, + flick = gesture.flick, + clientX = e.clientX, + clientY = e.clientY, + isOOB; + + if (sv._prevent.end) { + e.preventDefault(); + } + + // Store the end X/Y coordinates + gesture.endClientX = clientX; + gesture.endClientY = clientY; + + // Cleanup the event handlers + gesture.onGestureMove.detach(); + gesture.onGestureMoveEnd.detach(); + + // If this wasn't a flick, wrap up the gesture cycle + if (!flick) { + // @TODO: Be more intelligent about this. Look at the Flick attribute to see + // if it is safe to assume _flick did or didn't fire. + // Then, the order _flick and _onGestureMoveEnd fire doesn't matter? + + // If there was movement (_onGestureMove fired) + if (gesture.deltaX !== null && gesture.deltaY !== null) { + + isOOB = sv._isOutOfBounds(); + + // If we're out-out-bounds, then snapback + if (isOOB) { + sv._snapBack(); + } + + // Inbounds + else { + // Fire scrollEnd unless this is a paginated instance and the gesture axis is the same as paginator's + // Not totally confident this is ideal to access a plugin's properties from a host, @TODO revisit + if (!sv.pages || (sv.pages && !sv.pages.get(AXIS)[gesture.axis])) { + sv._onTransEnd(); + } + } + } + } + }, + + /** + * Execute a flick at the end of a scroll action + * + * @method _flick + * @param e {EventFacade} The Flick event facade + * @private + */ + _flick: function (e) { + if (this._cDisabled) { + return false; + } + + var sv = this, + svAxis = sv._cAxis, + flick = e.flick, + flickAxis = flick.axis, + flickVelocity = flick.velocity, + axisAttr = flickAxis === DIM_X ? SCROLL_X : SCROLL_Y, + startPosition = sv.get(axisAttr); + + // Sometimes flick is enabled, but drag is disabled + if (sv._gesture) { + sv._gesture.flick = flick; + } + + // Prevent unneccesary firing of _flickFrame if we can't scroll on the flick axis + if (svAxis[flickAxis]) { + sv._flickFrame(flickVelocity, flickAxis, startPosition); + } + }, + + /** + * Execute a single frame in the flick animation + * + * @method _flickFrame + * @param velocity {Number} The velocity of this animated frame + * @param flickAxis {String} The axis on which to animate + * @param startPosition {Number} The starting X/Y point to flick from + * @protected + */ + _flickFrame: function (velocity, flickAxis, startPosition) { + + var sv = this, + axisAttr = flickAxis === DIM_X ? SCROLL_X : SCROLL_Y, + bounds = sv._getBounds(), + + // Localize cached values + bounce = sv._cBounce, + bounceRange = sv._cBounceRange, + deceleration = sv._cDeceleration, + frameDuration = sv._cFrameDuration, + + // Calculate + newVelocity = velocity * deceleration, + newPosition = startPosition - (frameDuration * newVelocity), + + // Some convinience conditions + min = flickAxis === DIM_X ? bounds.minScrollX : bounds.minScrollY, + max = flickAxis === DIM_X ? bounds.maxScrollX : bounds.maxScrollY, + belowMin = (newPosition < min), + belowMax = (newPosition < max), + aboveMin = (newPosition > min), + aboveMax = (newPosition > max), + belowMinRange = (newPosition < (min - bounceRange)), + withinMinRange = (belowMin && (newPosition > (min - bounceRange))), + withinMaxRange = (aboveMax && (newPosition < (max + bounceRange))), + aboveMaxRange = (newPosition > (max + bounceRange)), + tooSlow; + + // If we're within the range but outside min/max, dampen the velocity + if (withinMinRange || withinMaxRange) { + newVelocity *= bounce; + } + + // Is the velocity too slow to bother? + tooSlow = (Math.abs(newVelocity).toFixed(4) < 0.015); + + // If the velocity is too slow or we're outside the range + if (tooSlow || belowMinRange || aboveMaxRange) { + // Cancel and delete sv._flickAnim + if (sv._flickAnim) { + sv._cancelFlick(); + } + + // If we're inside the scroll area, just end + if (aboveMin && belowMax) { + sv._onTransEnd(); + } + + // We're outside the scroll area, so we need to snap back + else { + sv._snapBack(); + } + } + + // Otherwise, animate to the next frame + else { + // @TODO: maybe use requestAnimationFrame instead + sv._flickAnim = Y.later(frameDuration, sv, '_flickFrame', [newVelocity, flickAxis, newPosition]); + sv.set(axisAttr, newPosition); + } + }, + + _cancelFlick: function () { + var sv = this; + + if (sv._flickAnim) { + // Cancel the flick (if it exists) + sv._flickAnim.cancel(); + + // Also delete it, otherwise _onGestureMoveStart will think we're still flicking + delete sv._flickAnim; + } + + }, + + /** + * Handle mousewheel events on the widget + * + * @method _mousewheel + * @param e {EventFacade} The mousewheel event facade + * @private + */ + _mousewheel: function (e) { + var sv = this, + scrollY = sv.get(SCROLL_Y), + bounds = sv._getBounds(), + bb = sv._bb, + scrollOffset = 10, // 10px + isForward = (e.wheelDelta > 0), + scrollToY = scrollY - ((isForward ? 1 : -1) * scrollOffset); + + scrollToY = _constrain(scrollToY, bounds.minScrollY, bounds.maxScrollY); + + // Because Mousewheel events fire off 'document', every ScrollView widget will react + // to any mousewheel anywhere on the page. This check will ensure that the mouse is currently + // over this specific ScrollView. Also, only allow mousewheel scrolling on Y-axis, + // becuase otherwise the 'prevent' will block page scrolling. + if (bb.contains(e.target) && sv._cAxis[DIM_Y]) { + + // Reset lastScrolledAmt + sv.lastScrolledAmt = 0; + + // Jump to the new offset + sv.set(SCROLL_Y, scrollToY); + + // if we have scrollbars plugin, update & set the flash timer on the scrollbar + // @TODO: This probably shouldn't be in this module + if (sv.scrollbars) { + // @TODO: The scrollbars should handle this themselves + sv.scrollbars._update(); + sv.scrollbars.flash(); + // or just this + // sv.scrollbars._hostDimensionsChange(); + } + + // Fire the 'scrollEnd' event + sv._onTransEnd(); + + // prevent browser default behavior on mouse scroll + e.preventDefault(); + } + }, + + /** + * Checks to see the current scrollX/scrollY position beyond the min/max boundary + * + * @method _isOutOfBounds + * @param x {Number} [optional] The X position to check + * @param y {Number} [optional] The Y position to check + * @return {Boolean} Whether the current X/Y position is out of bounds (true) or not (false) + * @private + */ + _isOutOfBounds: function (x, y) { + var sv = this, + svAxis = sv._cAxis, + svAxisX = svAxis.x, + svAxisY = svAxis.y, + currentX = x || sv.get(SCROLL_X), + currentY = y || sv.get(SCROLL_Y), + bounds = sv._getBounds(), + minX = bounds.minScrollX, + minY = bounds.minScrollY, + maxX = bounds.maxScrollX, + maxY = bounds.maxScrollY; + + return (svAxisX && (currentX < minX || currentX > maxX)) || (svAxisY && (currentY < minY || currentY > maxY)); + }, + + /** + * Bounces back + * @TODO: Should be more generalized and support both X and Y detection + * + * @method _snapBack + * @private + */ + _snapBack: function () { + var sv = this, + currentX = sv.get(SCROLL_X), + currentY = sv.get(SCROLL_Y), + bounds = sv._getBounds(), + minX = bounds.minScrollX, + minY = bounds.minScrollY, + maxX = bounds.maxScrollX, + maxY = bounds.maxScrollY, + newY = _constrain(currentY, minY, maxY), + newX = _constrain(currentX, minX, maxX), + duration = sv.get(SNAP_DURATION), + easing = sv.get(SNAP_EASING); + + if (newX !== currentX) { + sv.set(SCROLL_X, newX, {duration:duration, easing:easing}); + } + else if (newY !== currentY) { + sv.set(SCROLL_Y, newY, {duration:duration, easing:easing}); + } + else { + sv._onTransEnd(); + } + }, + + /** + * After listener for changes to the scrollX or scrollY attribute + * + * @method _afterScrollChange + * @param e {EventFacade} The event facade + * @protected + */ + _afterScrollChange: function (e) { + if (e.src === ScrollView.UI_SRC) { + return false; + } + + var sv = this, + duration = e.duration, + easing = e.easing, + val = e.newVal, + scrollToArgs = []; + + // Set the scrolled value + sv.lastScrolledAmt = sv.lastScrolledAmt + (e.newVal - e.prevVal); + + // Generate the array of args to pass to scrollTo() + if (e.attrName === SCROLL_X) { + scrollToArgs.push(val); + scrollToArgs.push(sv.get(SCROLL_Y)); + } + else { + scrollToArgs.push(sv.get(SCROLL_X)); + scrollToArgs.push(val); + } + + scrollToArgs.push(duration); + scrollToArgs.push(easing); + + sv.scrollTo.apply(sv, scrollToArgs); + }, + + /** + * After listener for changes to the flick attribute + * + * @method _afterFlickChange + * @param e {EventFacade} The event facade + * @protected + */ + _afterFlickChange: function (e) { + this._bindFlick(e.newVal); + }, + + /** + * After listener for changes to the disabled attribute + * + * @method _afterDisabledChange + * @param e {EventFacade} The event facade + * @protected + */ + _afterDisabledChange: function (e) { + // Cache for performance - we check during move + this._cDisabled = e.newVal; + }, + + /** + * After listener for the axis attribute + * + * @method _afterAxisChange + * @param e {EventFacade} The event facade + * @protected + */ + _afterAxisChange: function (e) { + this._cAxis = e.newVal; + }, + + /** + * After listener for changes to the drag attribute + * + * @method _afterDragChange + * @param e {EventFacade} The event facade + * @protected + */ + _afterDragChange: function (e) { + this._bindDrag(e.newVal); + }, + + /** + * After listener for the height or width attribute + * + * @method _afterDimChange + * @param e {EventFacade} The event facade + * @protected + */ + _afterDimChange: function () { + this._uiDimensionsChange(); + }, + + /** + * After listener for scrollEnd, for cleanup + * + * @method _afterScrollEnd + * @param e {EventFacade} The event facade + * @protected + */ + _afterScrollEnd: function () { + var sv = this; + + if (sv._flickAnim) { + sv._cancelFlick(); + } + + // Ideally this should be removed, but doing so causing some JS errors with fast swiping + // because _gesture is being deleted after the previous one has been overwritten + // delete sv._gesture; // TODO: Move to sv.prevGesture? + }, + + /** + * Setter for 'axis' attribute + * + * @method _axisSetter + * @param val {Mixed} A string ('x', 'y', 'xy') to specify which axis/axes to allow scrolling on + * @param name {String} The attribute name + * @return {Object} An object to specify scrollability on the x & y axes + * + * @protected + */ + _axisSetter: function (val) { + + // Turn a string into an axis object + if (Y.Lang.isString(val)) { + return { + x: val.match(/x/i) ? true : false, + y: val.match(/y/i) ? true : false + }; + } + }, + + /** + * The scrollX, scrollY setter implementation + * + * @method _setScroll + * @private + * @param {Number} val + * @param {String} dim + * + * @return {Number} The value + */ + _setScroll : function(val) { + + // Just ensure the widget is not disabled + if (this._cDisabled) { + val = Y.Attribute.INVALID_VALUE; + } + + return val; + }, + + /** + * Setter for the scrollX attribute + * + * @method _setScrollX + * @param val {Number} The new scrollX value + * @return {Number} The normalized value + * @protected + */ + _setScrollX: function(val) { + return this._setScroll(val, DIM_X); + }, + + /** + * Setter for the scrollY ATTR + * + * @method _setScrollY + * @param val {Number} The new scrollY value + * @return {Number} The normalized value + * @protected + */ + _setScrollY: function(val) { + return this._setScroll(val, DIM_Y); + } + + // End prototype properties + +}, { + + // Static properties + + /** + * The identity of the widget. + * + * @property NAME + * @type String + * @default 'scrollview' + * @readOnly + * @protected + * @static + */ + NAME: 'scrollview', + + /** + * Static property used to define the default attribute configuration of + * the Widget. + * + * @property ATTRS + * @type {Object} + * @protected + * @static + */ + ATTRS: { + + /** + * Specifies ability to scroll on x, y, or x and y axis/axes. + * + * @attribute axis + * @type String + */ + axis: { + setter: '_axisSetter', + writeOnce: 'initOnly' + }, + + /** + * The current scroll position in the x-axis + * + * @attribute scrollX + * @type Number + * @default 0 + */ + scrollX: { + value: 0, + setter: '_setScrollX' + }, + + /** + * The current scroll position in the y-axis + * + * @attribute scrollY + * @type Number + * @default 0 + */ + scrollY: { + value: 0, + setter: '_setScrollY' + }, + + /** + * Drag coefficent for inertial scrolling. The closer to 1 this + * value is, the less friction during scrolling. + * + * @attribute deceleration + * @default 0.93 + */ + deceleration: { + value: 0.93 + }, + + /** + * Drag coefficient for intertial scrolling at the upper + * and lower boundaries of the scrollview. Set to 0 to + * disable "rubber-banding". + * + * @attribute bounce + * @type Number + * @default 0.1 + */ + bounce: { + value: 0.1 + }, + + /** + * The minimum distance and/or velocity which define a flick. Can be set to false, + * to disable flick support (note: drag support is enabled/disabled separately) + * + * @attribute flick + * @type Object + * @default Object with properties minDistance = 10, minVelocity = 0.3. + */ + flick: { + value: { + minDistance: 10, + minVelocity: 0.3 + } + }, + + /** + * Enable/Disable dragging the ScrollView content (note: flick support is enabled/disabled separately) + * @attribute drag + * @type boolean + * @default true + */ + drag: { + value: true + }, + + /** + * The default duration to use when animating the bounce snap back. + * + * @attribute snapDuration + * @type Number + * @default 400 + */ + snapDuration: { + value: 400 + }, + + /** + * The default easing to use when animating the bounce snap back. + * + * @attribute snapEasing + * @type String + * @default 'ease-out' + */ + snapEasing: { + value: 'ease-out' + }, + + /** + * The default easing used when animating the flick + * + * @attribute easing + * @type String + * @default 'cubic-bezier(0, 0.1, 0, 1.0)' + */ + easing: { + value: 'cubic-bezier(0, 0.1, 0, 1.0)' + }, + + /** + * The interval (ms) used when animating the flick for JS-timer animations + * + * @attribute frameDuration + * @type Number + * @default 15 + */ + frameDuration: { + value: 15 + }, + + /** + * The default bounce distance in pixels + * + * @attribute bounceRange + * @type Number + * @default 150 + */ + bounceRange: { + value: 150 + } + }, + + /** + * List of class names used in the scrollview's DOM + * + * @property CLASS_NAMES + * @type Object + * @static + */ + CLASS_NAMES: CLASS_NAMES, + + /** + * Flag used to source property changes initiated from the DOM + * + * @property UI_SRC + * @type String + * @static + * @default 'ui' + */ + UI_SRC: UI, + + /** + * Object map of style property names used to set transition properties. + * Defaults to the vendor prefix established by the Transition module. + * The configured property names are `_TRANSITION.DURATION` (e.g. "WebkitTransitionDuration") and + * `_TRANSITION.PROPERTY (e.g. "WebkitTransitionProperty"). + * + * @property _TRANSITION + * @private + */ + _TRANSITION: { + DURATION: (vendorPrefix ? vendorPrefix + 'TransitionDuration' : 'transitionDuration'), + PROPERTY: (vendorPrefix ? vendorPrefix + 'TransitionProperty' : 'transitionProperty') + }, + + /** + * The default bounce distance in pixels + * + * @property BOUNCE_RANGE + * @type Number + * @static + * @default false + * @deprecated (in 3.7.0) + */ + BOUNCE_RANGE: false, + + /** + * The interval (ms) used when animating the flick + * + * @property FRAME_STEP + * @type Number + * @static + * @default false + * @deprecated (in 3.7.0) + */ + FRAME_STEP: false, + + /** + * The default easing used when animating the flick + * + * @property EASING + * @type String + * @static + * @default false + * @deprecated (in 3.7.0) + */ + EASING: false, + + /** + * The default easing to use when animating the bounce snap back. + * + * @property SNAP_EASING + * @type String + * @static + * @default false + * @deprecated (in 3.7.0) + */ + SNAP_EASING: false, + + /** + * The default duration to use when animating the bounce snap back. + * + * @property SNAP_DURATION + * @type Number + * @static + * @default false + * @deprecated (in 3.7.0) + */ + SNAP_DURATION: false + + // End static properties + +}); + + +}, '@VERSION@', {"requires": ["widget", "event-gestures", "event-mousewheel", "transition"], "skinnable": true});