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