Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / src / layout / container / boxOverflow / Scroller.js
1 /**
2  * @class Ext.layout.container.boxOverflow.Scroller
3  * @extends Ext.layout.container.boxOverflow.None
4  * @private
5  */
6 Ext.define('Ext.layout.container.boxOverflow.Scroller', {
7
8     /* Begin Definitions */
9
10     extend: 'Ext.layout.container.boxOverflow.None',
11     requires: ['Ext.util.ClickRepeater', 'Ext.core.Element'],
12     alternateClassName: 'Ext.layout.boxOverflow.Scroller',
13     mixins: {
14         observable: 'Ext.util.Observable'
15     },
16     
17     /* End Definitions */
18
19     /**
20      * @cfg {Boolean} animateScroll
21      * True to animate the scrolling of items within the layout (defaults to true, ignored if enableScroll is false)
22      */
23     animateScroll: false,
24
25     /**
26      * @cfg {Number} scrollIncrement
27      * The number of pixels to scroll by on scroller click (defaults to 24)
28      */
29     scrollIncrement: 20,
30
31     /**
32      * @cfg {Number} wheelIncrement
33      * The number of pixels to increment on mouse wheel scrolling (defaults to <tt>3</tt>).
34      */
35     wheelIncrement: 10,
36
37     /**
38      * @cfg {Number} scrollRepeatInterval
39      * Number of milliseconds between each scroll while a scroller button is held down (defaults to 20)
40      */
41     scrollRepeatInterval: 60,
42
43     /**
44      * @cfg {Number} scrollDuration
45      * Number of milliseconds that each scroll animation lasts (defaults to 400)
46      */
47     scrollDuration: 400,
48
49     /**
50      * @cfg {String} beforeCtCls
51      * CSS class added to the beforeCt element. This is the element that holds any special items such as scrollers,
52      * which must always be present at the leftmost edge of the Container
53      */
54
55     /**
56      * @cfg {String} afterCtCls
57      * CSS class added to the afterCt element. This is the element that holds any special items such as scrollers,
58      * which must always be present at the rightmost edge of the Container
59      */
60
61     /**
62      * @cfg {String} scrollerCls
63      * CSS class added to both scroller elements if enableScroll is used
64      */
65     scrollerCls: Ext.baseCSSPrefix + 'box-scroller',
66
67     /**
68      * @cfg {String} beforeScrollerCls
69      * CSS class added to the left scroller element if enableScroll is used
70      */
71
72     /**
73      * @cfg {String} afterScrollerCls
74      * CSS class added to the right scroller element if enableScroll is used
75      */
76     
77     constructor: function(layout, config) {
78         this.layout = layout;
79         Ext.apply(this, config || {});
80         
81         this.addEvents(
82             /**
83              * @event scroll
84              * @param {Ext.layout.container.boxOverflow.Scroller} scroller The layout scroller
85              * @param {Number} newPosition The new position of the scroller
86              * @param {Boolean/Object} animate If animating or not. If true, it will be a animation configuration, else it will be false
87              */
88             'scroll'
89         );
90     },
91     
92     initCSSClasses: function() {
93         var me = this,
94         layout = me.layout;
95
96         if (!me.CSSinitialized) {
97             me.beforeCtCls = me.beforeCtCls || Ext.baseCSSPrefix + 'box-scroller-' + layout.parallelBefore;
98             me.afterCtCls  = me.afterCtCls  || Ext.baseCSSPrefix + 'box-scroller-' + layout.parallelAfter;
99             me.beforeScrollerCls = me.beforeScrollerCls || Ext.baseCSSPrefix + layout.owner.getXType() + '-scroll-' + layout.parallelBefore;
100             me.afterScrollerCls  = me.afterScrollerCls  || Ext.baseCSSPrefix + layout.owner.getXType() + '-scroll-' + layout.parallelAfter;
101             me.CSSinitializes = true;
102         }
103     },
104
105     handleOverflow: function(calculations, targetSize) {
106         var me = this,
107             layout = me.layout,
108             methodName = 'get' + layout.parallelPrefixCap,
109             newSize = {};
110
111         me.initCSSClasses();
112         me.callParent(arguments);
113         this.createInnerElements();
114         this.showScrollers();
115         newSize[layout.perpendicularPrefix] = targetSize[layout.perpendicularPrefix];
116         newSize[layout.parallelPrefix] = targetSize[layout.parallelPrefix] - (me.beforeCt[methodName]() + me.afterCt[methodName]());
117         return { targetSize: newSize };
118     },
119
120     /**
121      * @private
122      * Creates the beforeCt and afterCt elements if they have not already been created
123      */
124     createInnerElements: function() {
125         var me = this,
126             target = me.layout.getRenderTarget();
127
128         //normal items will be rendered to the innerCt. beforeCt and afterCt allow for fixed positioning of
129         //special items such as scrollers or dropdown menu triggers
130         if (!me.beforeCt) {
131             target.addCls(Ext.baseCSSPrefix + me.layout.direction + '-box-overflow-body');
132             me.beforeCt = target.insertSibling({cls: Ext.layout.container.Box.prototype.innerCls + ' ' + me.beforeCtCls}, 'before');
133             me.afterCt  = target.insertSibling({cls: Ext.layout.container.Box.prototype.innerCls + ' ' + me.afterCtCls},  'after');
134             me.createWheelListener();
135         }
136     },
137
138     /**
139      * @private
140      * Sets up an listener to scroll on the layout's innerCt mousewheel event
141      */
142     createWheelListener: function() {
143         this.layout.innerCt.on({
144             scope     : this,
145             mousewheel: function(e) {
146                 e.stopEvent();
147
148                 this.scrollBy(e.getWheelDelta() * this.wheelIncrement * -1, false);
149             }
150         });
151     },
152
153     /**
154      * @private
155      */
156     clearOverflow: function() {
157         this.hideScrollers();
158     },
159
160     /**
161      * @private
162      * Shows the scroller elements in the beforeCt and afterCt. Creates the scrollers first if they are not already
163      * present. 
164      */
165     showScrollers: function() {
166         this.createScrollers();
167         this.beforeScroller.show();
168         this.afterScroller.show();
169         this.updateScrollButtons();
170         
171         this.layout.owner.addClsWithUI('scroller');
172     },
173
174     /**
175      * @private
176      * Hides the scroller elements in the beforeCt and afterCt
177      */
178     hideScrollers: function() {
179         if (this.beforeScroller != undefined) {
180             this.beforeScroller.hide();
181             this.afterScroller.hide();
182             
183             this.layout.owner.removeClsWithUI('scroller');
184         }
185     },
186
187     /**
188      * @private
189      * Creates the clickable scroller elements and places them into the beforeCt and afterCt
190      */
191     createScrollers: function() {
192         if (!this.beforeScroller && !this.afterScroller) {
193             var before = this.beforeCt.createChild({
194                 cls: Ext.String.format("{0} {1} ", this.scrollerCls, this.beforeScrollerCls)
195             });
196
197             var after = this.afterCt.createChild({
198                 cls: Ext.String.format("{0} {1}", this.scrollerCls, this.afterScrollerCls)
199             });
200
201             before.addClsOnOver(this.beforeScrollerCls + '-hover');
202             after.addClsOnOver(this.afterScrollerCls + '-hover');
203
204             before.setVisibilityMode(Ext.core.Element.DISPLAY);
205             after.setVisibilityMode(Ext.core.Element.DISPLAY);
206
207             this.beforeRepeater = Ext.create('Ext.util.ClickRepeater', before, {
208                 interval: this.scrollRepeatInterval,
209                 handler : this.scrollLeft,
210                 scope   : this
211             });
212
213             this.afterRepeater = Ext.create('Ext.util.ClickRepeater', after, {
214                 interval: this.scrollRepeatInterval,
215                 handler : this.scrollRight,
216                 scope   : this
217             });
218
219             /**
220              * @property beforeScroller
221              * @type Ext.core.Element
222              * The left scroller element. Only created when needed.
223              */
224             this.beforeScroller = before;
225
226             /**
227              * @property afterScroller
228              * @type Ext.core.Element
229              * The left scroller element. Only created when needed.
230              */
231             this.afterScroller = after;
232         }
233     },
234
235     /**
236      * @private
237      */
238     destroy: function() {
239         Ext.destroy(this.beforeRepeater, this.afterRepeater, this.beforeScroller, this.afterScroller, this.beforeCt, this.afterCt);
240     },
241
242     /**
243      * @private
244      * Scrolls left or right by the number of pixels specified
245      * @param {Number} delta Number of pixels to scroll to the right by. Use a negative number to scroll left
246      */
247     scrollBy: function(delta, animate) {
248         this.scrollTo(this.getScrollPosition() + delta, animate);
249     },
250
251     /**
252      * @private
253      * @return {Object} Object passed to scrollTo when scrolling
254      */
255     getScrollAnim: function() {
256         return {
257             duration: this.scrollDuration, 
258             callback: this.updateScrollButtons, 
259             scope   : this
260         };
261     },
262
263     /**
264      * @private
265      * Enables or disables each scroller button based on the current scroll position
266      */
267     updateScrollButtons: function() {
268         if (this.beforeScroller == undefined || this.afterScroller == undefined) {
269             return;
270         }
271
272         var beforeMeth = this.atExtremeBefore()  ? 'addCls' : 'removeCls',
273             afterMeth  = this.atExtremeAfter() ? 'addCls' : 'removeCls',
274             beforeCls  = this.beforeScrollerCls + '-disabled',
275             afterCls   = this.afterScrollerCls  + '-disabled';
276         
277         this.beforeScroller[beforeMeth](beforeCls);
278         this.afterScroller[afterMeth](afterCls);
279         this.scrolling = false;
280     },
281
282     /**
283      * @private
284      * Returns true if the innerCt scroll is already at its left-most point
285      * @return {Boolean} True if already at furthest left point
286      */
287     atExtremeBefore: function() {
288         return this.getScrollPosition() === 0;
289     },
290
291     /**
292      * @private
293      * Scrolls to the left by the configured amount
294      */
295     scrollLeft: function() {
296         this.scrollBy(-this.scrollIncrement, false);
297     },
298
299     /**
300      * @private
301      * Scrolls to the right by the configured amount
302      */
303     scrollRight: function() {
304         this.scrollBy(this.scrollIncrement, false);
305     },
306
307     /**
308      * Returns the current scroll position of the innerCt element
309      * @return {Number} The current scroll position
310      */
311     getScrollPosition: function(){
312         var layout = this.layout;
313         return parseInt(layout.innerCt.dom['scroll' + layout.parallelBeforeCap], 10) || 0;
314     },
315
316     /**
317      * @private
318      * Returns the maximum value we can scrollTo
319      * @return {Number} The max scroll value
320      */
321     getMaxScrollPosition: function() {
322         var layout = this.layout;
323         return layout.innerCt.dom['scroll' + layout.parallelPrefixCap] - this.layout.innerCt['get' + layout.parallelPrefixCap]();
324     },
325
326     /**
327      * @private
328      * Returns true if the innerCt scroll is already at its right-most point
329      * @return {Boolean} True if already at furthest right point
330      */
331     atExtremeAfter: function() {
332         return this.getScrollPosition() >= this.getMaxScrollPosition();
333     },
334
335     /**
336      * @private
337      * Scrolls to the given position. Performs bounds checking.
338      * @param {Number} position The position to scroll to. This is constrained.
339      * @param {Boolean} animate True to animate. If undefined, falls back to value of this.animateScroll
340      */
341     scrollTo: function(position, animate) {
342         var me = this,
343             layout = me.layout,
344             oldPosition = me.getScrollPosition(),
345             newPosition = Ext.Number.constrain(position, 0, me.getMaxScrollPosition());
346
347         if (newPosition != oldPosition && !me.scrolling) {
348             if (animate == undefined) {
349                 animate = me.animateScroll;
350             }
351
352             layout.innerCt.scrollTo(layout.parallelBefore, newPosition, animate ? me.getScrollAnim() : false);
353             if (animate) {
354                 me.scrolling = true;
355             } else {
356                 me.scrolling = false;
357                 me.updateScrollButtons();
358             }
359             
360             me.fireEvent('scroll', me, newPosition, animate ? me.getScrollAnim() : false);
361         }
362     },
363
364     /**
365      * Scrolls to the given component.
366      * @param {String|Number|Ext.Component} item The item to scroll to. Can be a numerical index, component id 
367      * or a reference to the component itself.
368      * @param {Boolean} animate True to animate the scrolling
369      */
370     scrollToItem: function(item, animate) {
371         var me = this,
372             layout = me.layout,
373             visibility,
374             box,
375             newPos;
376
377         item = me.getItem(item);
378         if (item != undefined) {
379             visibility = this.getItemVisibility(item);
380             if (!visibility.fullyVisible) {
381                 box  = item.getBox(true, true);
382                 newPos = box[layout.parallelPosition];
383                 if (visibility.hiddenEnd) {
384                     newPos -= (this.layout.innerCt['get' + layout.parallelPrefixCap]() - box[layout.parallelPrefix]);
385                 }
386                 this.scrollTo(newPos, animate);
387             }
388         }
389     },
390
391     /**
392      * @private
393      * For a given item in the container, return an object with information on whether the item is visible
394      * with the current innerCt scroll value.
395      * @param {Ext.Component} item The item
396      * @return {Object} Values for fullyVisible, hiddenStart and hiddenEnd
397      */
398     getItemVisibility: function(item) {
399         var me          = this,
400             box         = me.getItem(item).getBox(true, true),
401             layout      = me.layout,
402             itemStart   = box[layout.parallelPosition],
403             itemEnd     = itemStart + box[layout.parallelPrefix],
404             scrollStart = me.getScrollPosition(),
405             scrollEnd   = scrollStart + layout.innerCt['get' + layout.parallelPrefixCap]();
406
407         return {
408             hiddenStart : itemStart < scrollStart,
409             hiddenEnd   : itemEnd > scrollEnd,
410             fullyVisible: itemStart > scrollStart && itemEnd < scrollEnd
411         };
412     }
413 });