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