2
Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3
Code licensed under the BSD License:
4
http://developer.yahoo.com/yui/license.html
8
YUI.add('scrollview-base', function(Y) {
11
* The scrollview-base module provides a basic ScrollView Widget, without scrollbar indicators
13
* @module scrollview-base
16
var getClassName = Y.ClassNameManager.getClassName,
17
SCROLLVIEW = 'scrollview',
19
vertical: getClassName(SCROLLVIEW, 'vert'),
20
horizontal: getClassName(SCROLLVIEW, 'horiz')
22
EV_SCROLL_END = 'scrollEnd',
23
EV_SCROLL_FLICK = 'flick',
25
FLICK = EV_SCROLL_FLICK,
41
BOUNDING_BOX = "boundingBox",
42
CONTENT_BOX = "contentBox",
47
OWNER_DOC = "ownerDocument",
52
NATIVE_TRANSITIONS = Y.Transition.useNative;
54
Y.Node.DOM_EVENTS.DOMSubtreeModified = true;
57
* ScrollView provides a scrollable widget, supporting flick gestures, across both touch and mouse based devices.
61
* @param config {Object} Object literal with initial attribute values
65
function ScrollView() {
66
ScrollView.superclass.constructor.apply(this, arguments);
69
Y.ScrollView = Y.extend(ScrollView, Y.Widget, {
71
// Y.ScrollView prototype
74
* Designated initializer
78
initializer: function() {
81
// Cache - they're write once, and not going to change
82
this._cb = this.get(CONTENT_BOX);
83
this._bb = this.get(BOUNDING_BOX);
87
* Publishes events which occur during the scroll lifecycle
89
* @method _createEvents
92
_createEvents: function() {
94
* Notification event fired at the end of a scroll transition
97
* @param e {EventFacade} The default event facade.
99
this.publish(EV_SCROLL_END);
102
* Notification event fired at the end of a flick gesture (the flick animation may still be in progress)
105
* @param e {EventFacade} The default event facade.
107
this.publish(EV_SCROLL_FLICK);
111
* Override the contentBox sizing method, since the contentBox height
112
* should not be that of the boundingBox.
117
_uiSizeCB: function() {},
120
* Content box transition callback
122
* @method _transitionEnded
123
* @param {Event.Facade} e The event facade
126
_transitionEnded: function(e) {
127
this.fire(EV_SCROLL_END);
131
* bindUI implementation
133
* Hooks up events for the widget
140
flick = this.get(FLICK);
142
bb.on('gesturemovestart', Y.bind(this._onGestureMoveStart, this));
144
// IE SELECT HACK. See if we can do this non-natively and in the gesture for a future release.
146
this._nativeBody = Y.Node.getDOMNode(Y.one("body", cb.get("ownerDocument")));
147
this._cbDoc = cb.get(OWNER_DOC);
149
cb.on("mousedown", function() {
150
this._selectstart = this._nativeBody.onselectstart;
151
this._nativeBody.onselectstart = this._iePreventSelect;
152
this._cbDoc.once(MOUSE_UP, this._ieRestoreSelect, this);
156
// TODO: Fires way to often when using non-native transitions, due to property change
157
if (NATIVE_TRANSITIONS) {
158
cb.on('DOMSubtreeModified', Y.bind(this._uiDimensionsChange, this));
162
cb.on("flick", Y.bind(this._flick, this), flick);
166
'scrollYChange' : this._afterScrollYChange,
167
'scrollXChange' : this._afterScrollXChange,
168
'heightChange' : this._afterHeightChange,
169
'widthChange' : this._afterWidthChange,
170
'renderedChange': function() { Y.later(0, this, '_uiDimensionsChange'); }
175
* syncUI implementation
177
* Update the scroll position, based on the current value of scrollY
181
this.scrollTo(this.get(SCROLL_X), this.get(SCROLL_Y));
185
* Scroll the element to a given y coordinate
188
* @param x {Number} The x-position to scroll to
189
* @param y {Number} The y-position to scroll to
190
* @param duration {Number} Duration, in ms, of the scroll animation (default is 0)
191
* @param easing {String} An easing equation if duration is set
193
scrollTo: function(x, y, duration, easing) {
198
xMove = (xSet) ? x * -1 : 0,
199
yMove = (ySet) ? y * -1 : 0,
201
callback = this._transEndCallback;
203
duration = duration || 0;
204
easing = easing || ScrollView.EASING;
207
this.set(SCROLL_X, x, { src: UI });
211
this.set(SCROLL_Y, y, { src: UI });
214
if (NATIVE_TRANSITIONS) {
215
// ANDROID WORKAROUND - try and stop existing transition, before kicking off new one.
216
cb.setStyle(ScrollView._TRANSITION_DURATION, ZERO);
217
cb.setStyle(ScrollView._TRANSITION_PROPERTY, EMPTY);
219
// Causes bounce back from 0,0 instead of current translation for bottom/right edge animation
220
// cb.setStyle("WebkitTransform", cb.getComputedStyle("WebkitTransform"));
223
if (duration !== 0) {
227
duration : duration/1000
230
if (NATIVE_TRANSITIONS) {
231
transition.transform = 'translate3D('+ xMove +'px,'+ yMove +'px, 0px)';
233
if (xSet) { transition.left = xMove + PX; }
234
if (ySet) { transition.top = yMove + PX; }
237
Y.log("Transition: duration, easing:" + [transition.duration, transition.easing], "scrollview");
240
callback = this._transEndCallback = Y.bind(this._transitionEnded, this);
243
cb.transition(transition, callback);
246
if (NATIVE_TRANSITIONS) {
247
cb.setStyle('transform', 'translate3D('+ xMove +'px,'+ yMove +'px, 0px)');
249
if (xSet) { cb.setStyle(LEFT, xMove + PX); }
250
if (ySet) { cb.setStyle(TOP, yMove + PX); }
256
* Native onselectstart handle to prevent selection in IE
258
* @method _iePreventSelect
261
_iePreventSelect : function() {
266
* Restores native onselectstart handle, backed up to prevent selection in IE
268
* @method _ieRestoreSelect
271
_ieRestoreSelect : function() {
272
this._nativeBody.onselectstart = this._selectstart;
275
_preventStart : false,
282
* gesturemovestart event handler
284
* @method _onGestureMoveStart
285
* @param e {Event.Facade} The gesturemovestart event facade
288
_onGestureMoveStart: function(e) {
292
if (this._preventStart) {
298
this._moveEvt = bb.on('gesturemove', Y.bind(this._onGestureMove, this));
299
this._moveEndEvt = bb.on('gesturemoveend', Y.bind(this._onGestureMoveEnd, this));
301
this._moveStartY = e.clientY + this.get(SCROLL_Y);
302
this._moveStartX = e.clientX + this.get(SCROLL_X);
304
this._moveStartTime = (new Date()).getTime();
305
this._moveStartClientY = this._moveEndClientY = e.clientY;
306
this._moveStartClientX = this._moveEndClientX = e.clientX;
309
* Internal state, defines whether or not the scrollview is currently being dragged
311
* @property _isDragging
315
this._isDragging = false;
318
* Internal state, defines whether or not the scrollview is currently animating a flick
320
* @property _flicking
324
this._flicking = false;
327
* Internal state, defines whether or not the scrollview needs to snap to a boundary edge
329
* @property _snapToEdge
333
this._snapToEdge = false;
337
* gesturemove event handler
339
* @method _onGestureMove
340
* @param e {Event.Facade} The gesturemove event facade
343
_onGestureMove: function(e) {
345
if (this._preventMove) {
349
this._isDragging = true;
350
this._moveEndClientY = e.clientY;
351
this._moveEndClientX = e.clientX;
352
this._lastMoved = (new Date()).getTime();
354
if(this._scrollsVertical) {
355
this.set(SCROLL_Y, -(e.clientY - this._moveStartY));
358
if(this._scrollsHorizontal) {
359
this.set(SCROLL_X, -(e.clientX - this._moveStartX));
364
* gestureend event handler
366
* @method _onGestureMoveEnd
367
* @param e {Event.Facade} The gesturemoveend event facade
370
_onGestureMoveEnd: function(e) {
372
if (this._preventEnd) {
376
var minY = this._minScrollY,
377
maxY = this._maxScrollY,
378
minX = this._minScrollX,
379
maxX = this._maxScrollX,
380
startPoint = this._scrollsVertical ? this._moveStartClientY : this._moveStartClientX,
381
endPoint = this._scrollsVertical ? this._moveEndClientY : this._moveEndClientX,
382
distance = startPoint - endPoint;
384
this._moveEvt.detach();
385
this._moveEndEvt.detach();
388
* Internal state, defines whether or not the scrollview has been scrolled half it's width/height
390
* @property _scrolledHalfway
394
this._scrolledHalfway = false;
395
this._snapToEdge = false;
396
this._isDragging = false;
399
* Contains the distance (postive or negative) in pixels by which the scrollview was last scrolled. This is useful when
400
* setting up click listeners on the scrollview content, which on mouse based devices are always fired, even after a
403
* <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,
404
* if working in a purely touch based environment</p>
406
* @property lastScrolledAmt
410
this.lastScrolledAmt = distance;
412
if(this._scrollsHorizontal && Math.abs(distance) > (this.get('width')/2)) {
413
this._scrolledHalfway = true;
416
* Internal state, defines whether or not the scrollview has been scrolled in the forward (distance > 0), or backward (distance < 0) direction
418
* @property _scrolledForward
422
this._scrolledForward = distance > 0;
425
if(this._scrollsVertical && Math.abs(distance) > (this.get('height')/2)) {
426
this._scrolledHalfway = true;
427
this._scrolledForward = distance > 0;
431
if(this._scrollsVertical && this.get(SCROLL_Y) < minY) {
432
this._snapToEdge = true;
433
this.set(SCROLL_Y, minY);
437
if(this._scrollsHorizontal && this.get(SCROLL_X) < minX) {
438
this._snapToEdge = true;
439
this.set(SCROLL_X, minX);
443
if(this.get(SCROLL_Y) > maxY) {
444
this._snapToEdge = true;
445
this.set(SCROLL_Y, maxY);
449
if(this.get(SCROLL_X) > maxX) {
450
this._snapToEdge = true;
451
this.set(SCROLL_X, maxX);
454
Y.log("half:" + this._scrolledHalfway + ", fwd:" + this._scrolledForward, "scrollview");
456
if(this._snapToEdge) {
460
this.fire(EV_SCROLL_END, {
461
onGestureMoveEnd: true
468
* After listener for changes to the scrollY attribute
470
* @method _afterScrollYChange
471
* @param e {Event.Facade} The event facade
474
_afterScrollYChange : function(e) {
476
this._uiScrollY(e.newVal, e.duration, e.easing);
481
* Update the UI when the scrollY attribute changes
484
* @param val {Number} The scrollY value
485
* @param duration {Number} The length (in ms) of the scroll animation
486
* @param easing {String} An easing equation, if duration is defined
489
_uiScrollY : function(val, duration, easing) {
490
duration = duration || this._snapToEdge ? 400 : 0;
491
easing = easing || this._snapToEdge ? ScrollView.SNAP_EASING : null;
493
this.scrollTo(null, val, duration, easing);
497
* After listener for changes to the scrollX attribute
499
* @method _afterScrollXChange
500
* @param e {Event.Facade} The event facade
503
_afterScrollXChange : function(e) {
505
this._uiScrollX(e.newVal, e.duration, e.easing);
510
* Update the UI when the scrollX attribute changes
513
* @param val {Number} The scrollX value
514
* @param duration {Number} The length (in ms) of the scroll animation
515
* @param easing {String} An easing equation, if duration is defined
518
_uiScrollX : function(val, duration, easing) {
519
duration = duration || this._snapToEdge ? 400 : 0;
520
easing = easing || this._snapToEdge ? ScrollView.SNAP_EASING : null;
522
this.scrollTo(val, null, duration, easing);
526
* After listener for the height attribute
528
* @method _afterHeightChange
529
* @param e {Event.Facade} The event facade
532
_afterHeightChange: function() {
533
this._uiDimensionsChange();
537
* After listener for the width attribute
539
* @method _afterWidthChange
540
* @param e {Event.Facade} The event facade
543
_afterWidthChange: function() {
544
this._uiDimensionsChange();
548
* This method gets invoked whenever the height or width attributes change,
549
* allowing us to determine which scrolling axes need to be enabled.
551
* @method _uiDimensionsChange
554
_uiDimensionsChange: function() {
557
height = this.get('height'),
558
width = this.get('width'),
560
// Use bb instead of cb. cb doesn't gives us the right results
561
// in FF (due to overflow:hidden)
562
scrollHeight = bb.get('scrollHeight'),
563
scrollWidth = bb.get('scrollWidth');
565
if(height && scrollHeight > height) {
566
this._scrollsVertical = true;
567
this._maxScrollY = scrollHeight - height;
568
this._minScrollY = 0;
569
this._scrollHeight = scrollHeight;
570
bb.addClass(ScrollView.CLASS_NAMES.vertical);
573
if(width && scrollWidth > width) {
574
this._scrollsHorizontal = true;
575
this._maxScrollX = scrollWidth - width;
576
this._minScrollX = 0;
577
this._scrollWidth = scrollWidth;
578
bb.addClass(ScrollView.CLASS_NAMES.horizontal);
582
* Internal state, defines whether or not the scrollview can scroll vertically
584
* @property _scrollsVertical
590
* Internal state, defines the maximum amount that the scrollview can be scrolled along the Y axis
592
* @property _maxScrollY
598
* Internal state, defines the minimum amount that the scrollview can be scrolled along the Y axis
600
* @property _minScrollY
606
* Internal state, cached scrollHeight, for performance
608
* @property _scrollHeight
614
* Internal state, defines whether or not the scrollview can scroll horizontally
616
* @property _scrollsHorizontal
622
* Internal state, defines the maximum amount that the scrollview can be scrolled along the X axis
624
* @property _maxScrollX
630
* Internal state, defines the minimum amount that the scrollview can be scrolled along the X axis
632
* @property _minScrollX
638
* Internal state, cached scrollWidth, for performance
640
* @property _scrollWidth
647
* Execute a flick at the end of a scroll action
650
* @param distance {Number} The distance (in px) the user scrolled before the flick
651
* @param time {Number} The number of ms the scroll event lasted before the flick
654
_flick: function(e) {
658
* Internal state, currently calculated velocity from the flick
660
* @property _currentVelocity
664
this._currentVelocity = flick.velocity;
665
this._flicking = true;
667
this._decelCached = this.get('deceleration');
668
this._bounceCached = this.get('bounce');
670
this._pastYEdge = false;
671
this._pastXEdge = false;
675
this.fire(EV_SCROLL_FLICK);
679
* Execute a single frame in the flick animation
681
* @method _flickFrame
684
_flickFrame: function() {
691
scrollsVertical = this._scrollsVertical,
692
scrollsHorizontal = this._scrollsHorizontal,
693
deceleration = this._decelCached,
694
bounce = this._bounceCached,
695
step = ScrollView.FRAME_STEP;
697
if(scrollsVertical) {
698
maxY = this._maxScrollY;
699
minY = this._minScrollY;
700
newY = this.get(SCROLL_Y) - (this._currentVelocity * step);
703
if(scrollsHorizontal) {
704
maxX = this._maxScrollX;
705
minX = this._minScrollX;
706
newX = this.get(SCROLL_X) - (this._currentVelocity * step);
709
this._currentVelocity = (this._currentVelocity * deceleration);
711
if(Math.abs(this._currentVelocity).toFixed(4) <= 0.015) {
712
this._flicking = false;
713
this._killTimer(!(this._pastYEdge || this._pastXEdge));
715
if(scrollsVertical) {
717
this._snapToEdge = true;
718
this.set(SCROLL_Y, minY);
719
} else if(newY > maxY) {
720
this._snapToEdge = true;
721
this.set(SCROLL_Y, maxY);
725
if(scrollsHorizontal) {
727
this._snapToEdge = true;
728
this.set(SCROLL_X, minX);
729
} else if(newX > maxX) {
730
this._snapToEdge = true;
731
this.set(SCROLL_X, maxX);
738
if (scrollsVertical) {
739
if (newY < minY || newY > maxY) {
740
this._pastYEdge = true;
741
this._currentVelocity *= bounce;
744
this.set(SCROLL_Y, newY);
747
if (scrollsHorizontal) {
748
if (newX < minX || newX > maxX) {
749
this._pastXEdge = true;
750
this._currentVelocity *= bounce;
753
this.set(SCROLL_X, newX);
756
if (!this._flickTimer) {
757
this._flickTimer = Y.later(step, this, '_flickFrame', null, true);
762
* Stop the animation timer
765
* @param fireEvent {Boolean} If true, fire the scrollEnd event
768
_killTimer: function(fireEvent) {
769
if(this._flickTimer) {
770
this._flickTimer.cancel();
771
this._flickTimer = null;
775
this.fire(EV_SCROLL_END);
780
* The scrollX, scrollY setter implementation
784
* @param {Number} val
785
* @param {String} dim
787
* @return {Number} The constrained value, if it exceeds min/max range
789
_setScroll : function(val, dim) {
790
var bouncing = this._cachedBounce || this.get(BOUNCE),
791
range = ScrollView.BOUNCE_RANGE,
793
maxScroll = (dim == DIM_X) ? this._maxScrollX : this._maxScrollY,
795
min = bouncing ? -range : 0,
796
max = bouncing ? maxScroll + range : maxScroll;
798
if(!bouncing || !this._isDragging) {
801
} else if(val > max) {
810
* Setter for the scrollX attribute
812
* @method _setScrollX
813
* @param val {Number} The new scrollX value
814
* @return {Number} The normalized value
817
_setScrollX: function(val) {
818
return this._setScroll(val, DIM_X);
822
* Setter for the scrollY ATTR
824
* @method _setScrollY
825
* @param val {Number} The new scrollY value
826
* @return {Number} The normalized value
829
_setScrollY: function(val) {
830
return this._setScroll(val, DIM_Y);
835
// Y.ScrollView static properties
838
* The identity of the widget.
840
* @property ScrollView.NAME
842
* @default 'scrollview'
850
* Static property used to define the default attribute configuration of
853
* @property ScrollView.ATTRS
861
* The scroll position in the y-axis
869
setter: '_setScrollY'
873
* The scroll position in the x-axis
881
setter: '_setScrollX'
885
* Drag coefficent for inertial scrolling. The closer to 1 this
886
* value is, the less friction during scrolling.
888
* @attribute deceleration
896
* Drag coefficient for intertial scrolling at the upper
897
* and lower boundaries of the scrollview. Set to 0 to
898
* disable "rubber-banding".
909
* The minimum distance and/or velocity which define a flick
913
* @default Object with properties minDistance = 10, minVelocity = 0.3.
924
* List of class names used in the scrollview's DOM
926
* @property ScrollView.CLASS_NAMES
930
CLASS_NAMES: CLASS_NAMES,
933
* Flag used to source property changes initiated from the DOM
935
* @property ScrollView.UI_SRC
943
* The default bounce distance in pixels
945
* @property ScrollView.BOUNCE_RANGE
953
* The interval used when animating the flick
955
* @property ScrollView.FRAME_STEP
963
* The default easing used when animating the flick
965
* @property ScrollView.EASING
968
* @default 'cubic-bezier(0, 0.1, 0, 1.0)'
970
EASING : 'cubic-bezier(0, 0.1, 0, 1.0)',
973
* The default easing to use when animatiing the bounce snap back.
975
* @property ScrollView.SNAP_EASING
978
* @default 'ease-out'
980
SNAP_EASING : 'ease-out',
983
* Style property name to use to set transition duration. Currently Webkit specific (WebkitTransitionDuration)
985
* @property ScrollView._TRANSITION_DURATION
988
_TRANSITION_DURATION : "WebkitTransitionDuration",
991
* Style property name to use to set transition property. Currently, Webkit specific (WebkitTransitionProperty)
993
* @property ScrollView._TRANSITION_PROPERTY
996
_TRANSITION_PROPERTY : "WebkitTransitionProperty"
1000
}, '3.2.0' ,{skinnable:true, requires:['widget', 'event-gestures', 'transition']});