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