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; }
239
callback = this._transEndCallback = Y.bind(this._transitionEnded, this);
242
cb.transition(transition, callback);
245
if (NATIVE_TRANSITIONS) {
246
cb.setStyle('transform', 'translate3D('+ xMove +'px,'+ yMove +'px, 0px)');
248
if (xSet) { cb.setStyle(LEFT, xMove + PX); }
249
if (ySet) { cb.setStyle(TOP, yMove + PX); }
255
* Native onselectstart handle to prevent selection in IE
257
* @method _iePreventSelect
260
_iePreventSelect : function() {
265
* Restores native onselectstart handle, backed up to prevent selection in IE
267
* @method _ieRestoreSelect
270
_ieRestoreSelect : function() {
271
this._nativeBody.onselectstart = this._selectstart;
274
_preventStart : false,
281
* gesturemovestart event handler
283
* @method _onGestureMoveStart
284
* @param e {Event.Facade} The gesturemovestart event facade
287
_onGestureMoveStart: function(e) {
291
if (this._preventStart) {
297
this._moveEvt = bb.on('gesturemove', Y.bind(this._onGestureMove, this));
298
this._moveEndEvt = bb.on('gesturemoveend', Y.bind(this._onGestureMoveEnd, this));
300
this._moveStartY = e.clientY + this.get(SCROLL_Y);
301
this._moveStartX = e.clientX + this.get(SCROLL_X);
303
this._moveStartTime = (new Date()).getTime();
304
this._moveStartClientY = this._moveEndClientY = e.clientY;
305
this._moveStartClientX = this._moveEndClientX = e.clientX;
308
* Internal state, defines whether or not the scrollview is currently being dragged
310
* @property _isDragging
314
this._isDragging = false;
317
* Internal state, defines whether or not the scrollview is currently animating a flick
319
* @property _flicking
323
this._flicking = false;
326
* Internal state, defines whether or not the scrollview needs to snap to a boundary edge
328
* @property _snapToEdge
332
this._snapToEdge = false;
336
* gesturemove event handler
338
* @method _onGestureMove
339
* @param e {Event.Facade} The gesturemove event facade
342
_onGestureMove: function(e) {
344
if (this._preventMove) {
348
this._isDragging = true;
349
this._moveEndClientY = e.clientY;
350
this._moveEndClientX = e.clientX;
351
this._lastMoved = (new Date()).getTime();
353
if(this._scrollsVertical) {
354
this.set(SCROLL_Y, -(e.clientY - this._moveStartY));
357
if(this._scrollsHorizontal) {
358
this.set(SCROLL_X, -(e.clientX - this._moveStartX));
363
* gestureend event handler
365
* @method _onGestureMoveEnd
366
* @param e {Event.Facade} The gesturemoveend event facade
369
_onGestureMoveEnd: function(e) {
371
if (this._preventEnd) {
375
var minY = this._minScrollY,
376
maxY = this._maxScrollY,
377
minX = this._minScrollX,
378
maxX = this._maxScrollX,
379
startPoint = this._scrollsVertical ? this._moveStartClientY : this._moveStartClientX,
380
endPoint = this._scrollsVertical ? this._moveEndClientY : this._moveEndClientX,
381
distance = startPoint - endPoint;
383
this._moveEvt.detach();
384
this._moveEndEvt.detach();
387
* Internal state, defines whether or not the scrollview has been scrolled half it's width/height
389
* @property _scrolledHalfway
393
this._scrolledHalfway = false;
394
this._snapToEdge = false;
395
this._isDragging = false;
398
* Contains the distance (postive or negative) in pixels by which the scrollview was last scrolled. This is useful when
399
* setting up click listeners on the scrollview content, which on mouse based devices are always fired, even after a
402
* <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,
403
* if working in a purely touch based environment</p>
405
* @property lastScrolledAmt
409
this.lastScrolledAmt = distance;
411
if(this._scrollsHorizontal && Math.abs(distance) > (this.get('width')/2)) {
412
this._scrolledHalfway = true;
415
* Internal state, defines whether or not the scrollview has been scrolled in the forward (distance > 0), or backward (distance < 0) direction
417
* @property _scrolledForward
421
this._scrolledForward = distance > 0;
424
if(this._scrollsVertical && Math.abs(distance) > (this.get('height')/2)) {
425
this._scrolledHalfway = true;
426
this._scrolledForward = distance > 0;
430
if(this._scrollsVertical && this.get(SCROLL_Y) < minY) {
431
this._snapToEdge = true;
432
this.set(SCROLL_Y, minY);
436
if(this._scrollsHorizontal && this.get(SCROLL_X) < minX) {
437
this._snapToEdge = true;
438
this.set(SCROLL_X, minX);
442
if(this.get(SCROLL_Y) > maxY) {
443
this._snapToEdge = true;
444
this.set(SCROLL_Y, maxY);
448
if(this.get(SCROLL_X) > maxX) {
449
this._snapToEdge = true;
450
this.set(SCROLL_X, maxX);
454
if(this._snapToEdge) {
458
this.fire(EV_SCROLL_END, {
459
onGestureMoveEnd: true
466
* After listener for changes to the scrollY attribute
468
* @method _afterScrollYChange
469
* @param e {Event.Facade} The event facade
472
_afterScrollYChange : function(e) {
474
this._uiScrollY(e.newVal, e.duration, e.easing);
479
* Update the UI when the scrollY attribute changes
482
* @param val {Number} The scrollY value
483
* @param duration {Number} The length (in ms) of the scroll animation
484
* @param easing {String} An easing equation, if duration is defined
487
_uiScrollY : function(val, duration, easing) {
488
duration = duration || this._snapToEdge ? 400 : 0;
489
easing = easing || this._snapToEdge ? ScrollView.SNAP_EASING : null;
491
this.scrollTo(null, val, duration, easing);
495
* After listener for changes to the scrollX attribute
497
* @method _afterScrollXChange
498
* @param e {Event.Facade} The event facade
501
_afterScrollXChange : function(e) {
503
this._uiScrollX(e.newVal, e.duration, e.easing);
508
* Update the UI when the scrollX attribute changes
511
* @param val {Number} The scrollX value
512
* @param duration {Number} The length (in ms) of the scroll animation
513
* @param easing {String} An easing equation, if duration is defined
516
_uiScrollX : function(val, duration, easing) {
517
duration = duration || this._snapToEdge ? 400 : 0;
518
easing = easing || this._snapToEdge ? ScrollView.SNAP_EASING : null;
520
this.scrollTo(val, null, duration, easing);
524
* After listener for the height attribute
526
* @method _afterHeightChange
527
* @param e {Event.Facade} The event facade
530
_afterHeightChange: function() {
531
this._uiDimensionsChange();
535
* After listener for the width attribute
537
* @method _afterWidthChange
538
* @param e {Event.Facade} The event facade
541
_afterWidthChange: function() {
542
this._uiDimensionsChange();
546
* This method gets invoked whenever the height or width attributes change,
547
* allowing us to determine which scrolling axes need to be enabled.
549
* @method _uiDimensionsChange
552
_uiDimensionsChange: function() {
555
height = this.get('height'),
556
width = this.get('width'),
558
// Use bb instead of cb. cb doesn't gives us the right results
559
// in FF (due to overflow:hidden)
560
scrollHeight = bb.get('scrollHeight'),
561
scrollWidth = bb.get('scrollWidth');
563
if(height && scrollHeight > height) {
564
this._scrollsVertical = true;
565
this._maxScrollY = scrollHeight - height;
566
this._minScrollY = 0;
567
this._scrollHeight = scrollHeight;
568
bb.addClass(ScrollView.CLASS_NAMES.vertical);
571
if(width && scrollWidth > width) {
572
this._scrollsHorizontal = true;
573
this._maxScrollX = scrollWidth - width;
574
this._minScrollX = 0;
575
this._scrollWidth = scrollWidth;
576
bb.addClass(ScrollView.CLASS_NAMES.horizontal);
580
* Internal state, defines whether or not the scrollview can scroll vertically
582
* @property _scrollsVertical
588
* Internal state, defines the maximum amount that the scrollview can be scrolled along the Y axis
590
* @property _maxScrollY
596
* Internal state, defines the minimum amount that the scrollview can be scrolled along the Y axis
598
* @property _minScrollY
604
* Internal state, cached scrollHeight, for performance
606
* @property _scrollHeight
612
* Internal state, defines whether or not the scrollview can scroll horizontally
614
* @property _scrollsHorizontal
620
* Internal state, defines the maximum amount that the scrollview can be scrolled along the X axis
622
* @property _maxScrollX
628
* Internal state, defines the minimum amount that the scrollview can be scrolled along the X axis
630
* @property _minScrollX
636
* Internal state, cached scrollWidth, for performance
638
* @property _scrollWidth
645
* Execute a flick at the end of a scroll action
648
* @param distance {Number} The distance (in px) the user scrolled before the flick
649
* @param time {Number} The number of ms the scroll event lasted before the flick
652
_flick: function(e) {
656
* Internal state, currently calculated velocity from the flick
658
* @property _currentVelocity
662
this._currentVelocity = flick.velocity;
663
this._flicking = true;
665
this._decelCached = this.get('deceleration');
666
this._bounceCached = this.get('bounce');
668
this._pastYEdge = false;
669
this._pastXEdge = false;
673
this.fire(EV_SCROLL_FLICK);
677
* Execute a single frame in the flick animation
679
* @method _flickFrame
682
_flickFrame: function() {
689
scrollsVertical = this._scrollsVertical,
690
scrollsHorizontal = this._scrollsHorizontal,
691
deceleration = this._decelCached,
692
bounce = this._bounceCached,
693
step = ScrollView.FRAME_STEP;
695
if(scrollsVertical) {
696
maxY = this._maxScrollY;
697
minY = this._minScrollY;
698
newY = this.get(SCROLL_Y) - (this._currentVelocity * step);
701
if(scrollsHorizontal) {
702
maxX = this._maxScrollX;
703
minX = this._minScrollX;
704
newX = this.get(SCROLL_X) - (this._currentVelocity * step);
707
this._currentVelocity = (this._currentVelocity * deceleration);
709
if(Math.abs(this._currentVelocity).toFixed(4) <= 0.015) {
710
this._flicking = false;
711
this._killTimer(!(this._pastYEdge || this._pastXEdge));
713
if(scrollsVertical) {
715
this._snapToEdge = true;
716
this.set(SCROLL_Y, minY);
717
} else if(newY > maxY) {
718
this._snapToEdge = true;
719
this.set(SCROLL_Y, maxY);
723
if(scrollsHorizontal) {
725
this._snapToEdge = true;
726
this.set(SCROLL_X, minX);
727
} else if(newX > maxX) {
728
this._snapToEdge = true;
729
this.set(SCROLL_X, maxX);
736
if (scrollsVertical) {
737
if (newY < minY || newY > maxY) {
738
this._pastYEdge = true;
739
this._currentVelocity *= bounce;
742
this.set(SCROLL_Y, newY);
745
if (scrollsHorizontal) {
746
if (newX < minX || newX > maxX) {
747
this._pastXEdge = true;
748
this._currentVelocity *= bounce;
751
this.set(SCROLL_X, newX);
754
if (!this._flickTimer) {
755
this._flickTimer = Y.later(step, this, '_flickFrame', null, true);
760
* Stop the animation timer
763
* @param fireEvent {Boolean} If true, fire the scrollEnd event
766
_killTimer: function(fireEvent) {
767
if(this._flickTimer) {
768
this._flickTimer.cancel();
769
this._flickTimer = null;
773
this.fire(EV_SCROLL_END);
778
* The scrollX, scrollY setter implementation
782
* @param {Number} val
783
* @param {String} dim
785
* @return {Number} The constrained value, if it exceeds min/max range
787
_setScroll : function(val, dim) {
788
var bouncing = this._cachedBounce || this.get(BOUNCE),
789
range = ScrollView.BOUNCE_RANGE,
791
maxScroll = (dim == DIM_X) ? this._maxScrollX : this._maxScrollY,
793
min = bouncing ? -range : 0,
794
max = bouncing ? maxScroll + range : maxScroll;
796
if(!bouncing || !this._isDragging) {
799
} else if(val > max) {
808
* Setter for the scrollX attribute
810
* @method _setScrollX
811
* @param val {Number} The new scrollX value
812
* @return {Number} The normalized value
815
_setScrollX: function(val) {
816
return this._setScroll(val, DIM_X);
820
* Setter for the scrollY ATTR
822
* @method _setScrollY
823
* @param val {Number} The new scrollY value
824
* @return {Number} The normalized value
827
_setScrollY: function(val) {
828
return this._setScroll(val, DIM_Y);
833
// Y.ScrollView static properties
836
* The identity of the widget.
838
* @property ScrollView.NAME
840
* @default 'scrollview'
848
* Static property used to define the default attribute configuration of
851
* @property ScrollView.ATTRS
859
* The scroll position in the y-axis
867
setter: '_setScrollY'
871
* The scroll position in the x-axis
879
setter: '_setScrollX'
883
* Drag coefficent for inertial scrolling. The closer to 1 this
884
* value is, the less friction during scrolling.
886
* @attribute deceleration
894
* Drag coefficient for intertial scrolling at the upper
895
* and lower boundaries of the scrollview. Set to 0 to
896
* disable "rubber-banding".
907
* The minimum distance and/or velocity which define a flick
911
* @default Object with properties minDistance = 10, minVelocity = 0.3.
922
* List of class names used in the scrollview's DOM
924
* @property ScrollView.CLASS_NAMES
928
CLASS_NAMES: CLASS_NAMES,
931
* Flag used to source property changes initiated from the DOM
933
* @property ScrollView.UI_SRC
941
* The default bounce distance in pixels
943
* @property ScrollView.BOUNCE_RANGE
951
* The interval used when animating the flick
953
* @property ScrollView.FRAME_STEP
961
* The default easing used when animating the flick
963
* @property ScrollView.EASING
966
* @default 'cubic-bezier(0, 0.1, 0, 1.0)'
968
EASING : 'cubic-bezier(0, 0.1, 0, 1.0)',
971
* The default easing to use when animatiing the bounce snap back.
973
* @property ScrollView.SNAP_EASING
976
* @default 'ease-out'
978
SNAP_EASING : 'ease-out',
981
* Style property name to use to set transition duration. Currently Webkit specific (WebkitTransitionDuration)
983
* @property ScrollView._TRANSITION_DURATION
986
_TRANSITION_DURATION : "WebkitTransitionDuration",
989
* Style property name to use to set transition property. Currently, Webkit specific (WebkitTransitionProperty)
991
* @property ScrollView._TRANSITION_PROPERTY
994
_TRANSITION_PROPERTY : "WebkitTransitionProperty"
998
}, '3.2.0' ,{skinnable:true, requires:['widget', 'event-gestures', 'transition']});