NOBUG: Fixed file access permissions
[moodle.git] / lib / yuilib / 3.13.0 / scrollview-base / scrollview-base-debug.js
blob88666826ee88098231fd71ce3dbf8da54a94f0b9
1 /*
2 YUI 3.13.0 (build 508226d)
3 Copyright 2013 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
8 YUI.add('scrollview-base', function (Y, NAME) {
10 /**
11  * The scrollview-base module provides a basic ScrollView Widget, without scrollbar indicators
12  *
13  * @module scrollview
14  * @submodule scrollview-base
15  */
17  // Local vars
18 var getClassName = Y.ClassNameManager.getClassName,
19     DOCUMENT = Y.config.doc,
20     IE = Y.UA.ie,
21     NATIVE_TRANSITIONS = Y.Transition.useNative,
22     vendorPrefix = Y.Transition._VENDOR_PREFIX, // Todo: This is a private property, and alternative approaches should be investigated
23     SCROLLVIEW = 'scrollview',
24     CLASS_NAMES = {
25         vertical: getClassName(SCROLLVIEW, 'vert'),
26         horizontal: getClassName(SCROLLVIEW, 'horiz')
27     },
28     EV_SCROLL_END = 'scrollEnd',
29     FLICK = 'flick',
30     DRAG = 'drag',
31     MOUSEWHEEL = 'mousewheel',
32     UI = 'ui',
33     TOP = 'top',
34     LEFT = 'left',
35     PX = 'px',
36     AXIS = 'axis',
37     SCROLL_Y = 'scrollY',
38     SCROLL_X = 'scrollX',
39     BOUNCE = 'bounce',
40     DISABLED = 'disabled',
41     DECELERATION = 'deceleration',
42     DIM_X = 'x',
43     DIM_Y = 'y',
44     BOUNDING_BOX = 'boundingBox',
45     CONTENT_BOX = 'contentBox',
46     GESTURE_MOVE = 'gesturemove',
47     START = 'start',
48     END = 'end',
49     EMPTY = '',
50     ZERO = '0s',
51     SNAP_DURATION = 'snapDuration',
52     SNAP_EASING = 'snapEasing',
53     EASING = 'easing',
54     FRAME_DURATION = 'frameDuration',
55     BOUNCE_RANGE = 'bounceRange',
56     _constrain = function (val, min, max) {
57         return Math.min(Math.max(val, min), max);
58     };
60 /**
61  * ScrollView provides a scrollable widget, supporting flick gestures,
62  * across both touch and mouse based devices.
63  *
64  * @class ScrollView
65  * @param config {Object} Object literal with initial attribute values
66  * @extends Widget
67  * @constructor
68  */
69 function ScrollView() {
70     ScrollView.superclass.constructor.apply(this, arguments);
73 Y.ScrollView = Y.extend(ScrollView, Y.Widget, {
75     // *** Y.ScrollView prototype
77     /**
78      * Flag driving whether or not we should try and force H/W acceleration when transforming. Currently enabled by default for Webkit.
79      * Used by the _transform method.
80      *
81      * @property _forceHWTransforms
82      * @type boolean
83      * @protected
84      */
85     _forceHWTransforms: Y.UA.webkit ? true : false,
87     /**
88      * <p>Used to control whether or not ScrollView's internal
89      * gesturemovestart, gesturemove and gesturemoveend
90      * event listeners should preventDefault. The value is an
91      * object, with "start", "move" and "end" properties used to
92      * specify which events should preventDefault and which shouldn't:</p>
93      *
94      * <pre>
95      * {
96      *    start: false,
97      *    move: true,
98      *    end: false
99      * }
100      * </pre>
101      *
102      * <p>The default values are set up in order to prevent panning,
103      * on touch devices, while allowing click listeners on elements inside
104      * the ScrollView to be notified as expected.</p>
105      *
106      * @property _prevent
107      * @type Object
108      * @protected
109      */
110     _prevent: {
111         start: false,
112         move: true,
113         end: false
114     },
116     /**
117      * Contains the distance (postive or negative) in pixels by which
118      *  the scrollview was last scrolled. This is useful when setting up
119      *  click listeners on the scrollview content, which on mouse based
120      *  devices are always fired, even after a drag/flick.
121      *
122      * <p>Touch based devices don't currently fire a click event,
123      *  if the finger has been moved (beyond a threshold) so this
124      *  check isn't required, if working in a purely touch based environment</p>
125      *
126      * @property lastScrolledAmt
127      * @type Number
128      * @public
129      * @default 0
130      */
131     lastScrolledAmt: 0,
133     /**
134      * Internal state, defines the minimum amount that the scrollview can be scrolled along the X axis
135      *
136      * @property _minScrollX
137      * @type number
138      * @protected
139      */
140     _minScrollX: null,
142     /**
143      * Internal state, defines the maximum amount that the scrollview can be scrolled along the X axis
144      *
145      * @property _maxScrollX
146      * @type number
147      * @protected
148      */
149     _maxScrollX: null,
151     /**
152      * Internal state, defines the minimum amount that the scrollview can be scrolled along the Y axis
153      *
154      * @property _minScrollY
155      * @type number
156      * @protected
157      */
158     _minScrollY: null,
160     /**
161      * Internal state, defines the maximum amount that the scrollview can be scrolled along the Y axis
162      *
163      * @property _maxScrollY
164      * @type number
165      * @protected
166      */
167     _maxScrollY: null,
169     /**
170      * Designated initializer
171      *
172      * @method initializer
173      * @param {config} Configuration object for the plugin
174      */
175     initializer: function () {
176         var sv = this;
178         // Cache these values, since they aren't going to change.
179         sv._bb = sv.get(BOUNDING_BOX);
180         sv._cb = sv.get(CONTENT_BOX);
182         // Cache some attributes
183         sv._cAxis = sv.get(AXIS);
184         sv._cBounce = sv.get(BOUNCE);
185         sv._cBounceRange = sv.get(BOUNCE_RANGE);
186         sv._cDeceleration = sv.get(DECELERATION);
187         sv._cFrameDuration = sv.get(FRAME_DURATION);
188     },
190     /**
191      * bindUI implementation
192      *
193      * Hooks up events for the widget
194      * @method bindUI
195      */
196     bindUI: function () {
197         var sv = this;
199         // Bind interaction listers
200         sv._bindFlick(sv.get(FLICK));
201         sv._bindDrag(sv.get(DRAG));
202         sv._bindMousewheel(true);
204         // Bind change events
205         sv._bindAttrs();
207         // IE SELECT HACK. See if we can do this non-natively and in the gesture for a future release.
208         if (IE) {
209             sv._fixIESelect(sv._bb, sv._cb);
210         }
212         // Set any deprecated static properties
213         if (ScrollView.SNAP_DURATION) {
214             sv.set(SNAP_DURATION, ScrollView.SNAP_DURATION);
215         }
217         if (ScrollView.SNAP_EASING) {
218             sv.set(SNAP_EASING, ScrollView.SNAP_EASING);
219         }
221         if (ScrollView.EASING) {
222             sv.set(EASING, ScrollView.EASING);
223         }
225         if (ScrollView.FRAME_STEP) {
226             sv.set(FRAME_DURATION, ScrollView.FRAME_STEP);
227         }
229         if (ScrollView.BOUNCE_RANGE) {
230             sv.set(BOUNCE_RANGE, ScrollView.BOUNCE_RANGE);
231         }
233         // Recalculate dimension properties
234         // TODO: This should be throttled.
235         // Y.one(WINDOW).after('resize', sv._afterDimChange, sv);
236     },
238     /**
239      * Bind event listeners
240      *
241      * @method _bindAttrs
242      * @private
243      */
244     _bindAttrs: function () {
245         var sv = this,
246             scrollChangeHandler = sv._afterScrollChange,
247             dimChangeHandler = sv._afterDimChange;
249         // Bind any change event listeners
250         sv.after({
251             'scrollEnd': sv._afterScrollEnd,
252             'disabledChange': sv._afterDisabledChange,
253             'flickChange': sv._afterFlickChange,
254             'dragChange': sv._afterDragChange,
255             'axisChange': sv._afterAxisChange,
256             'scrollYChange': scrollChangeHandler,
257             'scrollXChange': scrollChangeHandler,
258             'heightChange': dimChangeHandler,
259             'widthChange': dimChangeHandler
260         });
261     },
263     /**
264      * Bind (or unbind) gesture move listeners required for drag support
265      *
266      * @method _bindDrag
267      * @param drag {boolean} If true, the method binds listener to enable
268      *  drag (gesturemovestart). If false, the method unbinds gesturemove
269      *  listeners for drag support.
270      * @private
271      */
272     _bindDrag: function (drag) {
273         var sv = this,
274             bb = sv._bb;
276         // Unbind any previous 'drag' listeners
277         bb.detach(DRAG + '|*');
279         if (drag) {
280             bb.on(DRAG + '|' + GESTURE_MOVE + START, Y.bind(sv._onGestureMoveStart, sv));
281         }
282     },
284     /**
285      * Bind (or unbind) flick listeners.
286      *
287      * @method _bindFlick
288      * @param flick {Object|boolean} If truthy, the method binds listeners for
289      *  flick support. If false, the method unbinds flick listeners.
290      * @private
291      */
292     _bindFlick: function (flick) {
293         var sv = this,
294             bb = sv._bb;
296         // Unbind any previous 'flick' listeners
297         bb.detach(FLICK + '|*');
299         if (flick) {
300             bb.on(FLICK + '|' + FLICK, Y.bind(sv._flick, sv), flick);
302             // Rebind Drag, becuase _onGestureMoveEnd always has to fire -after- _flick
303             sv._bindDrag(sv.get(DRAG));
304         }
305     },
307     /**
308      * Bind (or unbind) mousewheel listeners.
309      *
310      * @method _bindMousewheel
311      * @param mousewheel {Object|boolean} If truthy, the method binds listeners for
312      *  mousewheel support. If false, the method unbinds mousewheel listeners.
313      * @private
314      */
315     _bindMousewheel: function (mousewheel) {
316         var sv = this,
317             bb = sv._bb;
319         // Unbind any previous 'mousewheel' listeners
320         // TODO: This doesn't actually appear to work properly. Fix. #2532743
321         bb.detach(MOUSEWHEEL + '|*');
323         // Only enable for vertical scrollviews
324         if (mousewheel) {
325             // Bound to document, because that's where mousewheel events fire off of.
326             Y.one(DOCUMENT).on(MOUSEWHEEL, Y.bind(sv._mousewheel, sv));
327         }
328     },
330     /**
331      * syncUI implementation.
332      *
333      * Update the scroll position, based on the current value of scrollX/scrollY.
334      *
335      * @method syncUI
336      */
337     syncUI: function () {
338         var sv = this,
339             scrollDims = sv._getScrollDims(),
340             width = scrollDims.offsetWidth,
341             height = scrollDims.offsetHeight,
342             scrollWidth = scrollDims.scrollWidth,
343             scrollHeight = scrollDims.scrollHeight;
345         // If the axis is undefined, auto-calculate it
346         if (sv._cAxis === undefined) {
347             // This should only ever be run once (for now).
348             // In the future SV might post-load axis changes
349             sv._cAxis = {
350                 x: (scrollWidth > width),
351                 y: (scrollHeight > height)
352             };
354             sv._set(AXIS, sv._cAxis);
355         }
357         // get text direction on or inherited by scrollview node
358         sv.rtl = (sv._cb.getComputedStyle('direction') === 'rtl');
360         // Cache the disabled value
361         sv._cDisabled = sv.get(DISABLED);
363         // Run this to set initial values
364         sv._uiDimensionsChange();
366         // If we're out-of-bounds, snap back.
367         if (sv._isOutOfBounds()) {
368             sv._snapBack();
369         }
370     },
372     /**
373      * Utility method to obtain widget dimensions
374      *
375      * @method _getScrollDims
376      * @return {Object} The offsetWidth, offsetHeight, scrollWidth and
377      *  scrollHeight as an array: [offsetWidth, offsetHeight, scrollWidth,
378      *  scrollHeight]
379      * @private
380      */
381     _getScrollDims: function () {
382         var sv = this,
383             cb = sv._cb,
384             bb = sv._bb,
385             TRANS = ScrollView._TRANSITION,
386             // Ideally using CSSMatrix - don't think we have it normalized yet though.
387             // origX = (new WebKitCSSMatrix(cb.getComputedStyle("transform"))).e,
388             // origY = (new WebKitCSSMatrix(cb.getComputedStyle("transform"))).f,
389             origX = sv.get(SCROLL_X),
390             origY = sv.get(SCROLL_Y),
391             origHWTransform,
392             dims;
394         // TODO: Is this OK? Just in case it's called 'during' a transition.
395         if (NATIVE_TRANSITIONS) {
396             cb.setStyle(TRANS.DURATION, ZERO);
397             cb.setStyle(TRANS.PROPERTY, EMPTY);
398         }
400         origHWTransform = sv._forceHWTransforms;
401         sv._forceHWTransforms = false; // the z translation was causing issues with picking up accurate scrollWidths in Chrome/Mac.
403         sv._moveTo(cb, 0, 0);
404         dims = {
405             'offsetWidth': bb.get('offsetWidth'),
406             'offsetHeight': bb.get('offsetHeight'),
407             'scrollWidth': bb.get('scrollWidth'),
408             'scrollHeight': bb.get('scrollHeight')
409         };
410         sv._moveTo(cb, -(origX), -(origY));
412         sv._forceHWTransforms = origHWTransform;
414         return dims;
415     },
417     /**
418      * This method gets invoked whenever the height or width attributes change,
419      * allowing us to determine which scrolling axes need to be enabled.
420      *
421      * @method _uiDimensionsChange
422      * @protected
423      */
424     _uiDimensionsChange: function () {
425         var sv = this,
426             bb = sv._bb,
427             scrollDims = sv._getScrollDims(),
428             width = scrollDims.offsetWidth,
429             height = scrollDims.offsetHeight,
430             scrollWidth = scrollDims.scrollWidth,
431             scrollHeight = scrollDims.scrollHeight,
432             rtl = sv.rtl,
433             svAxis = sv._cAxis,
434             minScrollX = (rtl ? Math.min(0, -(scrollWidth - width)) : 0),
435             maxScrollX = (rtl ? 0 : Math.max(0, scrollWidth - width)),
436             minScrollY = 0,
437             maxScrollY = Math.max(0, scrollHeight - height);
439         if (svAxis && svAxis.x) {
440             bb.addClass(CLASS_NAMES.horizontal);
441         }
443         if (svAxis && svAxis.y) {
444             bb.addClass(CLASS_NAMES.vertical);
445         }
447         sv._setBounds({
448             minScrollX: minScrollX,
449             maxScrollX: maxScrollX,
450             minScrollY: minScrollY,
451             maxScrollY: maxScrollY
452         });
453     },
455     /**
456      * Set the bounding dimensions of the ScrollView
457      *
458      * @method _setBounds
459      * @protected
460      * @param bounds {Object} [duration] ms of the scroll animation. (default is 0)
461      *   @param {Number} [bounds.minScrollX] The minimum scroll X value
462      *   @param {Number} [bounds.maxScrollX] The maximum scroll X value
463      *   @param {Number} [bounds.minScrollY] The minimum scroll Y value
464      *   @param {Number} [bounds.maxScrollY] The maximum scroll Y value
465      */
466     _setBounds: function (bounds) {
467         var sv = this;
469         // TODO: Do a check to log if the bounds are invalid
471         sv._minScrollX = bounds.minScrollX;
472         sv._maxScrollX = bounds.maxScrollX;
473         sv._minScrollY = bounds.minScrollY;
474         sv._maxScrollY = bounds.maxScrollY;
475     },
477     /**
478      * Get the bounding dimensions of the ScrollView
479      *
480      * @method _getBounds
481      * @protected
482      */
483     _getBounds: function () {
484         var sv = this;
486         return {
487             minScrollX: sv._minScrollX,
488             maxScrollX: sv._maxScrollX,
489             minScrollY: sv._minScrollY,
490             maxScrollY: sv._maxScrollY
491         };
493     },
495     /**
496      * Scroll the element to a given xy coordinate
497      *
498      * @method scrollTo
499      * @param x {Number} The x-position to scroll to. (null for no movement)
500      * @param y {Number} The y-position to scroll to. (null for no movement)
501      * @param {Number} [duration] ms of the scroll animation. (default is 0)
502      * @param {String} [easing] An easing equation if duration is set. (default is `easing` attribute)
503      * @param {String} [node] The node to transform.  Setting this can be useful in
504      *  dual-axis paginated instances. (default is the instance's contentBox)
505      */
506     scrollTo: function (x, y, duration, easing, node) {
507         // Check to see if widget is disabled
508         if (this._cDisabled) {
509             return;
510         }
512         var sv = this,
513             cb = sv._cb,
514             TRANS = ScrollView._TRANSITION,
515             callback = Y.bind(sv._onTransEnd, sv), // @Todo : cache this
516             newX = 0,
517             newY = 0,
518             transition = {},
519             transform;
521         // default the optional arguments
522         duration = duration || 0;
523         easing = easing || sv.get(EASING); // @TODO: Cache this
524         node = node || cb;
526         if (x !== null) {
527             sv.set(SCROLL_X, x, {src:UI});
528             newX = -(x);
529         }
531         if (y !== null) {
532             sv.set(SCROLL_Y, y, {src:UI});
533             newY = -(y);
534         }
536         transform = sv._transform(newX, newY);
538         if (NATIVE_TRANSITIONS) {
539             // ANDROID WORKAROUND - try and stop existing transition, before kicking off new one.
540             node.setStyle(TRANS.DURATION, ZERO).setStyle(TRANS.PROPERTY, EMPTY);
541         }
543         // Move
544         if (duration === 0) {
545             if (NATIVE_TRANSITIONS) {
546                 node.setStyle('transform', transform);
547             }
548             else {
549                 // TODO: If both set, batch them in the same update
550                 // Update: Nope, setStyles() just loops through each property and applies it.
551                 if (x !== null) {
552                     node.setStyle(LEFT, newX + PX);
553                 }
554                 if (y !== null) {
555                     node.setStyle(TOP, newY + PX);
556                 }
557             }
558         }
560         // Animate
561         else {
562             transition.easing = easing;
563             transition.duration = duration / 1000;
565             if (NATIVE_TRANSITIONS) {
566                 transition.transform = transform;
567             }
568             else {
569                 transition.left = newX + PX;
570                 transition.top = newY + PX;
571             }
573             node.transition(transition, callback);
574         }
575     },
577     /**
578      * Utility method, to create the translate transform string with the
579      * x, y translation amounts provided.
580      *
581      * @method _transform
582      * @param {Number} x Number of pixels to translate along the x axis
583      * @param {Number} y Number of pixels to translate along the y axis
584      * @private
585      */
586     _transform: function (x, y) {
587         // TODO: Would we be better off using a Matrix for this?
588         var prop = 'translate(' + x + 'px, ' + y + 'px)';
590         if (this._forceHWTransforms) {
591             prop += ' translateZ(0)';
592         }
594         return prop;
595     },
597     /**
598     * Utility method, to move the given element to the given xy position
599     *
600     * @method _moveTo
601     * @param node {Node} The node to move
602     * @param x {Number} The x-position to move to
603     * @param y {Number} The y-position to move to
604     * @private
605     */
606     _moveTo : function(node, x, y) {
607         if (NATIVE_TRANSITIONS) {
608             node.setStyle('transform', this._transform(x, y));
609         } else {
610             node.setStyle(LEFT, x + PX);
611             node.setStyle(TOP, y + PX);
612         }
613     },
616     /**
617      * Content box transition callback
618      *
619      * @method _onTransEnd
620      * @param {Event.Facade} e The event facade
621      * @private
622      */
623     _onTransEnd: function () {
624         var sv = this;
626         // If for some reason we're OOB, snapback
627         if (sv._isOutOfBounds()) {
628             sv._snapBack();
629         }
630         else {
631             /**
632              * Notification event fired at the end of a scroll transition
633              *
634              * @event scrollEnd
635              * @param e {EventFacade} The default event facade.
636              */
637             sv.fire(EV_SCROLL_END);
638         }
639     },
641     /**
642      * gesturemovestart event handler
643      *
644      * @method _onGestureMoveStart
645      * @param e {Event.Facade} The gesturemovestart event facade
646      * @private
647      */
648     _onGestureMoveStart: function (e) {
650         if (this._cDisabled) {
651             return false;
652         }
654         var sv = this,
655             bb = sv._bb,
656             currentX = sv.get(SCROLL_X),
657             currentY = sv.get(SCROLL_Y),
658             clientX = e.clientX,
659             clientY = e.clientY;
661         if (sv._prevent.start) {
662             e.preventDefault();
663         }
665         // if a flick animation is in progress, cancel it
666         if (sv._flickAnim) {
667             sv._cancelFlick();
668             sv._onTransEnd();
669         }
671         // Reset lastScrolledAmt
672         sv.lastScrolledAmt = 0;
674         // Stores data for this gesture cycle.  Cleaned up later
675         sv._gesture = {
677             // Will hold the axis value
678             axis: null,
680             // The current attribute values
681             startX: currentX,
682             startY: currentY,
684             // The X/Y coordinates where the event began
685             startClientX: clientX,
686             startClientY: clientY,
688             // The X/Y coordinates where the event will end
689             endClientX: null,
690             endClientY: null,
692             // The current delta of the event
693             deltaX: null,
694             deltaY: null,
696             // Will be populated for flicks
697             flick: null,
699             // Create some listeners for the rest of the gesture cycle
700             onGestureMove: bb.on(DRAG + '|' + GESTURE_MOVE, Y.bind(sv._onGestureMove, sv)),
702             // @TODO: Don't bind gestureMoveEnd if it's a Flick?
703             onGestureMoveEnd: bb.on(DRAG + '|' + GESTURE_MOVE + END, Y.bind(sv._onGestureMoveEnd, sv))
704         };
705     },
707     /**
708      * gesturemove event handler
709      *
710      * @method _onGestureMove
711      * @param e {Event.Facade} The gesturemove event facade
712      * @private
713      */
714     _onGestureMove: function (e) {
715         var sv = this,
716             gesture = sv._gesture,
717             svAxis = sv._cAxis,
718             svAxisX = svAxis.x,
719             svAxisY = svAxis.y,
720             startX = gesture.startX,
721             startY = gesture.startY,
722             startClientX = gesture.startClientX,
723             startClientY = gesture.startClientY,
724             clientX = e.clientX,
725             clientY = e.clientY;
727         if (sv._prevent.move) {
728             e.preventDefault();
729         }
731         gesture.deltaX = startClientX - clientX;
732         gesture.deltaY = startClientY - clientY;
734         // Determine if this is a vertical or horizontal movement
735         // @TODO: This is crude, but it works.  Investigate more intelligent ways to detect intent
736         if (gesture.axis === null) {
737             gesture.axis = (Math.abs(gesture.deltaX) > Math.abs(gesture.deltaY)) ? DIM_X : DIM_Y;
738         }
740         // Move X or Y.  @TODO: Move both if dualaxis.
741         if (gesture.axis === DIM_X && svAxisX) {
742             sv.set(SCROLL_X, startX + gesture.deltaX);
743         }
744         else if (gesture.axis === DIM_Y && svAxisY) {
745             sv.set(SCROLL_Y, startY + gesture.deltaY);
746         }
747     },
749     /**
750      * gesturemoveend event handler
751      *
752      * @method _onGestureMoveEnd
753      * @param e {Event.Facade} The gesturemoveend event facade
754      * @private
755      */
756     _onGestureMoveEnd: function (e) {
757         var sv = this,
758             gesture = sv._gesture,
759             flick = gesture.flick,
760             clientX = e.clientX,
761             clientY = e.clientY,
762             isOOB;
764         if (sv._prevent.end) {
765             e.preventDefault();
766         }
768         // Store the end X/Y coordinates
769         gesture.endClientX = clientX;
770         gesture.endClientY = clientY;
772         // Cleanup the event handlers
773         gesture.onGestureMove.detach();
774         gesture.onGestureMoveEnd.detach();
776         // If this wasn't a flick, wrap up the gesture cycle
777         if (!flick) {
778             // @TODO: Be more intelligent about this. Look at the Flick attribute to see
779             // if it is safe to assume _flick did or didn't fire.
780             // Then, the order _flick and _onGestureMoveEnd fire doesn't matter?
782             // If there was movement (_onGestureMove fired)
783             if (gesture.deltaX !== null && gesture.deltaY !== null) {
785                 isOOB = sv._isOutOfBounds();
787                 // If we're out-out-bounds, then snapback
788                 if (isOOB) {
789                     sv._snapBack();
790                 }
792                 // Inbounds
793                 else {
794                     // Fire scrollEnd unless this is a paginated instance and the gesture axis is the same as paginator's
795                     // Not totally confident this is ideal to access a plugin's properties from a host, @TODO revisit
796                     if (!sv.pages || (sv.pages && !sv.pages.get(AXIS)[gesture.axis])) {
797                         sv._onTransEnd();
798                     }
799                 }
800             }
801         }
802     },
804     /**
805      * Execute a flick at the end of a scroll action
806      *
807      * @method _flick
808      * @param e {Event.Facade} The Flick event facade
809      * @private
810      */
811     _flick: function (e) {
812         if (this._cDisabled) {
813             return false;
814         }
816         var sv = this,
817             svAxis = sv._cAxis,
818             flick = e.flick,
819             flickAxis = flick.axis,
820             flickVelocity = flick.velocity,
821             axisAttr = flickAxis === DIM_X ? SCROLL_X : SCROLL_Y,
822             startPosition = sv.get(axisAttr);
824         // Sometimes flick is enabled, but drag is disabled
825         if (sv._gesture) {
826             sv._gesture.flick = flick;
827         }
829         // Prevent unneccesary firing of _flickFrame if we can't scroll on the flick axis
830         if (svAxis[flickAxis]) {
831             sv._flickFrame(flickVelocity, flickAxis, startPosition);
832         }
833     },
835     /**
836      * Execute a single frame in the flick animation
837      *
838      * @method _flickFrame
839      * @param velocity {Number} The velocity of this animated frame
840      * @param flickAxis {String} The axis on which to animate
841      * @param startPosition {Number} The starting X/Y point to flick from
842      * @protected
843      */
844     _flickFrame: function (velocity, flickAxis, startPosition) {
846         var sv = this,
847             axisAttr = flickAxis === DIM_X ? SCROLL_X : SCROLL_Y,
848             bounds = sv._getBounds(),
850             // Localize cached values
851             bounce = sv._cBounce,
852             bounceRange = sv._cBounceRange,
853             deceleration = sv._cDeceleration,
854             frameDuration = sv._cFrameDuration,
856             // Calculate
857             newVelocity = velocity * deceleration,
858             newPosition = startPosition - (frameDuration * newVelocity),
860             // Some convinience conditions
861             min = flickAxis === DIM_X ? bounds.minScrollX : bounds.minScrollY,
862             max = flickAxis === DIM_X ? bounds.maxScrollX : bounds.maxScrollY,
863             belowMin       = (newPosition < min),
864             belowMax       = (newPosition < max),
865             aboveMin       = (newPosition > min),
866             aboveMax       = (newPosition > max),
867             belowMinRange  = (newPosition < (min - bounceRange)),
868             withinMinRange = (belowMin && (newPosition > (min - bounceRange))),
869             withinMaxRange = (aboveMax && (newPosition < (max + bounceRange))),
870             aboveMaxRange  = (newPosition > (max + bounceRange)),
871             tooSlow;
873         // If we're within the range but outside min/max, dampen the velocity
874         if (withinMinRange || withinMaxRange) {
875             newVelocity *= bounce;
876         }
878         // Is the velocity too slow to bother?
879         tooSlow = (Math.abs(newVelocity).toFixed(4) < 0.015);
881         // If the velocity is too slow or we're outside the range
882         if (tooSlow || belowMinRange || aboveMaxRange) {
883             // Cancel and delete sv._flickAnim
884             if (sv._flickAnim) {
885                 sv._cancelFlick();
886             }
888             // If we're inside the scroll area, just end
889             if (aboveMin && belowMax) {
890                 sv._onTransEnd();
891             }
893             // We're outside the scroll area, so we need to snap back
894             else {
895                 sv._snapBack();
896             }
897         }
899         // Otherwise, animate to the next frame
900         else {
901             // @TODO: maybe use requestAnimationFrame instead
902             sv._flickAnim = Y.later(frameDuration, sv, '_flickFrame', [newVelocity, flickAxis, newPosition]);
903             sv.set(axisAttr, newPosition);
904         }
905     },
907     _cancelFlick: function () {
908         var sv = this;
910         if (sv._flickAnim) {
911             // Cancel the flick (if it exists)
912             sv._flickAnim.cancel();
914             // Also delete it, otherwise _onGestureMoveStart will think we're still flicking
915             delete sv._flickAnim;
916         }
918     },
920     /**
921      * Handle mousewheel events on the widget
922      *
923      * @method _mousewheel
924      * @param e {Event.Facade} The mousewheel event facade
925      * @private
926      */
927     _mousewheel: function (e) {
928         var sv = this,
929             scrollY = sv.get(SCROLL_Y),
930             bounds = sv._getBounds(),
931             bb = sv._bb,
932             scrollOffset = 10, // 10px
933             isForward = (e.wheelDelta > 0),
934             scrollToY = scrollY - ((isForward ? 1 : -1) * scrollOffset);
936         scrollToY = _constrain(scrollToY, bounds.minScrollY, bounds.maxScrollY);
938         // Because Mousewheel events fire off 'document', every ScrollView widget will react
939         // to any mousewheel anywhere on the page. This check will ensure that the mouse is currently
940         // over this specific ScrollView.  Also, only allow mousewheel scrolling on Y-axis,
941         // becuase otherwise the 'prevent' will block page scrolling.
942         if (bb.contains(e.target) && sv._cAxis[DIM_Y]) {
944             // Reset lastScrolledAmt
945             sv.lastScrolledAmt = 0;
947             // Jump to the new offset
948             sv.set(SCROLL_Y, scrollToY);
950             // if we have scrollbars plugin, update & set the flash timer on the scrollbar
951             // @TODO: This probably shouldn't be in this module
952             if (sv.scrollbars) {
953                 // @TODO: The scrollbars should handle this themselves
954                 sv.scrollbars._update();
955                 sv.scrollbars.flash();
956                 // or just this
957                 // sv.scrollbars._hostDimensionsChange();
958             }
960             // Fire the 'scrollEnd' event
961             sv._onTransEnd();
963             // prevent browser default behavior on mouse scroll
964             e.preventDefault();
965         }
966     },
968     /**
969      * Checks to see the current scrollX/scrollY position beyond the min/max boundary
970      *
971      * @method _isOutOfBounds
972      * @param x {Number} [optional] The X position to check
973      * @param y {Number} [optional] The Y position to check
974      * @return {boolen} Whether the current X/Y position is out of bounds (true) or not (false)
975      * @private
976      */
977     _isOutOfBounds: function (x, y) {
978         var sv = this,
979             svAxis = sv._cAxis,
980             svAxisX = svAxis.x,
981             svAxisY = svAxis.y,
982             currentX = x || sv.get(SCROLL_X),
983             currentY = y || sv.get(SCROLL_Y),
984             bounds = sv._getBounds(),
985             minX = bounds.minScrollX,
986             minY = bounds.minScrollY,
987             maxX = bounds.maxScrollX,
988             maxY = bounds.maxScrollY;
990         return (svAxisX && (currentX < minX || currentX > maxX)) || (svAxisY && (currentY < minY || currentY > maxY));
991     },
993     /**
994      * Bounces back
995      * @TODO: Should be more generalized and support both X and Y detection
996      *
997      * @method _snapBack
998      * @private
999      */
1000     _snapBack: function () {
1001         var sv = this,
1002             currentX = sv.get(SCROLL_X),
1003             currentY = sv.get(SCROLL_Y),
1004             bounds = sv._getBounds(),
1005             minX = bounds.minScrollX,
1006             minY = bounds.minScrollY,
1007             maxX = bounds.maxScrollX,
1008             maxY = bounds.maxScrollY,
1009             newY = _constrain(currentY, minY, maxY),
1010             newX = _constrain(currentX, minX, maxX),
1011             duration = sv.get(SNAP_DURATION),
1012             easing = sv.get(SNAP_EASING);
1014         if (newX !== currentX) {
1015             sv.set(SCROLL_X, newX, {duration:duration, easing:easing});
1016         }
1017         else if (newY !== currentY) {
1018             sv.set(SCROLL_Y, newY, {duration:duration, easing:easing});
1019         }
1020         else {
1021             sv._onTransEnd();
1022         }
1023     },
1025     /**
1026      * After listener for changes to the scrollX or scrollY attribute
1027      *
1028      * @method _afterScrollChange
1029      * @param e {Event.Facade} The event facade
1030      * @protected
1031      */
1032     _afterScrollChange: function (e) {
1033         if (e.src === ScrollView.UI_SRC) {
1034             return false;
1035         }
1037         var sv = this,
1038             duration = e.duration,
1039             easing = e.easing,
1040             val = e.newVal,
1041             scrollToArgs = [];
1043         // Set the scrolled value
1044         sv.lastScrolledAmt = sv.lastScrolledAmt + (e.newVal - e.prevVal);
1046         // Generate the array of args to pass to scrollTo()
1047         if (e.attrName === SCROLL_X) {
1048             scrollToArgs.push(val);
1049             scrollToArgs.push(sv.get(SCROLL_Y));
1050         }
1051         else {
1052             scrollToArgs.push(sv.get(SCROLL_X));
1053             scrollToArgs.push(val);
1054         }
1056         scrollToArgs.push(duration);
1057         scrollToArgs.push(easing);
1059         sv.scrollTo.apply(sv, scrollToArgs);
1060     },
1062     /**
1063      * After listener for changes to the flick attribute
1064      *
1065      * @method _afterFlickChange
1066      * @param e {Event.Facade} The event facade
1067      * @protected
1068      */
1069     _afterFlickChange: function (e) {
1070         this._bindFlick(e.newVal);
1071     },
1073     /**
1074      * After listener for changes to the disabled attribute
1075      *
1076      * @method _afterDisabledChange
1077      * @param e {Event.Facade} The event facade
1078      * @protected
1079      */
1080     _afterDisabledChange: function (e) {
1081         // Cache for performance - we check during move
1082         this._cDisabled = e.newVal;
1083     },
1085     /**
1086      * After listener for the axis attribute
1087      *
1088      * @method _afterAxisChange
1089      * @param e {Event.Facade} The event facade
1090      * @protected
1091      */
1092     _afterAxisChange: function (e) {
1093         this._cAxis = e.newVal;
1094     },
1096     /**
1097      * After listener for changes to the drag attribute
1098      *
1099      * @method _afterDragChange
1100      * @param e {Event.Facade} The event facade
1101      * @protected
1102      */
1103     _afterDragChange: function (e) {
1104         this._bindDrag(e.newVal);
1105     },
1107     /**
1108      * After listener for the height or width attribute
1109      *
1110      * @method _afterDimChange
1111      * @param e {Event.Facade} The event facade
1112      * @protected
1113      */
1114     _afterDimChange: function () {
1115         this._uiDimensionsChange();
1116     },
1118     /**
1119      * After listener for scrollEnd, for cleanup
1120      *
1121      * @method _afterScrollEnd
1122      * @param e {Event.Facade} The event facade
1123      * @protected
1124      */
1125     _afterScrollEnd: function () {
1126         var sv = this;
1128         if (sv._flickAnim) {
1129             sv._cancelFlick();
1130         }
1132         // Ideally this should be removed, but doing so causing some JS errors with fast swiping
1133         // because _gesture is being deleted after the previous one has been overwritten
1134         // delete sv._gesture; // TODO: Move to sv.prevGesture?
1135     },
1137     /**
1138      * Setter for 'axis' attribute
1139      *
1140      * @method _axisSetter
1141      * @param val {Mixed} A string ('x', 'y', 'xy') to specify which axis/axes to allow scrolling on
1142      * @param name {String} The attribute name
1143      * @return {Object} An object to specify scrollability on the x & y axes
1144      *
1145      * @protected
1146      */
1147     _axisSetter: function (val) {
1149         // Turn a string into an axis object
1150         if (Y.Lang.isString(val)) {
1151             return {
1152                 x: val.match(/x/i) ? true : false,
1153                 y: val.match(/y/i) ? true : false
1154             };
1155         }
1156     },
1158     /**
1159     * The scrollX, scrollY setter implementation
1160     *
1161     * @method _setScroll
1162     * @private
1163     * @param {Number} val
1164     * @param {String} dim
1165     *
1166     * @return {Number} The value
1167     */
1168     _setScroll : function(val) {
1170         // Just ensure the widget is not disabled
1171         if (this._cDisabled) {
1172             val = Y.Attribute.INVALID_VALUE;
1173         }
1175         return val;
1176     },
1178     /**
1179     * Setter for the scrollX attribute
1180     *
1181     * @method _setScrollX
1182     * @param val {Number} The new scrollX value
1183     * @return {Number} The normalized value
1184     * @protected
1185     */
1186     _setScrollX: function(val) {
1187         return this._setScroll(val, DIM_X);
1188     },
1190     /**
1191     * Setter for the scrollY ATTR
1192     *
1193     * @method _setScrollY
1194     * @param val {Number} The new scrollY value
1195     * @return {Number} The normalized value
1196     * @protected
1197     */
1198     _setScrollY: function(val) {
1199         return this._setScroll(val, DIM_Y);
1200     }
1202     // End prototype properties
1204 }, {
1206     // Static properties
1208     /**
1209      * The identity of the widget.
1210      *
1211      * @property NAME
1212      * @type String
1213      * @default 'scrollview'
1214      * @readOnly
1215      * @protected
1216      * @static
1217      */
1218     NAME: 'scrollview',
1220     /**
1221      * Static property used to define the default attribute configuration of
1222      * the Widget.
1223      *
1224      * @property ATTRS
1225      * @type {Object}
1226      * @protected
1227      * @static
1228      */
1229     ATTRS: {
1231         /**
1232          * Specifies ability to scroll on x, y, or x and y axis/axes.
1233          *
1234          * @attribute axis
1235          * @type String
1236          */
1237         axis: {
1238             setter: '_axisSetter',
1239             writeOnce: 'initOnly'
1240         },
1242         /**
1243          * The current scroll position in the x-axis
1244          *
1245          * @attribute scrollX
1246          * @type Number
1247          * @default 0
1248          */
1249         scrollX: {
1250             value: 0,
1251             setter: '_setScrollX'
1252         },
1254         /**
1255          * The current scroll position in the y-axis
1256          *
1257          * @attribute scrollY
1258          * @type Number
1259          * @default 0
1260          */
1261         scrollY: {
1262             value: 0,
1263             setter: '_setScrollY'
1264         },
1266         /**
1267          * Drag coefficent for inertial scrolling. The closer to 1 this
1268          * value is, the less friction during scrolling.
1269          *
1270          * @attribute deceleration
1271          * @default 0.93
1272          */
1273         deceleration: {
1274             value: 0.93
1275         },
1277         /**
1278          * Drag coefficient for intertial scrolling at the upper
1279          * and lower boundaries of the scrollview. Set to 0 to
1280          * disable "rubber-banding".
1281          *
1282          * @attribute bounce
1283          * @type Number
1284          * @default 0.1
1285          */
1286         bounce: {
1287             value: 0.1
1288         },
1290         /**
1291          * The minimum distance and/or velocity which define a flick. Can be set to false,
1292          * to disable flick support (note: drag support is enabled/disabled separately)
1293          *
1294          * @attribute flick
1295          * @type Object
1296          * @default Object with properties minDistance = 10, minVelocity = 0.3.
1297          */
1298         flick: {
1299             value: {
1300                 minDistance: 10,
1301                 minVelocity: 0.3
1302             }
1303         },
1305         /**
1306          * Enable/Disable dragging the ScrollView content (note: flick support is enabled/disabled separately)
1307          * @attribute drag
1308          * @type boolean
1309          * @default true
1310          */
1311         drag: {
1312             value: true
1313         },
1315         /**
1316          * The default duration to use when animating the bounce snap back.
1317          *
1318          * @attribute snapDuration
1319          * @type Number
1320          * @default 400
1321          */
1322         snapDuration: {
1323             value: 400
1324         },
1326         /**
1327          * The default easing to use when animating the bounce snap back.
1328          *
1329          * @attribute snapEasing
1330          * @type String
1331          * @default 'ease-out'
1332          */
1333         snapEasing: {
1334             value: 'ease-out'
1335         },
1337         /**
1338          * The default easing used when animating the flick
1339          *
1340          * @attribute easing
1341          * @type String
1342          * @default 'cubic-bezier(0, 0.1, 0, 1.0)'
1343          */
1344         easing: {
1345             value: 'cubic-bezier(0, 0.1, 0, 1.0)'
1346         },
1348         /**
1349          * The interval (ms) used when animating the flick for JS-timer animations
1350          *
1351          * @attribute frameDuration
1352          * @type Number
1353          * @default 15
1354          */
1355         frameDuration: {
1356             value: 15
1357         },
1359         /**
1360          * The default bounce distance in pixels
1361          *
1362          * @attribute bounceRange
1363          * @type Number
1364          * @default 150
1365          */
1366         bounceRange: {
1367             value: 150
1368         }
1369     },
1371     /**
1372      * List of class names used in the scrollview's DOM
1373      *
1374      * @property CLASS_NAMES
1375      * @type Object
1376      * @static
1377      */
1378     CLASS_NAMES: CLASS_NAMES,
1380     /**
1381      * Flag used to source property changes initiated from the DOM
1382      *
1383      * @property UI_SRC
1384      * @type String
1385      * @static
1386      * @default 'ui'
1387      */
1388     UI_SRC: UI,
1390     /**
1391      * Object map of style property names used to set transition properties.
1392      * Defaults to the vendor prefix established by the Transition module.
1393      * The configured property names are `_TRANSITION.DURATION` (e.g. "WebkitTransitionDuration") and
1394      * `_TRANSITION.PROPERTY (e.g. "WebkitTransitionProperty").
1395      *
1396      * @property _TRANSITION
1397      * @private
1398      */
1399     _TRANSITION: {
1400         DURATION: (vendorPrefix ? vendorPrefix + 'TransitionDuration' : 'transitionDuration'),
1401         PROPERTY: (vendorPrefix ? vendorPrefix + 'TransitionProperty' : 'transitionProperty')
1402     },
1404     /**
1405      * The default bounce distance in pixels
1406      *
1407      * @property BOUNCE_RANGE
1408      * @type Number
1409      * @static
1410      * @default false
1411      * @deprecated (in 3.7.0)
1412      */
1413     BOUNCE_RANGE: false,
1415     /**
1416      * The interval (ms) used when animating the flick
1417      *
1418      * @property FRAME_STEP
1419      * @type Number
1420      * @static
1421      * @default false
1422      * @deprecated (in 3.7.0)
1423      */
1424     FRAME_STEP: false,
1426     /**
1427      * The default easing used when animating the flick
1428      *
1429      * @property EASING
1430      * @type String
1431      * @static
1432      * @default false
1433      * @deprecated (in 3.7.0)
1434      */
1435     EASING: false,
1437     /**
1438      * The default easing to use when animating the bounce snap back.
1439      *
1440      * @property SNAP_EASING
1441      * @type String
1442      * @static
1443      * @default false
1444      * @deprecated (in 3.7.0)
1445      */
1446     SNAP_EASING: false,
1448     /**
1449      * The default duration to use when animating the bounce snap back.
1450      *
1451      * @property SNAP_DURATION
1452      * @type Number
1453      * @static
1454      * @default false
1455      * @deprecated (in 3.7.0)
1456      */
1457     SNAP_DURATION: false
1459     // End static properties
1464 }, '3.13.0', {"requires": ["widget", "event-gestures", "event-mousewheel", "transition"], "skinnable": true});