3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
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.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.grid.Lockable
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:
24 * - The TablePanel has the lockable configuration set to true
25 * - One of the columns in the TablePanel has locked set to true/false
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.
32 * Columns which are locked must specify a fixed width. They do NOT support a
35 * Configurations which are specified in this class will be available on any grid or
36 * tree which is using the lockable functionality.
38 Ext.define('Ext.grid.Lockable', {
40 requires: ['Ext.grid.LockingView'],
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.
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.
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.
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.
69 // private variable to track whether or not the spacer is hidden/visible
78 determineXTypeToCreate: function() {
82 if (me.subGridXType) {
83 typeToCreate = me.subGridXType;
85 var xtypes = this.getXTypes().split('/'),
86 xtypesLn = xtypes.length,
87 xtype = xtypes[xtypesLn - 1],
88 superxtype = xtypes[xtypesLn - 2];
90 if (superxtype !== 'tablepanel') {
91 typeToCreate = superxtype;
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
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
113 xtype = me.determineXTypeToCreate(),
114 // share the selection model
115 selModel = me.getSelectionModel(),
118 // Lockable does NOT support animations for Tree
119 enableAnimations: false,
121 scrollerOwner: false,
124 cls: Ext.baseCSSPrefix + 'grid-inner-locked'
128 enableAnimations: false,
129 scrollerOwner: false,
138 me.addCls(Ext.baseCSSPrefix + 'grid-locked');
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]];
148 for (i = 0; i < me.lockedCfgCopy.length; i++) {
149 delete me[me.lockedCfgCopy[i]];
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.
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.
170 me.addStateEvents(['lockcolumn', 'unlockcolumn']);
172 me.lockedHeights = [];
173 me.normalHeights = [];
175 columns = me.processColumns(me.columns);
177 lockedGrid.width = columns.lockedWidth + Ext.num(selModel.headerWidth, 0);
178 lockedGrid.columns = columns.locked;
179 normalGrid.columns = columns.normal;
181 me.store = Ext.StoreManager.lookup(me.store);
182 lockedGrid.store = me.store;
183 normalGrid.store = me.store;
185 // normal grid should flex the rest of the width
187 lockedGrid.viewConfig = me.lockedViewConfig || {};
188 lockedGrid.viewConfig.loadingUseMsg = false;
189 normalGrid.viewConfig = me.normalViewConfig || {};
191 Ext.applyIf(lockedGrid.viewConfig, me.viewConfig);
192 Ext.applyIf(normalGrid.viewConfig, me.viewConfig);
194 me.normalGrid = Ext.ComponentManager.create(normalGrid);
195 me.lockedGrid = Ext.ComponentManager.create(lockedGrid);
197 me.view = Ext.create('Ext.grid.LockingView', {
198 locked: me.lockedGrid,
199 normal: me.normalGrid,
203 if (me.syncRowHeight) {
204 me.lockedGrid.getView().on({
205 refresh: me.onLockedGridAfterRefresh,
206 itemupdate: me.onLockedGridAfterUpdate,
210 me.normalGrid.getView().on({
211 refresh: me.onNormalGridAfterRefresh,
212 itemupdate: me.onNormalGridAfterUpdate,
217 lockedHeaderCt = me.lockedGrid.headerCt;
218 normalHeaderCt = me.normalGrid.headerCt;
220 lockedHeaderCt.lockedCt = true;
221 lockedHeaderCt.lockableInjected = true;
222 normalHeaderCt.lockableInjected = true;
225 columnshow: me.onLockedHeaderShow,
226 columnhide: me.onLockedHeaderHide,
227 columnmove: me.onLockedHeaderMove,
228 sortchange: me.onLockedHeaderSortChange,
229 columnresize: me.onLockedHeaderResize,
234 columnmove: me.onNormalHeaderMove,
235 sortchange: me.onNormalHeaderSortChange,
240 scrollershow: me.onScrollerShow,
241 scrollerhide: me.onScrollerHide,
245 me.lockedGrid.on('afterlayout', me.onLockedGridAfterLayout, me, {single: true});
248 me.items = [me.lockedGrid, me.normalGrid];
250 me.relayHeaderCtEvents(lockedHeaderCt);
251 me.relayHeaderCtEvents(normalHeaderCt);
259 processColumns: function(columns){
260 // split apart normal and lockedWidths
262 len = columns.length,
268 for (; i < len; ++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;
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.");
279 if (!column.hidden) {
280 lockedWidth += column.width || Ext.grid.header.Container.prototype.defaultWidth;
282 lockedHeaders.push(column);
284 normalHeaders.push(column);
286 if (!column.headerId) {
287 column.headerId = (column.initialConfig || column).id || ('L' + (++this.headerCounter));
291 lockedWidth: lockedWidth,
292 locked: lockedHeaders,
293 normal: normalHeaders
297 // create a new spacer after the table is refreshed
298 onLockedGridAfterLayout: function() {
300 lockedView = me.lockedGrid.getView();
302 beforerefresh: me.destroySpacer,
307 // trigger a pseudo refresh on the normal side
308 onLockedHeaderMove: function() {
309 if (this.syncRowHeight) {
310 this.onNormalGridAfterRefresh();
314 // trigger a pseudo refresh on the locked side
315 onNormalHeaderMove: function() {
316 if (this.syncRowHeight) {
317 this.onLockedGridAfterRefresh();
321 // create a spacer in lockedsection and store a reference
322 // TODO: Should destroy before refreshing content
323 getSpacerEl: function() {
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();
336 me.spacerEl = Ext.DomHelper.append(el, {
337 cls: me.spacerHidden ? (Ext.baseCSSPrefix + 'hidden') : '',
338 style: 'height: ' + w + 'px;'
344 destroySpacer: function() {
347 me.spacerEl.destroy();
352 // cache the heights of all locked rows and sync rowheights
353 onLockedGridAfterRefresh: function() {
355 view = me.lockedGrid.getView(),
357 rowEls = el.query(view.getItemSelector()),
361 // reset heights each time.
362 me.lockedHeights = [];
364 for (; i < ln; i++) {
365 me.lockedHeights[i] = rowEls[i].clientHeight;
370 // cache the heights of all normal rows and sync rowheights
371 onNormalGridAfterRefresh: function() {
373 view = me.normalGrid.getView(),
375 rowEls = el.query(view.getItemSelector()),
379 // reset heights each time.
380 me.normalHeights = [];
382 for (; i < ln; i++) {
383 me.normalHeights[i] = rowEls[i].clientHeight;
388 // rows can get bigger/smaller
389 onLockedGridAfterUpdate: function(record, index, node) {
390 this.lockedHeights[index] = node.clientHeight;
391 this.syncRowHeights();
394 // rows can get bigger/smaller
395 onNormalGridAfterUpdate: function(record, index, node) {
396 this.normalHeights[index] = node.clientHeight;
397 this.syncRowHeights();
400 // match the rowheights to the biggest rowheight on either
402 syncRowHeights: function() {
404 lockedHeights = me.lockedHeights,
405 normalHeights = me.normalHeights,
407 ln = lockedHeights.length,
409 lockedView, normalView,
410 lockedRowEls, normalRowEls,
411 vertScroller = me.getVerticalScroller(),
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());
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]);
434 // invalidate the scroller and sync the scrollers
435 me.normalGrid.invalidateScroller();
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);
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;
451 me.lockedHeights = [];
452 me.normalHeights = [];
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');
464 // track when scroller is hidden
465 onScrollerHide: function(scroller, direction) {
466 if (direction === 'horizontal') {
467 this.spacerHidden = true;
469 this.spacerEl.addCls(Ext.baseCSSPrefix + 'hidden');
475 // inject Lock and Unlock text
476 modifyHeaderCt: function() {
478 me.lockedGrid.headerCt.getMenuItems = me.getMenuItems(true);
479 me.normalGrid.headerCt.getMenuItems = me.getMenuItems(false);
482 onUnlockMenuClick: function() {
486 onLockMenuClick: function() {
490 getMenuItems: function(locked) {
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);
499 // runs in the scope of headerCt
501 var o = Ext.grid.header.Container.prototype.getMenuItems.call(this);
505 handler: unlockHandler,
511 handler: lockHandler,
518 // going from unlocked section to locked
520 * Locks the activeHeader as determined by which menu is open OR a header
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.
526 lock: function(activeHd, toIdx) {
528 normalGrid = me.normalGrid,
529 lockedGrid = me.lockedGrid,
530 normalHCt = normalGrid.headerCt,
531 lockedHCt = lockedGrid.headerCt;
533 activeHd = activeHd || normalHCt.getMenu().activeHeader;
535 // if column was previously flexed, get/set current width
536 // and remove the flex
538 activeHd.width = activeHd.getWidth();
539 delete activeHd.flex;
542 normalHCt.remove(activeHd, false);
543 lockedHCt.suspendLayout = true;
544 activeHd.locked = true;
545 if (Ext.isDefined(toIdx)) {
546 lockedHCt.insert(toIdx, activeHd);
548 lockedHCt.add(activeHd);
550 lockedHCt.suspendLayout = false;
551 me.syncLockedSection();
553 me.fireEvent('lockcolumn', me, activeHd);
556 syncLockedSection: function() {
558 me.syncLockedWidth();
559 me.lockedGrid.getView().refresh();
560 me.normalGrid.getView().refresh();
563 // adjust the locked section to the width of its respective
565 syncLockedWidth: function() {
567 width = me.lockedGrid.headerCt.getFullWidth(true);
568 me.lockedGrid.setWidth(width+1); // +1 for border pixel
569 me.doComponentLayout();
572 onLockedHeaderResize: function() {
573 this.syncLockedWidth();
576 onLockedHeaderHide: function() {
577 this.syncLockedWidth();
580 onLockedHeaderShow: function() {
581 this.syncLockedWidth();
584 onLockedHeaderSortChange: function(headerCt, header, sortState) {
586 // no real header, and silence the event so we dont get into an
588 this.normalGrid.headerCt.clearOtherSortStates(null, true);
592 onNormalHeaderSortChange: function(headerCt, header, sortState) {
594 // no real header, and silence the event so we dont get into an
596 this.lockedGrid.headerCt.clearOtherSortStates(null, true);
600 // going from locked section to unlocked
602 * Unlocks the activeHeader as determined by which menu is open OR a header
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.
608 unlock: function(activeHd, toIdx) {
610 normalGrid = me.normalGrid,
611 lockedGrid = me.lockedGrid,
612 normalHCt = normalGrid.headerCt,
613 lockedHCt = lockedGrid.headerCt;
615 if (!Ext.isDefined(toIdx)) {
618 activeHd = activeHd || lockedHCt.getMenu().activeHeader;
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();
627 me.fireEvent('unlockcolumn', me, activeHd);
630 applyColumnsState: function (columns) {
632 lockedGrid = me.lockedGrid,
633 lockedHeaderCt = lockedGrid.headerCt,
634 normalHeaderCt = me.normalGrid.headerCt,
635 lockedCols = lockedHeaderCt.items,
636 normalCols = normalHeaderCt.items,
643 Ext.each(columns, function (col) {
644 function matches (item) {
645 return item.headerId == col.id;
648 lockedDefault = true;
649 if (!(existing = lockedCols.findBy(matches))) {
650 existing = normalCols.findBy(matches);
651 lockedDefault = false;
655 if (existing.applyColumnState) {
656 existing.applyColumnState(col);
658 if (!Ext.isDefined(existing.locked)) {
659 existing.locked = lockedDefault;
661 if (existing.locked) {
662 locked.push(existing);
663 if (!existing.hidden && Ext.isNumber(existing.width)) {
664 lockedWidth += existing.width;
667 normal.push(existing);
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);
677 lockedHeaderCt.add(locked);
678 normalHeaderCt.add(normal);
680 lockedGrid.setWidth(lockedWidth);
684 getColumnsState: function () {
686 locked = me.lockedGrid.headerCt.getColumnsState(),
687 normal = me.normalGrid.headerCt.getColumnsState();
689 return locked.concat(normal);
692 // we want to totally override the reconfigure behaviour here, since we're creating 2 sub-grids
693 reconfigureLockable: function(store, columns) {
695 lockedGrid = me.lockedGrid,
696 normalGrid = me.normalGrid;
699 lockedGrid.headerCt.suspendLayout = true;
700 normalGrid.headerCt.suspendLayout = true;
701 lockedGrid.headerCt.removeAll();
702 normalGrid.headerCt.removeAll();
704 columns = me.processColumns(columns);
705 lockedGrid.setWidth(columns.lockedWidth);
706 lockedGrid.headerCt.add(columns.locked);
707 normalGrid.headerCt.add(columns.normal);
711 store = Ext.data.StoreManager.lookup(store);
713 lockedGrid.bindStore(store);
714 normalGrid.bindStore(store);
716 lockedGrid.getView().refresh();
717 normalGrid.getView().refresh();
721 lockedGrid.headerCt.suspendLayout = false;
722 normalGrid.headerCt.suspendLayout = false;
723 lockedGrid.headerCt.forceComponentLayout();
724 normalGrid.headerCt.forceComponentLayout();