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