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