Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / src / panel / Table.js
1 /**
2  * @class Ext.panel.Table
3  * @extends Ext.panel.Panel
4  * @xtype tablepanel
5  * @private
6  * @author Nicolas Ferrero
7  * TablePanel is a private class and the basis of both TreePanel and GridPanel.
8  *
9  * TablePanel aggregates:
10  *
11  *  - a Selection Model
12  *  - a View
13  *  - a Store
14  *  - Scrollers
15  *  - Ext.grid.header.Container
16  *
17  */
18 Ext.define('Ext.panel.Table', {
19     extend: 'Ext.panel.Panel',
20
21     alias: 'widget.tablepanel',
22
23     uses: [
24         'Ext.selection.RowModel',
25         'Ext.grid.Scroller',
26         'Ext.grid.header.Container',
27         'Ext.grid.Lockable'
28     ],
29
30     cls: Ext.baseCSSPrefix + 'grid',
31     extraBodyCls: Ext.baseCSSPrefix + 'grid-body',
32
33     layout: 'fit',
34     /**
35      * Boolean to indicate that a view has been injected into the panel.
36      * @property hasView
37      */
38     hasView: false,
39
40     // each panel should dictate what viewType and selType to use
41     viewType: null,
42     selType: 'rowmodel',
43
44     /**
45      * @cfg {Number} scrollDelta
46      * Number of pixels to scroll when scrolling with mousewheel.
47      * Defaults to 40.
48      */
49     scrollDelta: 40,
50
51     /**
52      * @cfg {String/Boolean} scroll
53      * Valid values are 'both', 'horizontal' or 'vertical'. true implies 'both'. false implies 'none'.
54      * Defaults to true.
55      */
56     scroll: true,
57
58     /**
59      * @cfg {Array} columns
60      * An array of {@link Ext.grid.column.Column column} definition objects which define all columns that appear in this grid. Each
61      * column definition provides the header text for the column, and a definition of where the data for that column comes from.
62      */
63
64     /**
65      * @cfg {Boolean} forceFit
66      * Specify as <code>true</code> to force the columns to fit into the available width. Headers are first sized according to configuration, whether that be
67      * a specific width, or flex. Then they are all proportionally changed in width so that the entire content width is used..
68      */
69
70     /**
71      * @cfg {Boolean} hideHeaders
72      * Specify as <code>true</code> to hide the headers.
73      */
74
75     /**
76      * @cfg {Boolean} sortableColumns
77      * Defaults to true. Set to false to disable column sorting via clicking the
78      * header and via the Sorting menu items.
79      */
80     sortableColumns: true,
81
82     verticalScrollDock: 'right',
83     verticalScrollerType: 'gridscroller',
84
85     horizontalScrollerPresentCls: Ext.baseCSSPrefix + 'horizontal-scroller-present',
86     verticalScrollerPresentCls: Ext.baseCSSPrefix + 'vertical-scroller-present',
87
88     // private property used to determine where to go down to find views
89     // this is here to support locking.
90     scrollerOwner: true,
91
92     invalidateScrollerOnRefresh: true,
93     
94     enableColumnMove: true,
95     enableColumnResize: true,
96
97
98     initComponent: function() {
99         //<debug>
100         if (!this.viewType) {
101             Ext.Error.raise("You must specify a viewType config.");
102         }
103         if (!this.store) {
104             Ext.Error.raise("You must specify a store config");
105         }
106         if (this.headers) {
107             Ext.Error.raise("The headers config is not supported. Please specify columns instead.");
108         }
109         //</debug>
110
111         var me          = this,
112             scroll      = me.scroll,
113             vertical    = false,
114             horizontal  = false,
115             headerCtCfg = me.columns || me.colModel,
116             i           = 0,
117             view,
118             border = me.border;
119
120         // Set our determinScrollbars method to reference a buffered call to determinScrollbars which fires on a 30ms buffer.
121         me.determineScrollbars = Ext.Function.createBuffered(me.determineScrollbars, 30);
122         me.invalidateScroller = Ext.Function.createBuffered(me.invalidateScroller, 30);
123         me.injectView = Ext.Function.createBuffered(me.injectView, 30);
124
125         if (me.hideHeaders) {
126             border = false;
127         }
128
129         // The columns/colModel config may be either a fully instantiated HeaderContainer, or an array of Column definitions, or a config object of a HeaderContainer
130         // Either way, we extract a columns property referencing an array of Column definitions.
131         if (headerCtCfg instanceof Ext.grid.header.Container) {
132             me.headerCt = headerCtCfg;
133             me.headerCt.border = border;
134             me.columns = me.headerCt.items.items;
135         } else {
136             if (Ext.isArray(headerCtCfg)) {
137                 headerCtCfg = {
138                     items: headerCtCfg,
139                     border: border
140                 };
141             }
142             Ext.apply(headerCtCfg, {
143                 forceFit: me.forceFit,
144                 sortable: me.sortableColumns,
145                 enableColumnMove: me.enableColumnMove,
146                 enableColumnResize: me.enableColumnResize,
147                 border:  border
148             });
149             me.columns = headerCtCfg.items;
150
151              // If any of the Column objects contain a locked property, and are not processed, this is a lockable TablePanel, a
152              // special view will be injected by the Ext.grid.Lockable mixin, so no processing of .
153              if (Ext.ComponentQuery.query('{locked !== undefined}{processed != true}', me.columns).length) {
154                  me.self.mixin('lockable', Ext.grid.Lockable);
155                  me.injectLockable();
156              }
157         }
158
159         me.store = Ext.data.StoreManager.lookup(me.store);
160         me.addEvents(
161             /**
162              * @event scrollerhide
163              * Fires when a scroller is hidden
164              * @param {Ext.grid.Scroller} scroller
165              * @param {String} orientation Orientation, can be 'vertical' or 'horizontal'
166              */
167             'scrollerhide',
168             /**
169              * @event scrollershow
170              * Fires when a scroller is shown
171              * @param {Ext.grid.Scroller} scroller
172              * @param {String} orientation Orientation, can be 'vertical' or 'horizontal'
173              */
174             'scrollershow'
175         );
176
177         me.bodyCls = me.bodyCls || '';
178         me.bodyCls += (' ' + me.extraBodyCls);
179
180         // autoScroll is not a valid configuration
181         delete me.autoScroll;
182
183         // If this TablePanel is lockable (Either configured lockable, or any of the defined columns has a 'locked' property)
184         // than a special lockable view containing 2 side-by-side grids will have been injected so we do not need to set up any UI.
185         if (!me.hasView) {
186
187             // If we were not configured with a ready-made headerCt (either by direct config with a headerCt property, or by passing
188             // a HeaderContainer instance as the 'columns' property, then go ahead and create one from the config object created above.
189             if (!me.headerCt) {
190                 me.headerCt = Ext.create('Ext.grid.header.Container', headerCtCfg);
191             }
192
193             // Extract the array of Column objects
194             me.columns = me.headerCt.items.items;
195
196             if (me.hideHeaders) {
197                 me.headerCt.height = 0;
198                 me.headerCt.border = false;
199                 me.headerCt.addCls(Ext.baseCSSPrefix + 'grid-header-ct-hidden');
200                 me.addCls(Ext.baseCSSPrefix + 'grid-header-hidden');
201                 // IE Quirks Mode fix
202                 // If hidden configuration option was used, several layout calculations will be bypassed.
203                 if (Ext.isIEQuirks) {
204                     me.headerCt.style = {
205                         display: 'none'
206                     };
207                 }
208             }
209
210             // turn both on.
211             if (scroll === true || scroll === 'both') {
212                 vertical = horizontal = true;
213             } else if (scroll === 'horizontal') {
214                 horizontal = true;
215             } else if (scroll === 'vertical') {
216                 vertical = true;
217             // All other values become 'none' or false.
218             } else {
219                 me.headerCt.availableSpaceOffset = 0;
220             }
221
222             if (vertical) {
223                 me.verticalScroller = me.verticalScroller || {};
224                 Ext.applyIf(me.verticalScroller, {
225                     dock: me.verticalScrollDock,
226                     xtype: me.verticalScrollerType,
227                     store: me.store
228                 });
229                 me.verticalScroller = Ext.ComponentManager.create(me.verticalScroller);
230                 me.mon(me.verticalScroller, {
231                     bodyscroll: me.onVerticalScroll,
232                     scope: me
233                 });
234             }
235
236             if (horizontal) {
237                 me.horizontalScroller = Ext.ComponentManager.create({
238                     xtype: 'gridscroller',
239                     section: me,
240                     dock: 'bottom',
241                     store: me.store
242                 });
243                 me.mon(me.horizontalScroller, {
244                     bodyscroll: me.onHorizontalScroll,
245                     scope: me
246                 });
247             }
248
249             me.headerCt.on('columnresize', me.onHeaderResize, me);
250             me.relayEvents(me.headerCt, ['columnresize', 'columnmove', 'columnhide', 'columnshow', 'sortchange']);
251             me.features = me.features || [];
252             me.dockedItems = me.dockedItems || [];
253             me.dockedItems.unshift(me.headerCt);
254             me.viewConfig = me.viewConfig || {};
255             me.viewConfig.invalidateScrollerOnRefresh = me.invalidateScrollerOnRefresh;
256
257             // AbstractDataView will look up a Store configured as an object
258             // getView converts viewConfig into a View instance
259             view = me.getView();
260
261             if (view) {
262                 me.mon(view.store, {
263                     load: me.onStoreLoad,
264                     scope: me
265                 });
266                 me.mon(view, {
267                     refresh: {
268                         fn: this.onViewRefresh,
269                         scope: me,
270                         buffer: 50
271                     },
272                     itemupdate: me.onViewItemUpdate,
273                     scope: me
274                 });
275                 this.relayEvents(view, [
276                     /**
277                      * @event beforeitemmousedown
278                      * Fires before the mousedown event on an item is processed. Returns false to cancel the default action.
279                      * @param {Ext.view.View} this
280                      * @param {Ext.data.Model} record The record that belongs to the item
281                      * @param {HTMLElement} item The item's element
282                      * @param {Number} index The item's index
283                      * @param {Ext.EventObject} e The raw event object
284                      */
285                     'beforeitemmousedown',
286                     /**
287                      * @event beforeitemmouseup
288                      * Fires before the mouseup event on an item is processed. Returns false to cancel the default action.
289                      * @param {Ext.view.View} this
290                      * @param {Ext.data.Model} record The record that belongs to the item
291                      * @param {HTMLElement} item The item's element
292                      * @param {Number} index The item's index
293                      * @param {Ext.EventObject} e The raw event object
294                      */
295                     'beforeitemmouseup',
296                     /**
297                      * @event beforeitemmouseenter
298                      * Fires before the mouseenter event on an item is processed. Returns false to cancel the default action.
299                      * @param {Ext.view.View} this
300                      * @param {Ext.data.Model} record The record that belongs to the item
301                      * @param {HTMLElement} item The item's element
302                      * @param {Number} index The item's index
303                      * @param {Ext.EventObject} e The raw event object
304                      */
305                     'beforeitemmouseenter',
306                     /**
307                      * @event beforeitemmouseleave
308                      * Fires before the mouseleave event on an item is processed. Returns false to cancel the default action.
309                      * @param {Ext.view.View} this
310                      * @param {Ext.data.Model} record The record that belongs to the item
311                      * @param {HTMLElement} item The item's element
312                      * @param {Number} index The item's index
313                      * @param {Ext.EventObject} e The raw event object
314                      */
315                     'beforeitemmouseleave',
316                     /**
317                      * @event beforeitemclick
318                      * Fires before the click event on an item is processed. Returns false to cancel the default action.
319                      * @param {Ext.view.View} this
320                      * @param {Ext.data.Model} record The record that belongs to the item
321                      * @param {HTMLElement} item The item's element
322                      * @param {Number} index The item's index
323                      * @param {Ext.EventObject} e The raw event object
324                      */
325                     'beforeitemclick',
326                     /**
327                      * @event beforeitemdblclick
328                      * Fires before the dblclick event on an item is processed. Returns false to cancel the default action.
329                      * @param {Ext.view.View} this
330                      * @param {Ext.data.Model} record The record that belongs to the item
331                      * @param {HTMLElement} item The item's element
332                      * @param {Number} index The item's index
333                      * @param {Ext.EventObject} e The raw event object
334                      */
335                     'beforeitemdblclick',
336                     /**
337                      * @event beforeitemcontextmenu
338                      * Fires before the contextmenu event on an item is processed. Returns false to cancel the default action.
339                      * @param {Ext.view.View} this
340                      * @param {Ext.data.Model} record The record that belongs to the item
341                      * @param {HTMLElement} item The item's element
342                      * @param {Number} index The item's index
343                      * @param {Ext.EventObject} e The raw event object
344                      */
345                     'beforeitemcontextmenu',
346                     /**
347                      * @event itemmousedown
348                      * Fires when there is a mouse down on an item
349                      * @param {Ext.view.View} this
350                      * @param {Ext.data.Model} record The record that belongs to the item
351                      * @param {HTMLElement} item The item's element
352                      * @param {Number} index The item's index
353                      * @param {Ext.EventObject} e The raw event object
354                      */
355                     'itemmousedown',
356                     /**
357                      * @event itemmouseup
358                      * Fires when there is a mouse up on an item
359                      * @param {Ext.view.View} this
360                      * @param {Ext.data.Model} record The record that belongs to the item
361                      * @param {HTMLElement} item The item's element
362                      * @param {Number} index The item's index
363                      * @param {Ext.EventObject} e The raw event object
364                      */
365                     'itemmouseup',
366                     /**
367                      * @event itemmouseenter
368                      * Fires when the mouse enters an item.
369                      * @param {Ext.view.View} this
370                      * @param {Ext.data.Model} record The record that belongs to the item
371                      * @param {HTMLElement} item The item's element
372                      * @param {Number} index The item's index
373                      * @param {Ext.EventObject} e The raw event object
374                      */
375                     'itemmouseenter',
376                     /**
377                      * @event itemmouseleave
378                      * Fires when the mouse leaves an item.
379                      * @param {Ext.view.View} this
380                      * @param {Ext.data.Model} record The record that belongs to the item
381                      * @param {HTMLElement} item The item's element
382                      * @param {Number} index The item's index
383                      * @param {Ext.EventObject} e The raw event object
384                      */
385                     'itemmouseleave',
386                     /**
387                      * @event itemclick
388                      * Fires when an item is clicked.
389                      * @param {Ext.view.View} this
390                      * @param {Ext.data.Model} record The record that belongs to the item
391                      * @param {HTMLElement} item The item's element
392                      * @param {Number} index The item's index
393                      * @param {Ext.EventObject} e The raw event object
394                      */
395                     'itemclick',
396                     /**
397                      * @event itemdblclick
398                      * Fires when an item is double clicked.
399                      * @param {Ext.view.View} this
400                      * @param {Ext.data.Model} record The record that belongs to the item
401                      * @param {HTMLElement} item The item's element
402                      * @param {Number} index The item's index
403                      * @param {Ext.EventObject} e The raw event object
404                      */
405                     'itemdblclick',
406                     /**
407                      * @event itemcontextmenu
408                      * Fires when an item is right clicked.
409                      * @param {Ext.view.View} this
410                      * @param {Ext.data.Model} record The record that belongs to the item
411                      * @param {HTMLElement} item The item's element
412                      * @param {Number} index The item's index
413                      * @param {Ext.EventObject} e The raw event object
414                      */
415                     'itemcontextmenu',
416                     /**
417                      * @event beforecontainermousedown
418                      * Fires before the mousedown event on the container is processed. Returns false to cancel the default action.
419                      * @param {Ext.view.View} this
420                      * @param {Ext.EventObject} e The raw event object
421                      */
422                     'beforecontainermousedown',
423                     /**
424                      * @event beforecontainermouseup
425                      * Fires before the mouseup event on the container is processed. Returns false to cancel the default action.
426                      * @param {Ext.view.View} this
427                      * @param {Ext.EventObject} e The raw event object
428                      */
429                     'beforecontainermouseup',
430                     /**
431                      * @event beforecontainermouseover
432                      * Fires before the mouseover event on the container is processed. Returns false to cancel the default action.
433                      * @param {Ext.view.View} this
434                      * @param {Ext.EventObject} e The raw event object
435                      */
436                     'beforecontainermouseover',
437                     /**
438                      * @event beforecontainermouseout
439                      * Fires before the mouseout event on the container is processed. Returns false to cancel the default action.
440                      * @param {Ext.view.View} this
441                      * @param {Ext.EventObject} e The raw event object
442                      */
443                     'beforecontainermouseout',
444                     /**
445                      * @event beforecontainerclick
446                      * Fires before the click event on the container is processed. Returns false to cancel the default action.
447                      * @param {Ext.view.View} this
448                      * @param {Ext.EventObject} e The raw event object
449                      */
450                     'beforecontainerclick',
451                     /**
452                      * @event beforecontainerdblclick
453                      * Fires before the dblclick event on the container is processed. Returns false to cancel the default action.
454                      * @param {Ext.view.View} this
455                      * @param {Ext.EventObject} e The raw event object
456                      */
457                     'beforecontainerdblclick',
458                     /**
459                      * @event beforecontainercontextmenu
460                      * Fires before the contextmenu event on the container is processed. Returns false to cancel the default action.
461                      * @param {Ext.view.View} this
462                      * @param {Ext.EventObject} e The raw event object
463                      */
464                     'beforecontainercontextmenu',
465                     /**
466                      * @event containermouseup
467                      * Fires when there is a mouse up on the container
468                      * @param {Ext.view.View} this
469                      * @param {Ext.EventObject} e The raw event object
470                      */
471                     'containermouseup',
472                     /**
473                      * @event containermouseover
474                      * Fires when you move the mouse over the container.
475                      * @param {Ext.view.View} this
476                      * @param {Ext.EventObject} e The raw event object
477                      */
478                     'containermouseover',
479                     /**
480                      * @event containermouseout
481                      * Fires when you move the mouse out of the container.
482                      * @param {Ext.view.View} this
483                      * @param {Ext.EventObject} e The raw event object
484                      */
485                     'containermouseout',
486                     /**
487                      * @event containerclick
488                      * Fires when the container is clicked.
489                      * @param {Ext.view.View} this
490                      * @param {Ext.EventObject} e The raw event object
491                      */
492                     'containerclick',
493                     /**
494                      * @event containerdblclick
495                      * Fires when the container is double clicked.
496                      * @param {Ext.view.View} this
497                      * @param {Ext.EventObject} e The raw event object
498                      */
499                     'containerdblclick',
500                     /**
501                      * @event containercontextmenu
502                      * Fires when the container is right clicked.
503                      * @param {Ext.view.View} this
504                      * @param {Ext.EventObject} e The raw event object
505                      */
506                     'containercontextmenu',
507
508                     /**
509                      * @event selectionchange
510                      * Fires when the selected nodes change. Relayed event from the underlying selection model.
511                      * @param {Ext.view.View} this
512                      * @param {Array} selections Array of the selected nodes
513                      */
514                     'selectionchange',
515                     /**
516                      * @event beforeselect
517                      * Fires before a selection is made. If any handlers return false, the selection is cancelled.
518                      * @param {Ext.view.View} this
519                      * @param {HTMLElement} node The node to be selected
520                      * @param {Array} selections Array of currently selected nodes
521                      */
522                     'beforeselect'
523                 ]);
524             }
525         }
526         me.callParent(arguments);
527     },
528
529     // state management
530     initStateEvents: function(){
531         var events = this.stateEvents;
532         // push on stateEvents if they don't exist
533         Ext.each(['columnresize', 'columnmove', 'columnhide', 'columnshow', 'sortchange'], function(event){
534             if (Ext.Array.indexOf(events, event)) {
535                 events.push(event);
536             }
537         });
538         this.callParent();
539     },
540
541     getState: function(){
542         var state = {
543             columns: []
544         },
545         sorter = this.store.sorters.first();
546
547         this.headerCt.items.each(function(header){
548             state.columns.push({
549                 id: header.headerId,
550                 width: header.flex ? undefined : header.width,
551                 hidden: header.hidden,
552                 sortable: header.sortable
553             });
554         });
555
556         if (sorter) {
557             state.sort = {
558                 property: sorter.property,
559                 direction: sorter.direction
560             };
561         }
562         return state;
563     },
564
565     applyState: function(state) {
566         var headers = state.columns,
567             length = headers ? headers.length : 0,
568             headerCt = this.headerCt,
569             items = headerCt.items,
570             sorter = state.sort,
571             store = this.store,
572             i = 0,
573             index,
574             headerState,
575             header;
576
577         for (; i < length; ++i) {
578             headerState = headers[i];
579             header = headerCt.down('gridcolumn[headerId=' + headerState.id + ']');
580             index = items.indexOf(header);
581             if (i !== index) {
582                 headerCt.moveHeader(index, i);
583             }
584             header.sortable = headerState.sortable;
585             if (Ext.isDefined(headerState.width)) {
586                 delete header.flex;
587                 if (header.rendered) {
588                     header.setWidth(headerState.width);
589                 } else {
590                     header.minWidth = header.width = headerState.width;
591                 }
592             }
593             header.hidden = headerState.hidden;
594         }
595
596         if (sorter) {
597             if (store.remoteSort) {
598                 store.sorters.add(Ext.create('Ext.util.Sorter', {
599                     property: sorter.property,
600                     direction: sorter.direction
601                 }));
602             }
603             else {
604                 store.sort(sorter.property, sorter.direction);
605             }
606         }
607     },
608
609     /**
610      * Returns the store associated with this Panel.
611      * @return {Ext.data.Store} The store
612      */
613     getStore: function(){
614         return this.store;
615     },
616
617     /**
618      * Gets the view for this panel.
619      * @return {Ext.view.Table}
620      */
621     getView: function() {
622         var me = this,
623             sm;
624
625         if (!me.view) {
626             sm = me.getSelectionModel();
627             me.view = me.createComponent(Ext.apply({}, me.viewConfig, {
628                 xtype: me.viewType,
629                 store: me.store,
630                 headerCt: me.headerCt,
631                 selModel: sm,
632                 features: me.features,
633                 panel: me
634             }));
635             me.mon(me.view, {
636                 uievent: me.processEvent,
637                 scope: me
638             });
639             sm.view = me.view;
640             me.headerCt.view = me.view;
641             me.relayEvents(me.view, ['cellclick', 'celldblclick']);
642         }
643         return me.view;
644     },
645
646     /**
647      * @private
648      * @override
649      * autoScroll is never valid for all classes which extend TablePanel.
650      */
651     setAutoScroll: Ext.emptyFn,
652
653     // This method hijacks Ext.view.Table's el scroll method.
654     // This enables us to keep the virtualized scrollbars in sync
655     // with the view. It currently does NOT support animation.
656     elScroll: function(direction, distance, animate) {
657         var me = this,
658             scroller;
659
660         if (direction === "up" || direction === "left") {
661             distance = -distance;
662         }
663
664         if (direction === "down" || direction === "up") {
665             scroller = me.getVerticalScroller();
666             scroller.scrollByDeltaY(distance);
667         } else {
668             scroller = me.getHorizontalScroller();
669             scroller.scrollByDeltaX(distance);
670         }
671     },
672     
673     afterLayout: function() {
674         this.callParent(arguments);
675         this.injectView();
676     },
677     
678
679     /**
680      * @private
681      * Called after this Component has achieved its correct initial size, after all layouts have done their thing.
682      * This is so we can add the View only after the initial size is known. This method is buffered 30ms.
683      */
684     injectView: function() {
685         if (!this.hasView && !this.collapsed) {
686             var me   = this,
687                 view = me.getView();
688
689             me.hasView = true;
690             me.add(view);
691
692             // hijack the view el's scroll method
693             view.el.scroll = Ext.Function.bind(me.elScroll, me);
694             // We use to listen to document.body wheel events, but that's a
695             // little much. We scope just to the view now.
696             me.mon(view.el, {
697                 mousewheel: me.onMouseWheel,
698                 scope: me
699             });
700         }
701     },
702
703     afterExpand: function() {
704         this.callParent(arguments);
705         if (!this.hasView) {
706             this.injectView();
707         }
708     },
709
710     /**
711      * @private
712      * Process UI events from the view. Propagate them to whatever internal Components need to process them
713      * @param {String} type Event type, eg 'click'
714      * @param {TableView} view TableView Component
715      * @param {HtmlElement} cell Cell HtmlElement the event took place within
716      * @param {Number} recordIndex Index of the associated Store Model (-1 if none)
717      * @param {Number} cellIndex Cell index within the row
718      * @param {EventObject} e Original event
719      */
720     processEvent: function(type, view, cell, recordIndex, cellIndex, e) {
721         var me = this,
722             header;
723
724         if (cellIndex !== -1) {
725             header = me.headerCt.getGridColumns()[cellIndex];
726             return header.processEvent.apply(header, arguments);
727         }
728     },
729
730     /**
731      * Request a recalculation of scrollbars and put them in if they are needed.
732      */
733     determineScrollbars: function() {
734         var me = this,
735             viewElDom,
736             centerScrollWidth,
737             centerClientWidth,
738             scrollHeight,
739             clientHeight;
740
741         if (!me.collapsed && me.view && me.view.el) {
742             viewElDom = me.view.el.dom;
743             //centerScrollWidth = viewElDom.scrollWidth;
744             centerScrollWidth = me.headerCt.getFullWidth();
745             /**
746              * clientWidth often returns 0 in IE resulting in an
747              * infinity result, here we use offsetWidth bc there are
748              * no possible scrollbars and we don't care about margins
749              */
750             centerClientWidth = viewElDom.offsetWidth;
751             if (me.verticalScroller && me.verticalScroller.el) {
752                 scrollHeight = me.verticalScroller.getSizeCalculation().height;
753             } else {
754                 scrollHeight = viewElDom.scrollHeight;
755             }
756
757             clientHeight = viewElDom.clientHeight;
758
759             me.suspendLayout = true;
760             me.scrollbarChanged = false;
761             if (!me.collapsed && scrollHeight > clientHeight) {
762                 me.showVerticalScroller();
763             } else {
764                 me.hideVerticalScroller();
765             }
766
767             if (!me.collapsed && centerScrollWidth > (centerClientWidth + Ext.getScrollBarWidth() - 2)) {
768                 me.showHorizontalScroller();
769             } else {
770                 me.hideHorizontalScroller();
771             }
772             me.suspendLayout = false;
773             if (me.scrollbarChanged) {
774                 me.doComponentLayout();
775             }
776         }
777     },
778
779     onHeaderResize: function() {
780         if (this.view && this.view.rendered) {
781             this.determineScrollbars();
782             this.invalidateScroller();
783         }
784     },
785
786     /**
787      * Hide the verticalScroller and remove the horizontalScrollerPresentCls.
788      */
789     hideHorizontalScroller: function() {
790         var me = this;
791
792         if (me.horizontalScroller && me.horizontalScroller.ownerCt === me) {
793             me.scrollbarChanged = true;
794             me.verticalScroller.offsets.bottom = 0;
795             me.removeDocked(me.horizontalScroller, false);
796             me.removeCls(me.horizontalScrollerPresentCls);
797             me.fireEvent('scrollerhide', me.horizontalScroller, 'horizontal');
798         }
799
800     },
801
802     /**
803      * Show the horizontalScroller and add the horizontalScrollerPresentCls.
804      */
805     showHorizontalScroller: function() {
806         var me = this;
807
808         if (me.verticalScroller) {
809             me.verticalScroller.offsets.bottom = Ext.getScrollBarWidth() - 2;
810         }
811         if (me.horizontalScroller && me.horizontalScroller.ownerCt !== me) {
812             me.scrollbarChanged = true;
813             me.addDocked(me.horizontalScroller);
814             me.addCls(me.horizontalScrollerPresentCls);
815             me.fireEvent('scrollershow', me.horizontalScroller, 'horizontal');
816         }
817     },
818
819     /**
820      * Hide the verticalScroller and remove the verticalScrollerPresentCls.
821      */
822     hideVerticalScroller: function() {
823         var me = this,
824             headerCt = me.headerCt;
825
826         // only trigger a layout when reserveOffset is changing
827         if (headerCt && headerCt.layout.reserveOffset) {
828             headerCt.layout.reserveOffset = false;
829             headerCt.doLayout();
830         }
831         if (me.verticalScroller && me.verticalScroller.ownerCt === me) {
832             me.scrollbarChanged = true;
833             me.removeDocked(me.verticalScroller, false);
834             me.removeCls(me.verticalScrollerPresentCls);
835             me.fireEvent('scrollerhide', me.verticalScroller, 'vertical');
836         }
837     },
838
839     /**
840      * Show the verticalScroller and add the verticalScrollerPresentCls.
841      */
842     showVerticalScroller: function() {
843         var me = this,
844             headerCt = me.headerCt;
845
846         // only trigger a layout when reserveOffset is changing
847         if (headerCt && !headerCt.layout.reserveOffset) {
848             headerCt.layout.reserveOffset = true;
849             headerCt.doLayout();
850         }
851         if (me.verticalScroller && me.verticalScroller.ownerCt !== me) {
852             me.scrollbarChanged = true;
853             me.addDocked(me.verticalScroller);
854             me.addCls(me.verticalScrollerPresentCls);
855             me.fireEvent('scrollershow', me.verticalScroller, 'vertical');
856         }
857     },
858
859     /**
860      * Invalides scrollers that are present and forces a recalculation.
861      * (Not related to showing/hiding the scrollers)
862      */
863     invalidateScroller: function() {
864         var me = this,
865             vScroll = me.verticalScroller,
866             hScroll = me.horizontalScroller;
867
868         if (vScroll) {
869             vScroll.invalidate();
870         }
871         if (hScroll) {
872             hScroll.invalidate();
873         }
874     },
875
876     // refresh the view when a header moves
877     onHeaderMove: function(headerCt, header, fromIdx, toIdx) {
878         this.view.refresh();
879     },
880
881     // Section onHeaderHide is invoked after view.
882     onHeaderHide: function(headerCt, header) {
883         this.invalidateScroller();
884     },
885
886     onHeaderShow: function(headerCt, header) {
887         this.invalidateScroller();
888     },
889
890     getVerticalScroller: function() {
891         return this.getScrollerOwner().down('gridscroller[dock=' + this.verticalScrollDock + ']');
892     },
893
894     getHorizontalScroller: function() {
895         return this.getScrollerOwner().down('gridscroller[dock=bottom]');
896     },
897
898     onMouseWheel: function(e) {
899         var me = this,
900             browserEvent = e.browserEvent,
901             vertScroller = me.getVerticalScroller(),
902             horizScroller = me.getHorizontalScroller(),
903             scrollDelta = me.scrollDelta,
904             deltaY, deltaX,
905             vertScrollerEl, horizScrollerEl,
906             vertScrollerElDom, horizScrollerElDom,
907             horizontalCanScrollLeft, horizontalCanScrollRight,
908             verticalCanScrollDown, verticalCanScrollUp;
909
910         // calculate whether or not both scrollbars can scroll right/left and up/down
911         if (horizScroller) {
912             horizScrollerEl = horizScroller.el;
913             if (horizScrollerEl) {
914                 horizScrollerElDom = horizScrollerEl.dom;
915                 horizontalCanScrollRight = horizScrollerElDom.scrollLeft !== horizScrollerElDom.scrollWidth - horizScrollerElDom.clientWidth;
916                 horizontalCanScrollLeft  = horizScrollerElDom.scrollLeft !== 0;
917             }
918         }
919         if (vertScroller) {
920             vertScrollerEl = vertScroller.el;
921             if (vertScrollerEl) {
922                 vertScrollerElDom = vertScrollerEl.dom;
923                 verticalCanScrollDown = vertScrollerElDom.scrollTop !== vertScrollerElDom.scrollHeight - vertScrollerElDom.clientHeight;
924                 verticalCanScrollUp   = vertScrollerElDom.scrollTop !== 0;
925             }
926         }
927
928         // Webkit Horizontal Axis
929         if (browserEvent.wheelDeltaX || browserEvent.wheelDeltaY) {        
930             deltaX = -browserEvent.wheelDeltaX / 120 * scrollDelta / 3;
931             deltaY = -browserEvent.wheelDeltaY / 120 * scrollDelta / 3;
932         } else {
933             // Gecko Horizontal Axis
934             if (browserEvent.axis && browserEvent.axis === 1) {
935                 deltaX = -(scrollDelta * e.getWheelDelta()) / 3;
936             } else {
937                 deltaY = -(scrollDelta * e.getWheelDelta() / 3);
938             }
939         }
940         
941         if (horizScroller) {
942             if ((deltaX < 0 && horizontalCanScrollLeft) || (deltaX > 0 && horizontalCanScrollRight)) {
943                 e.stopEvent();
944                 horizScroller.scrollByDeltaX(deltaX);
945             }
946         }
947         if (vertScroller) {
948             if ((deltaY < 0 && verticalCanScrollUp) || (deltaY > 0 && verticalCanScrollDown)) {
949                 e.stopEvent();
950                 vertScroller.scrollByDeltaY(deltaY);    
951             }
952         }
953     },
954
955     /**
956      * @private
957      * Determine and invalidate scrollers on view refresh
958      */
959     onViewRefresh: function() {
960         if (Ext.isIE) {
961             this.syncCellHeight();
962         }
963         this.determineScrollbars();
964         if (this.invalidateScrollerOnRefresh) {
965             this.invalidateScroller();
966         }
967     },
968
969     onViewItemUpdate: function(record, index, tr) {
970         if (Ext.isIE) {
971             this.syncCellHeight([tr]);
972         }
973     },
974
975     // BrowserBug: IE will not stretch the td to fit the height of the entire
976     // tr, so manually sync cellheights on refresh and when an item has been
977     // updated.
978     syncCellHeight: function(trs) {
979         var me    = this,
980             i     = 0,
981             tds,
982             j, tdsLn,
983             tr, td,
984             trsLn,
985             rowHeights = [],
986             cellHeights,
987             cellClsSelector = ('.' + Ext.baseCSSPrefix + 'grid-cell');
988
989         trs   = trs || me.view.getNodes();
990         
991         trsLn = trs.length;
992         // Reading loop
993         for (; i < trsLn; i++) {
994             tr = trs[i];
995             tds = Ext.fly(tr).query(cellClsSelector);
996             tdsLn = tds.length;
997             cellHeights = [];
998             for (j = 0; j < tdsLn; j++) {
999                 td = tds[j];
1000                 cellHeights.push(td.clientHeight);
1001             }
1002             rowHeights.push(Ext.Array.max(cellHeights));
1003         }
1004
1005         // Setting loop
1006         for (i = 0; i < trsLn; i++) {
1007             tr = trs[i];
1008             tdsLn = tr.childNodes.length;
1009             for (j = 0; j < tdsLn; j++) {
1010                 td = Ext.fly(tr.childNodes[j]);
1011                 if (rowHeights[i]) {
1012                     if (td.is(cellClsSelector)) {
1013                         td.setHeight(rowHeights[i]);
1014                     } else {
1015                         td.down(cellClsSelector).setHeight(rowHeights[i]);
1016                     }
1017                 }
1018                 
1019             }
1020         }
1021     },
1022
1023     /**
1024      * Sets the scrollTop of the TablePanel.
1025      * @param {Number} deltaY
1026      */
1027     setScrollTop: function(top) {
1028         var me               = this,
1029             rootCmp          = me.getScrollerOwner(),
1030             verticalScroller = me.getVerticalScroller();
1031
1032         rootCmp.virtualScrollTop = top;
1033         if (verticalScroller) {
1034             verticalScroller.setScrollTop(top);
1035         }
1036
1037     },
1038
1039     getScrollerOwner: function() {
1040         var rootCmp = this;
1041         if (!this.scrollerOwner) {
1042             rootCmp = this.up('[scrollerOwner]');
1043         }
1044         return rootCmp;
1045     },
1046
1047     /**
1048      * Scrolls the TablePanel by deltaY
1049      * @param {Number} deltaY
1050      */
1051     scrollByDeltaY: function(deltaY) {
1052         var rootCmp = this.getScrollerOwner(),
1053             scrollerRight;
1054         scrollerRight = rootCmp.down('gridscroller[dock=' + this.verticalScrollDock + ']');
1055         if (scrollerRight) {
1056             scrollerRight.scrollByDeltaY(deltaY);
1057         }
1058     },
1059
1060
1061     /**
1062      * Scrolls the TablePanel by deltaX
1063      * @param {Number} deltaY
1064      */
1065     scrollByDeltaX: function(deltaX) {
1066         this.horizontalScroller.scrollByDeltaX(deltaX);
1067     },
1068
1069     /**
1070      * Get left hand side marker for header resizing.
1071      * @private
1072      */
1073     getLhsMarker: function() {
1074         var me = this;
1075
1076         if (!me.lhsMarker) {
1077             me.lhsMarker = Ext.core.DomHelper.append(me.el, {
1078                 cls: Ext.baseCSSPrefix + 'grid-resize-marker'
1079             }, true);
1080         }
1081         return me.lhsMarker;
1082     },
1083
1084     /**
1085      * Get right hand side marker for header resizing.
1086      * @private
1087      */
1088     getRhsMarker: function() {
1089         var me = this;
1090
1091         if (!me.rhsMarker) {
1092             me.rhsMarker = Ext.core.DomHelper.append(me.el, {
1093                 cls: Ext.baseCSSPrefix + 'grid-resize-marker'
1094             }, true);
1095         }
1096         return me.rhsMarker;
1097     },
1098
1099     /**
1100      * Returns the selection model being used and creates it via the configuration
1101      * if it has not been created already.
1102      * @return {Ext.selection.Model} selModel
1103      */
1104     getSelectionModel: function(){
1105         if (!this.selModel) {
1106             this.selModel = {};
1107         }
1108
1109         var mode = 'SINGLE',
1110             type;
1111         if (this.simpleSelect) {
1112             mode = 'SIMPLE';
1113         } else if (this.multiSelect) {
1114             mode = 'MULTI';
1115         }
1116
1117         Ext.applyIf(this.selModel, {
1118             allowDeselect: this.allowDeselect,
1119             mode: mode
1120         });
1121
1122         if (!this.selModel.events) {
1123             type = this.selModel.selType || this.selType;
1124             this.selModel = Ext.create('selection.' + type, this.selModel);
1125         }
1126
1127         if (!this.selModel.hasRelaySetup) {
1128             this.relayEvents(this.selModel, ['selectionchange', 'select', 'deselect']);
1129             this.selModel.hasRelaySetup = true;
1130         }
1131
1132         // lock the selection model if user
1133         // has disabled selection
1134         if (this.disableSelection) {
1135             this.selModel.locked = true;
1136         }
1137         return this.selModel;
1138     },
1139
1140     onVerticalScroll: function(event, target) {
1141         var owner = this.getScrollerOwner(),
1142             items = owner.query('tableview'),
1143             i = 0,
1144             len = items.length;
1145
1146         for (; i < len; i++) {
1147             items[i].el.dom.scrollTop = target.scrollTop;
1148         }
1149     },
1150
1151     onHorizontalScroll: function(event, target) {
1152         var owner = this.getScrollerOwner(),
1153             items = owner.query('tableview'),
1154             i = 0,
1155             len = items.length,
1156             center,
1157             centerEl,
1158             centerScrollWidth,
1159             centerClientWidth,
1160             width;
1161
1162         center = items[1] || items[0];
1163         centerEl = center.el.dom;
1164         centerScrollWidth = centerEl.scrollWidth;
1165         centerClientWidth = centerEl.offsetWidth;
1166         width = this.horizontalScroller.getWidth();
1167
1168         centerEl.scrollLeft = target.scrollLeft;
1169         this.headerCt.el.dom.scrollLeft = target.scrollLeft;
1170     },
1171
1172     // template method meant to be overriden
1173     onStoreLoad: Ext.emptyFn,
1174
1175     getEditorParent: function() {
1176         return this.body;
1177     },
1178
1179     bindStore: function(store) {
1180         var me = this;
1181         me.store = store;
1182         me.getView().bindStore(store);
1183     },
1184
1185     reconfigure: function(store, columns) {
1186         var me = this;
1187
1188         if (me.lockable) {
1189             me.reconfigureLockable(store, columns);
1190             return;
1191         }
1192
1193         if (columns) {
1194             me.headerCt.removeAll();
1195             me.headerCt.add(columns);
1196         }
1197         if (store) {
1198             store = Ext.StoreManager.lookup(store);
1199             me.bindStore(store);
1200         } else {
1201             me.getView().refresh();
1202         }
1203     },
1204     
1205     afterComponentLayout: function() {
1206         var me = this;
1207         me.callParent(arguments);
1208         me.determineScrollbars();
1209         me.invalidateScroller();
1210     }
1211 });