Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / src / grid / Lockable.js
1 /**
2  * @class Ext.grid.Lockable
3  * @private
4  *
5  * Lockable is a private mixin which injects lockable behavior into any
6  * TablePanel subclass such as GridPanel or TreePanel. TablePanel will
7  * automatically inject the Ext.grid.Lockable mixin in when one of the
8  * these conditions are met:
9  * - The TablePanel has the lockable configuration set to true
10  * - One of the columns in the TablePanel has locked set to true/false
11  *
12  * Each TablePanel subclass *must* register an alias. It should have an array
13  * of configurations to copy to the 2 separate tablepanel's that will be generated
14  * to note what configurations should be copied. These are named normalCfgCopy and
15  * lockedCfgCopy respectively.
16  *
17  * Columns which are locked must specify a fixed width. They do *NOT* support a
18  * flex width.
19  *
20  * Configurations which are specified in this class will be available on any grid or
21  * tree which is using the lockable functionality.
22  */
23 Ext.define('Ext.grid.Lockable', {
24     
25     requires: ['Ext.grid.LockingView'],
26     
27     /**
28      * @cfg {Boolean} syncRowHeight Synchronize rowHeight between the normal and
29      * locked grid view. This is turned on by default. If your grid is guaranteed
30      * to have rows of all the same height, you should set this to false to
31      * optimize performance.
32      */
33     syncRowHeight: true,
34     
35     /**
36      * @cfg {String} subGridXType The xtype of the subgrid to specify. If this is
37      * not specified lockable will determine the subgrid xtype to create by the
38      * following rule. Use the superclasses xtype if the superclass is NOT
39      * tablepanel, otherwise use the xtype itself.
40      */
41     
42     /**
43      * @cfg {Object} lockedViewConfig A view configuration to be applied to the
44      * locked side of the grid. Any conflicting configurations between lockedViewConfig
45      * and viewConfig will be overwritten by the lockedViewConfig.
46      */
47
48     /**
49      * @cfg {Object} normalViewConfig A view configuration to be applied to the
50      * normal/unlocked side of the grid. Any conflicting configurations between normalViewConfig
51      * and viewConfig will be overwritten by the normalViewConfig.
52      */
53     
54     // private variable to track whether or not the spacer is hidden/visible
55     spacerHidden: true,
56     
57     // i8n text
58     unlockText: 'Unlock',
59     lockText: 'Lock',
60     
61     determineXTypeToCreate: function() {
62         var me = this,
63             typeToCreate;
64
65         if (me.subGridXType) {
66             typeToCreate = me.subGridXType;
67         } else {
68             var xtypes     = this.getXTypes().split('/'),
69                 xtypesLn   = xtypes.length,
70                 xtype      = xtypes[xtypesLn - 1],
71                 superxtype = xtypes[xtypesLn - 2];
72                 
73             if (superxtype !== 'tablepanel') {
74                 typeToCreate = superxtype;
75             } else {
76                 typeToCreate = xtype;
77             }
78         }
79         
80         return typeToCreate;
81     },
82     
83     // injectLockable will be invoked before initComponent's parent class implementation
84     // is called, so throughout this method this. are configurations
85     injectLockable: function() {
86         // ensure lockable is set to true in the TablePanel
87         this.lockable = true;
88         // Instruct the TablePanel it already has a view and not to create one.
89         // We are going to aggregate 2 copies of whatever TablePanel we are using
90         this.hasView = true;
91
92         var me = this,
93             // xtype of this class, 'treepanel' or 'gridpanel'
94             // (Note: this makes it a requirement that any subclass that wants to use lockable functionality needs to register an
95             // alias.)
96             xtype = me.determineXTypeToCreate(),
97             // share the selection model
98             selModel = me.getSelectionModel(),
99             lockedGrid = {
100                 xtype: xtype,
101                 // Lockable does NOT support animations for Tree
102                 enableAnimations: false,
103                 scroll: false,
104                 scrollerOwner: false,
105                 selModel: selModel,
106                 border: false,
107                 cls: Ext.baseCSSPrefix + 'grid-inner-locked'
108             },
109             normalGrid = {
110                 xtype: xtype,
111                 enableAnimations: false,
112                 scrollerOwner: false,
113                 selModel: selModel,
114                 border: false
115             },
116             i = 0,
117             columns,
118             lockedHeaderCt,
119             normalHeaderCt;
120         
121         me.addCls(Ext.baseCSSPrefix + 'grid-locked');
122         
123         // copy appropriate configurations to the respective
124         // aggregated tablepanel instances and then delete them
125         // from the master tablepanel.
126         Ext.copyTo(normalGrid, me, me.normalCfgCopy);
127         Ext.copyTo(lockedGrid, me, me.lockedCfgCopy);
128         for (; i < me.normalCfgCopy.length; i++) {
129             delete me[me.normalCfgCopy[i]];
130         }
131         for (i = 0; i < me.lockedCfgCopy.length; i++) {
132             delete me[me.lockedCfgCopy[i]];
133         }
134         
135         me.lockedHeights = [];
136         me.normalHeights = [];
137         
138         columns = me.processColumns(me.columns);
139
140         lockedGrid.width = columns.lockedWidth;
141         lockedGrid.columns = columns.locked;
142         normalGrid.columns = columns.normal;
143         
144         me.store = Ext.StoreManager.lookup(me.store);
145         lockedGrid.store = me.store;
146         normalGrid.store = me.store;
147         
148         // normal grid should flex the rest of the width
149         normalGrid.flex = 1;
150         lockedGrid.viewConfig = me.lockedViewConfig || {};
151         lockedGrid.viewConfig.loadingUseMsg = false;
152         normalGrid.viewConfig = me.normalViewConfig || {};
153         
154         Ext.applyIf(lockedGrid.viewConfig, me.viewConfig);
155         Ext.applyIf(normalGrid.viewConfig, me.viewConfig);
156         
157         me.normalGrid = Ext.ComponentManager.create(normalGrid);
158         me.lockedGrid = Ext.ComponentManager.create(lockedGrid);
159         
160         me.view = Ext.create('Ext.grid.LockingView', {
161             locked: me.lockedGrid,
162             normal: me.normalGrid,
163             panel: me    
164         });
165         
166         if (me.syncRowHeight) {
167             me.lockedGrid.getView().on({
168                 refresh: me.onLockedGridAfterRefresh,
169                 itemupdate: me.onLockedGridAfterUpdate,
170                 scope: me
171             });
172             
173             me.normalGrid.getView().on({
174                 refresh: me.onNormalGridAfterRefresh,
175                 itemupdate: me.onNormalGridAfterUpdate,
176                 scope: me
177             });
178         }
179         
180         lockedHeaderCt = me.lockedGrid.headerCt;
181         normalHeaderCt = me.normalGrid.headerCt;
182         
183         lockedHeaderCt.lockedCt = true;
184         lockedHeaderCt.lockableInjected = true;
185         normalHeaderCt.lockableInjected = true;
186         
187         lockedHeaderCt.on({
188             columnshow: me.onLockedHeaderShow,
189             columnhide: me.onLockedHeaderHide,
190             columnmove: me.onLockedHeaderMove,
191             sortchange: me.onLockedHeaderSortChange,
192             columnresize: me.onLockedHeaderResize,
193             scope: me
194         });
195         
196         normalHeaderCt.on({
197             columnmove: me.onNormalHeaderMove,
198             sortchange: me.onNormalHeaderSortChange,
199             scope: me
200         });
201         
202         me.normalGrid.on({
203             scrollershow: me.onScrollerShow,
204             scrollerhide: me.onScrollerHide,
205             scope: me
206         });
207         
208         me.lockedGrid.on('afterlayout', me.onLockedGridAfterLayout, me, {single: true});
209         
210         me.modifyHeaderCt();
211         me.items = [me.lockedGrid, me.normalGrid];
212
213         me.layout = {
214             type: 'hbox',
215             align: 'stretch'
216         };
217     },
218     
219     processColumns: function(columns){
220         // split apart normal and lockedWidths
221         var i = 0,
222             len = columns.length,
223             lockedWidth = 0,
224             lockedHeaders = [],
225             normalHeaders = [],
226             column;
227             
228         for (; i < len; ++i) {
229             column = columns[i];
230             // mark the column as processed so that the locked attribute does not
231             // trigger trying to aggregate the columns again.
232             column.processed = true;
233             if (column.locked) {
234                 // <debug>
235                 if (column.flex) {
236                     Ext.Error.raise("Columns which are locked do NOT support a flex width. You must set a width on the " + columns[i].text + "column.");
237                 }
238                 // </debug>
239                 lockedWidth += column.width;
240                 lockedHeaders.push(column);
241             } else {
242                 normalHeaders.push(column);
243             }
244         }
245         return {
246             lockedWidth: lockedWidth,
247             locked: lockedHeaders,
248             normal: normalHeaders    
249         };
250     },
251     
252     // create a new spacer after the table is refreshed
253     onLockedGridAfterLayout: function() {
254         var me         = this,
255             lockedView = me.lockedGrid.getView();
256         lockedView.on({
257             refresh: me.createSpacer,
258             beforerefresh: me.destroySpacer,
259             scope: me
260         });
261     },
262     
263     // trigger a pseudo refresh on the normal side
264     onLockedHeaderMove: function() {
265         if (this.syncRowHeight) {
266             this.onNormalGridAfterRefresh();
267         }
268     },
269     
270     // trigger a pseudo refresh on the locked side
271     onNormalHeaderMove: function() {
272         if (this.syncRowHeight) {
273             this.onLockedGridAfterRefresh();
274         }
275     },
276     
277     // create a spacer in lockedsection and store a reference
278     // TODO: Should destroy before refreshing content
279     createSpacer: function() {
280         var me   = this,
281             // This affects scrolling all the way to the bottom of a locked grid
282             // additional test, sort a column and make sure it synchronizes
283             w    = Ext.getScrollBarWidth() + (Ext.isIE ? 2 : 0),
284             view = me.lockedGrid.getView(),
285             el   = view.el;
286
287         me.spacerEl = Ext.core.DomHelper.append(el, {
288             cls: me.spacerHidden ? (Ext.baseCSSPrefix + 'hidden') : '',
289             style: 'height: ' + w + 'px;'
290         }, true);
291     },
292     
293     destroySpacer: function() {
294         var me = this;
295         if (me.spacerEl) {
296             me.spacerEl.destroy();
297             delete me.spacerEl;
298         }
299     },
300     
301     // cache the heights of all locked rows and sync rowheights
302     onLockedGridAfterRefresh: function() {
303         var me     = this,
304             view   = me.lockedGrid.getView(),
305             el     = view.el,
306             rowEls = el.query(view.getItemSelector()),
307             ln     = rowEls.length,
308             i = 0;
309             
310         // reset heights each time.
311         me.lockedHeights = [];
312         
313         for (; i < ln; i++) {
314             me.lockedHeights[i] = rowEls[i].clientHeight;
315         }
316         me.syncRowHeights();
317     },
318     
319     // cache the heights of all normal rows and sync rowheights
320     onNormalGridAfterRefresh: function() {
321         var me     = this,
322             view   = me.normalGrid.getView(),
323             el     = view.el,
324             rowEls = el.query(view.getItemSelector()),
325             ln     = rowEls.length,
326             i = 0;
327             
328         // reset heights each time.
329         me.normalHeights = [];
330         
331         for (; i < ln; i++) {
332             me.normalHeights[i] = rowEls[i].clientHeight;
333         }
334         me.syncRowHeights();
335     },
336     
337     // rows can get bigger/smaller
338     onLockedGridAfterUpdate: function(record, index, node) {
339         this.lockedHeights[index] = node.clientHeight;
340         this.syncRowHeights();
341     },
342     
343     // rows can get bigger/smaller
344     onNormalGridAfterUpdate: function(record, index, node) {
345         this.normalHeights[index] = node.clientHeight;
346         this.syncRowHeights();
347     },
348     
349     // match the rowheights to the biggest rowheight on either
350     // side
351     syncRowHeights: function() {
352         var me = this,
353             lockedHeights = me.lockedHeights,
354             normalHeights = me.normalHeights,
355             calcHeights   = [],
356             ln = lockedHeights.length,
357             i  = 0,
358             lockedView, normalView,
359             lockedRowEls, normalRowEls,
360             vertScroller = me.getVerticalScroller(),
361             scrollTop;
362
363         // ensure there are an equal num of locked and normal
364         // rows before synchronization
365         if (lockedHeights.length && normalHeights.length) {
366             lockedView = me.lockedGrid.getView();
367             normalView = me.normalGrid.getView();
368             lockedRowEls = lockedView.el.query(lockedView.getItemSelector());
369             normalRowEls = normalView.el.query(normalView.getItemSelector());
370
371             // loop thru all of the heights and sync to the other side
372             for (; i < ln; i++) {
373                 // ensure both are numbers
374                 if (!isNaN(lockedHeights[i]) && !isNaN(normalHeights[i])) {
375                     if (lockedHeights[i] > normalHeights[i]) {
376                         Ext.fly(normalRowEls[i]).setHeight(lockedHeights[i]);
377                     } else if (lockedHeights[i] < normalHeights[i]) {
378                         Ext.fly(lockedRowEls[i]).setHeight(normalHeights[i]);
379                     }
380                 }
381             }
382
383             // invalidate the scroller and sync the scrollers
384             me.normalGrid.invalidateScroller();
385             
386             // synchronize the view with the scroller, if we have a virtualScrollTop
387             // then the user is using a PagingScroller 
388             if (vertScroller && vertScroller.setViewScrollTop) {
389                 vertScroller.setViewScrollTop(me.virtualScrollTop);
390             } else {
391                 // We don't use setScrollTop here because if the scrollTop is
392                 // set to the exact same value some browsers won't fire the scroll
393                 // event. Instead, we directly set the scrollTop.
394                 scrollTop = normalView.el.dom.scrollTop;
395                 normalView.el.dom.scrollTop = scrollTop;
396                 lockedView.el.dom.scrollTop = scrollTop;
397             }
398             
399             // reset the heights
400             me.lockedHeights = [];
401             me.normalHeights = [];
402         }
403     },
404     
405     // track when scroller is shown
406     onScrollerShow: function(scroller, direction) {
407         if (direction === 'horizontal') {
408             this.spacerHidden = false;
409             this.spacerEl.removeCls(Ext.baseCSSPrefix + 'hidden');
410         }
411     },
412     
413     // track when scroller is hidden
414     onScrollerHide: function(scroller, direction) {
415         if (direction === 'horizontal') {
416             this.spacerHidden = true;
417             this.spacerEl.addCls(Ext.baseCSSPrefix + 'hidden');
418         }
419     },
420
421     
422     // inject Lock and Unlock text
423     modifyHeaderCt: function() {
424         var me = this;
425         me.lockedGrid.headerCt.getMenuItems = me.getMenuItems(true);
426         me.normalGrid.headerCt.getMenuItems = me.getMenuItems(false);
427     },
428     
429     onUnlockMenuClick: function() {
430         this.unlock();
431     },
432     
433     onLockMenuClick: function() {
434         this.lock();
435     },
436     
437     getMenuItems: function(locked) {
438         var me            = this,
439             unlockText    = me.unlockText,
440             lockText      = me.lockText,
441             // TODO: Refactor to use Ext.baseCSSPrefix
442             unlockCls     = 'xg-hmenu-unlock',
443             lockCls       = 'xg-hmenu-lock',
444             unlockHandler = Ext.Function.bind(me.onUnlockMenuClick, me),
445             lockHandler   = Ext.Function.bind(me.onLockMenuClick, me);
446         
447         // runs in the scope of headerCt
448         return function() {
449             var o = Ext.grid.header.Container.prototype.getMenuItems.call(this);
450             o.push('-',{
451                 cls: unlockCls,
452                 text: unlockText,
453                 handler: unlockHandler,
454                 disabled: !locked
455             });
456             o.push({
457                 cls: lockCls,
458                 text: lockText,
459                 handler: lockHandler,
460                 disabled: locked
461             });
462             return o;
463         };
464     },
465     
466     // going from unlocked section to locked
467     /**
468      * Locks the activeHeader as determined by which menu is open OR a header
469      * as specified.
470      * @param {Ext.grid.column.Column} header (Optional) Header to unlock from the locked section. Defaults to the header which has the menu open currently.
471      * @param {Number} toIdx (Optional) The index to move the unlocked header to. Defaults to appending as the last item.
472      * @private
473      */
474     lock: function(activeHd, toIdx) {
475         var me         = this,
476             normalGrid = me.normalGrid,
477             lockedGrid = me.lockedGrid,
478             normalHCt  = normalGrid.headerCt,
479             lockedHCt  = lockedGrid.headerCt;
480             
481         activeHd = activeHd || normalHCt.getMenu().activeHeader;
482         
483         // if column was previously flexed, get/set current width
484         // and remove the flex
485         if (activeHd.flex) {
486             activeHd.width = activeHd.getWidth();
487             delete activeHd.flex;
488         }
489         
490         normalHCt.remove(activeHd, false);
491         lockedHCt.suspendLayout = true;
492         if (Ext.isDefined(toIdx)) {
493             lockedHCt.insert(toIdx, activeHd);
494         } else {
495             lockedHCt.add(activeHd);
496         }
497         lockedHCt.suspendLayout = false;
498         me.syncLockedSection();
499     },
500     
501     syncLockedSection: function() {
502         var me = this;
503         me.syncLockedWidth();
504         me.lockedGrid.getView().refresh();
505         me.normalGrid.getView().refresh();
506     },
507     
508     // adjust the locked section to the width of its respective
509     // headerCt
510     syncLockedWidth: function() {
511         var me = this,
512             width = me.lockedGrid.headerCt.getFullWidth(true);
513         me.lockedGrid.setWidth(width);
514     },
515     
516     onLockedHeaderResize: function() {
517         this.syncLockedWidth();
518     },
519     
520     onLockedHeaderHide: function() {
521         this.syncLockedWidth();
522     },
523     
524     onLockedHeaderShow: function() {
525         this.syncLockedWidth();
526     },
527     
528     onLockedHeaderSortChange: function(headerCt, header, sortState) {
529         if (sortState) {
530             // no real header, and silence the event so we dont get into an
531             // infinite loop
532             this.normalGrid.headerCt.clearOtherSortStates(null, true);
533         }
534     },
535     
536     onNormalHeaderSortChange: function(headerCt, header, sortState) {
537         if (sortState) {
538             // no real header, and silence the event so we dont get into an
539             // infinite loop
540             this.lockedGrid.headerCt.clearOtherSortStates(null, true);
541         }
542     },
543     
544     // going from locked section to unlocked
545     /**
546      * Unlocks the activeHeader as determined by which menu is open OR a header
547      * as specified.
548      * @param {Ext.grid.column.Column} header (Optional) Header to unlock from the locked section. Defaults to the header which has the menu open currently.
549      * @param {Number} toIdx (Optional) The index to move the unlocked header to. Defaults to 0.
550      * @private
551      */
552     unlock: function(activeHd, toIdx) {
553         var me         = this,
554             normalGrid = me.normalGrid,
555             lockedGrid = me.lockedGrid,
556             normalHCt  = normalGrid.headerCt,
557             lockedHCt  = lockedGrid.headerCt;
558
559         if (!Ext.isDefined(toIdx)) {
560             toIdx = 0;
561         }
562         activeHd = activeHd || lockedHCt.getMenu().activeHeader;
563         
564         lockedHCt.remove(activeHd, false);
565         me.syncLockedWidth();
566         me.lockedGrid.getView().refresh();
567         normalHCt.insert(toIdx, activeHd);
568         me.normalGrid.getView().refresh();
569     },
570     
571     // we want to totally override the reconfigure behaviour here, since we're creating 2 sub-grids
572     reconfigureLockable: function(store, columns) {
573         var me = this,
574             lockedGrid = me.lockedGrid,
575             normalGrid = me.normalGrid;
576         
577         if (columns) {
578             lockedGrid.headerCt.removeAll();
579             normalGrid.headerCt.removeAll();
580             
581             columns = me.processColumns(columns);
582             lockedGrid.setWidth(columns.lockedWidth);
583             lockedGrid.headerCt.add(columns.locked);
584             normalGrid.headerCt.add(columns.normal);
585         }
586         
587         if (store) {
588             store = Ext.data.StoreManager.lookup(store);
589             me.store = store;
590             lockedGrid.bindStore(store);
591             normalGrid.bindStore(store);
592         } else {
593             lockedGrid.getView().refresh();
594             normalGrid.getView().refresh();
595         }
596     }
597 });