Форк Rambox
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1284 lines
48 KiB

/**
* @private
*
* Lockable is a private mixin which injects lockable behavior into any
* TablePanel subclass such as GridPanel or TreePanel. TablePanel will
* automatically inject the Ext.grid.locking.Lockable mixin in when one of the
* these conditions are met:
*
* - The TablePanel has the lockable configuration set to true
* - One of the columns in the TablePanel has locked set to true/false
*
* Each TablePanel subclass must register an alias. It should have an array
* of configurations to copy to the 2 separate tablepanels that will be generated
* to note what configurations should be copied. These are named normalCfgCopy and
* lockedCfgCopy respectively.
*
* Configurations which are specified in this class will be available on any grid or
* tree which is using the lockable functionality.
*
* By default the two grids, "locked" and "normal" will be arranged using an {@link Ext.layout.container.HBox hbox}
* layout. If the lockable grid is configured with `{@link #split split:true}`, a vertical splitter
* will be placed between the two grids to resize them.
*
* It is possible to override the layout of the lockable grid, or example, you may wish to
* use a border layout and have one of the grids collapsible.
*/
Ext.define('Ext.grid.locking.Lockable', {
alternateClassName: 'Ext.grid.Lockable',
requires: [
'Ext.grid.locking.View',
'Ext.grid.header.Container',
'Ext.grid.locking.HeaderContainer',
'Ext.view.Table'
],
/**
* @cfg {Boolean} syncRowHeight Synchronize rowHeight between the normal and
* locked grid view. This is turned on by default. If your grid is guaranteed
* to have rows of all the same height, you should set this to false to
* optimize performance.
*/
syncRowHeight: true,
/**
* @cfg {String} subGridXType The xtype of the subgrid to specify. If this is
* not specified lockable will determine the subgrid xtype to create by the
* following rule. Use the superclasses xtype if the superclass is NOT
* tablepanel, otherwise use the xtype itself.
*/
/**
* @cfg {Object} lockedViewConfig A view configuration to be applied to the
* locked side of the grid. Any conflicting configurations between lockedViewConfig
* and viewConfig will be overwritten by the lockedViewConfig.
*/
/**
* @cfg {Object} normalViewConfig A view configuration to be applied to the
* normal/unlocked side of the grid. Any conflicting configurations between normalViewConfig
* and viewConfig will be overwritten by the normalViewConfig.
*/
headerCounter: 0,
/**
* @cfg {Number} scrollDelta
* Number of pixels to scroll when scrolling the locked section with mousewheel.
*/
scrollDelta: 40,
/**
* @cfg {Object} lockedGridConfig
* Any special configuration options for the locked part of the grid
*/
/**
* @cfg {Object} normalGridConfig
* Any special configuration options for the normal part of the grid
*/
/**
* @cfg {Boolean} [split=false]
* Configure as true to place a resizing {@link Ext.resizer.Splitter splitter} between the locked
* and unlocked columns.
*/
/**
* @cfg {Object} [layout]
* By default, a lockable grid uses an {@link Ext.layout.container.HBox HBox} layout to arrange
* the two grids (possibly separated by a splitter).
*
* Using this config it is possible to specify a different layout to arrange the two grids.
*/
lockedGridCls: Ext.baseCSSPrefix + 'grid-inner-locked',
normalGridCls: Ext.baseCSSPrefix + 'grid-inner-normal',
// i8n text
//<locale>
unlockText: 'Unlock',
//</locale>
//<locale>
lockText: 'Lock',
//</locale>
// Required for the Lockable Mixin. These are the configurations which will be copied to the
// normal and locked sub tablepanels
bothCfgCopy: [
'invalidateScrollerOnRefresh',
'hideHeaders',
'enableColumnHide',
'enableColumnMove',
'enableColumnResize',
'sortableColumns',
'multiColumnSort',
'columnLines',
'rowLines',
'variableRowHeight',
'numFromEdge',
'trailingBufferZone',
'leadingBufferZone',
'scrollToLoadBuffer'
],
normalCfgCopy: [
'verticalScroller',
'verticalScrollDock',
'verticalScrollerType',
'scroll'
],
lockedCfgCopy: [],
/**
* @event processcolumns
* Fires when the configured (or **reconfigured**) column set is split into two depending on the {@link Ext.grid.column.Column#locked locked} flag.
* @param {Ext.grid.column.Column[]} lockedColumns The locked columns.
* @param {Ext.grid.column.Column[]} normalColumns The normal columns.
*/
/**
* @event lockcolumn
* Fires when a column is locked.
* @param {Ext.grid.Panel} this The gridpanel.
* @param {Ext.grid.column.Column} column The column being locked.
*/
/**
* @event unlockcolumn
* Fires when a column is unlocked.
* @param {Ext.grid.Panel} this The gridpanel.
* @param {Ext.grid.column.Column} column The column being unlocked.
*/
determineXTypeToCreate: function(lockedSide) {
var me = this,
typeToCreate,
xtypes, xtypesLn, xtype, superxtype;
if (me.subGridXType) {
typeToCreate = me.subGridXType;
} else {
// Treeness only moves down into the locked side.
// The normal side is always just a grid
if (!lockedSide) {
return 'gridpanel';
}
xtypes = this.getXTypes().split('/');
xtypesLn = xtypes.length;
xtype = xtypes[xtypesLn - 1];
superxtype = xtypes[xtypesLn - 2];
if (superxtype !== 'tablepanel') {
typeToCreate = superxtype;
} else {
typeToCreate = xtype;
}
}
return typeToCreate;
},
// injectLockable will be invoked before initComponent's parent class implementation
// is called, so throughout this method this. are configurations
injectLockable: function() {
// The child grids are focusable, not this one
this.focusable = false;
// ensure lockable is set to true in the TablePanel
this.lockable = true;
// Instruct the TablePanel it already has a view and not to create one.
// We are going to aggregate 2 copies of whatever TablePanel we are using
this.hasView = true;
var me = this,
scrollbarSize = Ext.getScrollbarSize(),
scrollbarWidth = scrollbarSize.width,
store = me.store = Ext.StoreManager.lookup(me.store),
lockedViewConfig = me.lockedViewConfig,
normalViewConfig = me.normalViewConfig,
Obj = Ext.Object,
// Hash of {lockedFeatures:[],normalFeatures:[]}
allFeatures,
// Hash of {topPlugins:[],lockedPlugins:[],normalPlugins:[]}
allPlugins,
lockedGrid,
normalGrid,
i,
columns,
lockedHeaderCt,
normalHeaderCt,
listeners,
viewConfig = me.viewConfig,
// When setting the loadMask value, the viewConfig wins if it is defined.
loadMaskCfg = viewConfig && viewConfig.loadMask,
loadMask = (loadMaskCfg !== undefined) ? loadMaskCfg : me.loadMask,
bufferedRenderer = me.bufferedRenderer,
clipVertLockedScrollbar = scrollbarWidth > 0 && Ext.supports.touchScroll !== 2;
allFeatures = me.constructLockableFeatures();
// This is just a "shell" Panel which acts as a Container for the two grids and must not use the features
me.features = null;
// Distribute plugins to whichever Component needs them
allPlugins = me.constructLockablePlugins();
me.plugins = allPlugins.topPlugins;
lockedGrid = {
id: me.id + '-locked',
isLocked: true,
bufferedRenderer: bufferedRenderer,
ownerGrid: me,
ownerLockable: me,
xtype: me.determineXTypeToCreate(true),
store: store,
// if the browser's scrollbars take up space we always reserve space for the
// vertical scrollbar on the locked side. This allows us to hide the vertical
// scrollbar by clipping it using the locked grid's body element.
reserveScrollbar: clipVertLockedScrollbar,
scrollable: {
indicators: {
x: true,
y: false
}
},
scrollerOwner: false,
// Lockable does NOT support animations for Tree
// Because the right side is just a grid, and the grid view doen't animate bulk insertions/removals
animate: false,
border: false,
cls: me.lockedGridCls,
// Usually a layout in one side necessitates the laying out of the other side even if each is fully
// managed in both dimensions, and is therefore a layout root.
// The only situation that we do *not* want layouts to escape into the owning lockable assembly
// is when using a border layout and any of the border regions is floated from a collapsed state.
isLayoutRoot: function() {
return this.floatedFromCollapse || me.normalGrid.floatedFromCollapse;
},
features: allFeatures.lockedFeatures,
plugins: allPlugins.lockedPlugins
};
normalGrid = {
id: me.id + '-normal',
isLocked: false,
bufferedRenderer: bufferedRenderer,
ownerGrid: me,
ownerLockable: me,
xtype: me.determineXTypeToCreate(),
store: store,
// Pass down our reserveScrollbar to the normal side:
reserveScrollbar: me.reserveScrollbar,
scrollerOwner: false,
border: false,
cls: me.normalGridCls,
// As described above, isolate layouts when floated out from a collapsed border region.
isLayoutRoot: function() {
return this.floatedFromCollapse || me.lockedGrid.floatedFromCollapse;
},
features: allFeatures.normalFeatures,
plugins: allPlugins.normalPlugins
};
me.addCls(Ext.baseCSSPrefix + 'grid-locked');
// Copy appropriate configurations to the respective aggregated tablepanel instances.
// Pass 4th param true to NOT exclude those settings on our prototype.
// Delete them from the master tablepanel.
Ext.copyTo(normalGrid, me, me.bothCfgCopy, true);
Ext.copyTo(lockedGrid, me, me.bothCfgCopy, true);
Ext.copyTo(normalGrid, me, me.normalCfgCopy, true);
Ext.copyTo(lockedGrid, me, me.lockedCfgCopy, true);
Ext.apply(normalGrid, me.normalGridConfig);
Ext.apply(lockedGrid, me.lockedGridConfig);
for (i = 0; i < me.normalCfgCopy.length; i++) {
delete me[me.normalCfgCopy[i]];
}
for (i = 0; i < me.lockedCfgCopy.length; i++) {
delete me[me.lockedCfgCopy[i]];
}
me.addStateEvents(['lockcolumn', 'unlockcolumn']);
columns = me.processColumns(me.columns || [], lockedGrid);
lockedGrid.columns = columns.locked;
// If no locked columns, hide the locked grid
if (!lockedGrid.columns.items.length) {
lockedGrid.hidden = true;
}
normalGrid.columns = columns.normal;
if (!normalGrid.columns.items.length) {
normalGrid.hidden = true;
}
// normal grid should flex the rest of the width
normalGrid.flex = 1;
// Chain view configs to avoid mutating user's config
lockedGrid.viewConfig = lockedViewConfig = (lockedViewConfig ? Obj.chain(lockedViewConfig) : {});
normalGrid.viewConfig = normalViewConfig = (normalViewConfig ? Obj.chain(normalViewConfig) : {});
lockedViewConfig.loadingUseMsg = false;
lockedViewConfig.loadMask = false;
if (clipVertLockedScrollbar) {
lockedViewConfig.margin = '0 -' + scrollbarWidth + ' 0 0';
}
normalViewConfig.loadMask = false;
//<debug>
if (viewConfig && viewConfig.id) {
Ext.log.warn('id specified on Lockable viewConfig, it will be shared between both views: "' + viewConfig.id + '"');
}
//</debug>
Ext.applyIf(lockedViewConfig, viewConfig);
Ext.applyIf(normalViewConfig, viewConfig);
// Allow developer to configure the layout.
// Instantiate the layout so its type can be ascertained.
if (!me.initialConfig.layout) {
me.layout = {
type: 'hbox',
align: 'stretch'
};
}
me.getLayout();
// Sanity check the split config.
// Only allowed to insert a splitter between the two grids if it's a box layout
if (me.layout.type === 'border') {
if (me.split) {
lockedGrid.split = true;
}
if (!lockedGrid.region) {
lockedGrid.region = 'west';
}
if (!normalGrid.region) {
normalGrid.region = 'center';
}
me.addCls(Ext.baseCSSPrefix + 'grid-locked-split');
}
if (!(me.layout instanceof Ext.layout.container.Box)) {
me.split = false;
}
// The LockingView is a pseudo view which owns the two grids.
// It listens for store events and relays the calls into each view bracketed by a layout suspension.
me.view = new Ext.grid.locking.View({
loadMask: loadMask,
locked: lockedGrid,
normal: normalGrid,
ownerGrid: me
});
// after creating the locking view we now have Grid instances for both locked and
// unlocked sides
lockedGrid = me.lockedGrid;
normalGrid = me.normalGrid;
// make the locked/unlocked sides mirror each other's vertical scroll positions.
normalGrid.getView().getScrollable().addPartner(lockedGrid.getView().getScrollable(), 'y');
// Extract the instantiated views from the locking View.
// The locking View injects lockingGrid and normalGrid into this lockable panel.
// This is because during constraction, it must be possible for descendant components
// to navigate up to the owning lockable panel and then down into either side.
// If there are system scrollbars, we have to monitor the mousewheel and fake a scroll
// Also we need to postprocess the border width because of inline border setting styles.
// The locked grid needs a bottom border to match with any scrollbar present in the normal grid .
// Keep locked section's bottom border width synched
if (scrollbarSize.height && Ext.supports.touchScroll !== 2) {
lockedGrid.on({
afterlayout: me.afterLockedViewLayout,
scope: me
});
// Ensure the overflow flags have been calculated from the various overflow configs
lockedGrid.getView().getOverflowStyle();
}
lockedHeaderCt = lockedGrid.headerCt;
normalHeaderCt = normalGrid.headerCt;
if (clipVertLockedScrollbar) {
// if we are clipping the locked vertical scrollbar, we do not want the
// headerCt to reserve room for one
lockedHeaderCt.reserveScrollbar = false;
}
// The top grid, and the LockingView both need to have a headerCt which is usable.
// It is part of their private API that framework code uses when dealing with a grid or grid view
me.headerCt = me.view.headerCt = new Ext.grid.locking.HeaderContainer(me);
lockedHeaderCt.lockedCt = true;
lockedHeaderCt.lockableInjected = true;
normalHeaderCt.lockableInjected = true;
lockedHeaderCt.on({
add: me.delaySyncLockedWidth,
remove: me.delaySyncLockedWidth,
columnshow: me.delaySyncLockedWidth,
columnhide: me.delaySyncLockedWidth,
sortchange: me.onLockedHeaderSortChange,
columnresize: me.delaySyncLockedWidth,
scope: me
});
normalHeaderCt.on({
add: me.delaySyncLockedWidth,
remove: me.delaySyncLockedWidth,
columnshow: me.delaySyncLockedWidth,
columnhide: me.delaySyncLockedWidth,
sortchange: me.onNormalHeaderSortChange,
scope: me
});
me.modifyHeaderCt();
me.items = [lockedGrid];
if (me.split) {
me.addCls(Ext.baseCSSPrefix + 'grid-locked-split');
me.items[1] = {
xtype: 'splitter'
};
}
me.items.push(normalGrid);
me.relayHeaderCtEvents(lockedHeaderCt);
me.relayHeaderCtEvents(normalHeaderCt);
// The top level Lockable container does not get bound to the store, so we need to programatically add the relayer so that
// The filterchange state event is fired.
me.storeRelayers = me.relayEvents(store, [
/**
* @event filterchange
* @inheritdoc Ext.data.Store#filterchange
*/
'filterchange',
/**
* @event groupchange
* @inheritdoc Ext.data.Store#groupchange
*/
'groupchange'
]);
// Only need to relay from the normalGrid. Since it's created after the lockedGrid,
// we can be confident to only listen to it.
me.gridRelayers = me.relayEvents(normalGrid, [
/**
* @event viewready
* @inheritdoc Ext.panel.Table#viewready
*/
'viewready'
]);
},
getLockingViewConfig: function(){
return {
xclass: 'Ext.grid.locking.View',
locked: this.lockedGrid,
normal: this.normalGrid,
panel: this
};
},
processColumns: function(columns, lockedGrid) {
// split apart normal and locked
var me = this,
i,
len,
column,
cp = new Ext.grid.header.Container(),
lockedHeaders = [],
normalHeaders = [],
lockedHeaderCt = {
itemId: 'lockedHeaderCt',
stretchMaxPartner: '^^>>#normalHeaderCt',
items: lockedHeaders
},
normalHeaderCt = {
itemId: 'normalHeaderCt',
stretchMaxPartner: '^^>>#lockedHeaderCt',
items: normalHeaders
},
result = {
lockedWidth: lockedGrid.width || 0,
locked: lockedHeaderCt,
normal: normalHeaderCt
},
shrinkWrapLocked = !(lockedGrid.width || lockedGrid.flex),
copy;
// Only save the initial configuration, since a width will be stamped on
// after we sync the width.
if (!me.hasOwnProperty('shrinkWrapLocked')) {
me.shrinkWrapLocked = shrinkWrapLocked;
}
// In case they specified a config object with items...
if (Ext.isObject(columns)) {
Ext.applyIf(lockedHeaderCt, columns);
Ext.applyIf(normalHeaderCt, columns);
copy = Ext.apply({}, columns);
delete copy.items;
Ext.apply(cp, copy);
columns = columns.items;
}
// Treat the column header as though we're just creating an instance, since this
// doesn't follow the normal column creation pattern
cp.constructing = true;
for (i = 0, len = columns.length; i < len; ++i) {
column = columns[i];
// Use the HeaderContainer object to correctly configure and create the column.
// MUST instantiate now because the locked or autoLock config which we read here might be in the prototype.
// MUST use a Container instance so that defaults from an object columns config get applied.
if (!column.isComponent) {
column = cp.applyDefaults(column);
column.initOwnerCt = cp;
column = cp.lookupComponent(column);
delete column.initOwnerCt;
}
// mark the column as processed so that the locked attribute does not
// trigger the locked subgrid to try to become a split lockable grid itself.
column.processed = true;
if (column.locked || column.autoLock) {
// If the locked grid has not been configured with a width, we must
// Calculate a width from the total width of locked columns
if (shrinkWrapLocked && !column.hidden) {
result.lockedWidth += me.getColumnWidth(column) || cp.defaultWidth;
}
lockedHeaders.push(column);
} else {
normalHeaders.push(column);
}
if (!column.headerId) {
column.headerId = (column.initialConfig || column).id || ('h' + (++me.headerCounter));
}
}
me.fireEvent('processcolumns', me, lockedHeaders, normalHeaders);
cp.destroy();
// If grid has not been configured with a width it must shrinkwrap columns with no horiontal scroll
// TODO: Use shrinkWrapDock on the locked grid when it works.
if (shrinkWrapLocked) {
lockedGrid.width = result.lockedWidth;
}
return result;
},
// Used when calculating total locked column width in processColumns
// Use shrinkwrapping of child columns if no top level width.
getColumnWidth: function(column) {
var result = column.width || 0,
subcols, len, i;
// <debug>
if (column.flex) {
Ext.Error.raise("Locked columns in an unsized locked side do NOT support a flex width. You must set a width on the " + column.text + "column.");
}
// </debug>
if (!result && column.isGroupHeader) {
subcols = column.items.items;
len = subcols.length;
for (i = 0; i < len; i++) {
result += this.getColumnWidth(subcols[i]);
}
}
return result;
},
// Due to automatic component border setting using inline style, to create the scrollbar-replacing
// bottom border, we have to postprocess the locked view *after* render.
// A tall bottom border takes the place of a horiz scrollbar if the opposite side has a horiz scrollbar.
// When we can use overflow-x: scroll to create a matching scrollbar, we do this instead.
afterLockedViewLayout: function() {
var me = this,
lockedGrid = me.lockedGrid,
normalGrid = me.normalGrid,
lockedView = lockedGrid.getView(),
normalView = normalGrid.getView(),
spacerHeight = Ext.getScrollbarSize().height,
lockedViewHorizScrollBar = (lockedView.scrollFlags.x && lockedGrid.headerCt.tooNarrow ? spacerHeight : 0),
normalViewHorizScrollBar = (normalView.scrollFlags.x && normalGrid.headerCt.tooNarrow ? spacerHeight : 0),
normalScroller = normalView.getScrollable(),
lockedScroller = lockedView.getScrollable();
if (lockedViewHorizScrollBar !== normalViewHorizScrollBar) {
if (lockedViewHorizScrollBar) {
normalScroller.setX('scroll');
lockedScroller.setX(true);
} else {
lockedScroller.setX('scroll');
normalScroller.setX(true);
}
} else {
lockedScroller.setX(normalViewHorizScrollBar ? 'scroll' : true);
normalScroller.setX(true);
}
},
ensureLockedVisible: function() {
this.lockedGrid.ensureVisible.apply(this.lockedGrid, arguments);
this.normalGrid.ensureVisible.apply(this.normalGrid, arguments);
},
onLockedViewMouseWheel: function(e) {
var me = this,
deltaY = -me.scrollDelta * e.getWheelDeltas().y,
lockedView = me.lockedGrid.getView(),
lockedViewElDom = lockedView.el.dom,
scrollTop, verticalCanScrollDown, verticalCanScrollUp;
if (!me.ignoreMousewheel) {
if (lockedViewElDom) {
scrollTop = lockedView.getScrollY();
verticalCanScrollDown = scrollTop !== lockedViewElDom.scrollHeight - lockedViewElDom.clientHeight;
verticalCanScrollUp = scrollTop !== 0;
}
if ((deltaY < 0 && verticalCanScrollUp) || (deltaY > 0 && verticalCanScrollDown)) {
e.stopEvent();
// Inhibit processing of any scroll events we *may* cause here.
// Some OSs do not fire a scroll event when we set the scrollTop of an overflow:hidden element,
// so we invoke the scroll handler programatically below.
scrollTop += deltaY;
lockedView.setScrollY(scrollTop);
me.normalGrid.getView().setScrollY(scrollTop);
// Invoke the scroll event handler programatically to sync everything.
me.onNormalViewScroll();
}
}
},
onLockedViewScroll: function() {
var me = this,
lockedView = me.lockedGrid.getView(),
normalView = me.normalGrid.getView(),
lockedScrollTop = lockedView.getScrollY(),
normalScrollTop = normalView.getScrollY(),
normalTable,
lockedTable;
if (normalScrollTop !== lockedScrollTop) {
normalView.setScrollY(lockedScrollTop);
// For buffered views, the absolute position is important as well as scrollTop
if (normalView.bufferedRenderer) {
lockedTable = lockedView.body.dom;
normalTable = normalView.body.dom;
normalTable.style.position = 'absolute';
normalTable.style.top = lockedTable.style.top;
}
}
},
onNormalViewScroll: function() {
var me = this,
lockedView = me.lockedGrid.getView(),
normalView = me.normalGrid.getView(),
lockedScrollTop = lockedView.getScrollY(),
normalScrollTop = normalView.getScrollY(),
lockedRowContainer;
if (normalScrollTop !== lockedScrollTop) {
lockedView.setScrollY(normalScrollTop);
// For buffered views, the absolute position is important as well as scrollTop
if (normalView.bufferedRenderer) {
lockedRowContainer = lockedView.body;
// If we have attached the Fly to a DOM (will not have happened if all locked columns are hidden)
if (lockedRowContainer.dom) {
lockedRowContainer.dom.style.position = 'absolute';
lockedRowContainer.translate(null, normalView.bufferedRenderer.bodyTop);
}
}
}
},
/**
* Synchronizes the row heights between the locked and non locked portion of the grid for each
* row. If one row is smaller than the other, the height will be increased to match the larger one.
*/
syncRowHeights: function() {
// This is now called on animationFrame. It may have been destroyed in the interval.
if (!this.isDestroyed) {
var me = this,
normalView = me.normalGrid.getView(),
lockedView = me.lockedGrid.getView(),
// These will reset any forced height styles from the last sync
normalSync = normalView.syncRowHeightBegin(),
lockedSync = lockedView.syncRowHeightBegin(),
scrollTop;
// Now bulk measure everything
normalView.syncRowHeightMeasure(normalSync);
lockedView.syncRowHeightMeasure(lockedSync);
// Now write out all the explicit heights we need to sync up
normalView.syncRowHeightFinish(normalSync, lockedSync);
lockedView.syncRowHeightFinish(lockedSync, normalSync);
// Synchronize the scrollTop positions of the two views
scrollTop = normalView.getScrollY();
lockedView.setScrollY(scrollTop);
}
},
// inject Lock and Unlock text
// Hide/show Lock/Unlock options
modifyHeaderCt: function() {
var me = this;
me.lockedGrid.headerCt.getMenuItems = me.getMenuItems(me.lockedGrid.headerCt.getMenuItems, true);
me.normalGrid.headerCt.getMenuItems = me.getMenuItems(me.normalGrid.headerCt.getMenuItems, false);
me.lockedGrid.headerCt.showMenuBy = Ext.Function.createInterceptor(me.lockedGrid.headerCt.showMenuBy, me.showMenuBy);
me.normalGrid.headerCt.showMenuBy = Ext.Function.createInterceptor(me.normalGrid.headerCt.showMenuBy, me.showMenuBy);
},
onUnlockMenuClick: function() {
this.unlock();
},
onLockMenuClick: function() {
this.lock();
},
showMenuBy: function(clickEvent, t, header) {
var menu = this.getMenu(),
unlockItem = menu.down('#unlockItem'),
lockItem = menu.down('#lockItem'),
sep = unlockItem.prev();
if (header.lockable === false) {
sep.hide();
unlockItem.hide();
lockItem.hide();
} else {
sep.show();
unlockItem.show();
lockItem.show();
if (!unlockItem.initialConfig.disabled) {
unlockItem.setDisabled(header.lockable === false);
}
if (!lockItem.initialConfig.disabled) {
lockItem.setDisabled(!header.isLockable());
}
}
},
getMenuItems: function(getMenuItems, locked) {
var me = this,
unlockText = me.unlockText,
lockText = me.lockText,
unlockCls = Ext.baseCSSPrefix + 'hmenu-unlock',
lockCls = Ext.baseCSSPrefix + 'hmenu-lock',
unlockHandler = me.onUnlockMenuClick.bind(me),
lockHandler = me.onLockMenuClick.bind(me);
// runs in the scope of headerCt
return function() {
// We cannot use the method from HeaderContainer's prototype here
// because other plugins or features may already have injected an implementation
var o = getMenuItems.call(this);
o.push('-', {
itemId: 'unlockItem',
iconCls: unlockCls,
text: unlockText,
handler: unlockHandler,
disabled: !locked
});
o.push({
itemId: 'lockItem',
iconCls: lockCls,
text: lockText,
handler: lockHandler,
disabled: locked
});
return o;
};
},
//<debug>
syncTaskDelay: 1,
//</debug>
delaySyncLockedWidth: function() {
var me = this,
task = me.syncLockedWidthTask;
if (!task) {
task = me.syncLockedWidthTask = new Ext.util.DelayedTask(me.syncLockedWidth, me);
}
// Only useful for unit testing so we don't have to mess around with timers
//<debug>
if (me.syncTaskDelay === 0) {
me.syncLockedWidth();
} else {
//</debug>
task.delay(1);
//<debug>
}
//</debug>
},
/**
* @private
* Updates the overall view after columns have been resized, or moved from
* the locked to unlocked side or vice-versa.
*
* If all columns are removed from either side, that side must be hidden, and the
* sole remaining column owning grid then becomes *the* grid. It must flex to occupy the
* whole of the locking view. And it must also allow scrolling.
*
* If columns are shared between the two sides, the *locked* grid shrinkwraps the
* width of the visible locked columns while the normal grid flexes in what space remains.
*
* @return {Boolean} `true` if there are visible locked columns which need refreshing.
*
*/
syncLockedWidth: function() {
var me = this,
rendered = me.rendered,
locked = me.lockedGrid,
lockedView = locked.view,
normal = me.normalGrid,
lockedColCount = locked.getVisibleColumnManager().getColumns().length,
normalColCount = normal.getVisibleColumnManager().getColumns().length,
task = me.syncLockedWidthTask;
// If we are called directly, veto any existing task.
if (task) {
task.cancel();
}
Ext.suspendLayouts();
// If there are still visible normal columns, then the normal grid will flex
// while we effectively shrinkwrap the width of the locked columns
if (normalColCount) {
normal.show();
if (lockedColCount) {
// The locked grid shrinkwraps the total column width while the normal grid flexes in what remains
// UNLESS it has been set to forceFit
if (rendered && me.shrinkWrapLocked && !locked.headerCt.forceFit) {
delete locked.flex;
// Don't pass the purge flag here
locked.setWidth(locked.headerCt.getTableWidth() + locked.el.getBorderWidth('lr'));
}
locked.addCls(me.lockedGridCls);
locked.show();
if (me.split) {
me.child('splitter').show();
}
} else {
// No visible locked columns: hide the locked grid
// We also need to trigger a clearViewEl to clear out any
// old dom nodes
if (rendered) {
locked.getView().clearViewEl(true);
}
locked.hide();
if (me.split) {
me.child('splitter').hide();
}
}
// Only if there is going to be an upcoming layout to correct the horizontal scrollbar setting.
if (Ext.supports.touchScroll !== 2 && Ext.Component.pendingLayouts) {
// We may have previously set horizontal placeholder scrollbar on the locked
// view to match the unlocked side. Undo this before continuing, so that
// the horizontal scrollbar does not affect the layout of the columns by
// possibly triggering a vertical scrollbar as well
lockedView.getScrollable().setX(true);
}
// Ignore mousewheel events if the view is configured to scroll vertically
if (rendered) {
me.ignoreMousewheel = lockedView.scrollFlags.y;
}
}
// There are no normal grid columns. The "locked" grid has to be *the*
// grid, and cannot have a shrinkwrapped width, but must flex the entire width.
else {
normal.hide();
// When the normal grid is hidden, we no longer need the bottom border "scrollbar replacement"
if (rendered) {
lockedView.getEl().setStyle('border-bottom-width', '0');
}
// The locked now becomes *the* grid and has to flex to occupy the full view width
locked.flex = 1;
delete locked.width;
locked.removeCls(me.lockedGridCls);
locked.show();
me.ignoreMousewheel = true;
}
Ext.resumeLayouts(true);
return [lockedColCount, normalColCount];
},
onLockedHeaderSortChange: Ext.emptyFn,
onNormalHeaderSortChange: Ext.emptyFn,
// going from unlocked section to locked
/**
* Locks the activeHeader as determined by which menu is open OR a header
* as specified.
* @param {Ext.grid.column.Column} [header] Header to unlock from the locked section.
* Defaults to the header which has the menu open currently.
* @param {Number} [toIdx] The index to move the unlocked header to.
* Defaults to appending as the last item.
* @private
*/
lock: function(activeHd, toIdx, toCt) {
var me = this,
normalGrid = me.normalGrid,
lockedGrid = me.lockedGrid,
normalView = normalGrid.view,
lockedView = lockedGrid.view,
normalHCt = normalGrid.headerCt,
refreshFlags,
ownerCt,
hadFocus;
activeHd = activeHd || normalHCt.getMenu().activeHeader;
hadFocus = activeHd.hasFocus;
toCt = toCt || lockedGrid.headerCt;
ownerCt = activeHd.ownerCt;
// isLockable will test for making the locked side too wide
if (!activeHd.isLockable()) {
return;
}
// if column was previously flexed, get/set current width
// and remove the flex
if (activeHd.flex) {
activeHd.width = activeHd.getWidth();
activeHd.flex = null;
}
Ext.suspendLayouts();
// If hidden, we need to show it now or the locked headerCt's .gridVisibleColumns may be out of sync as
// headers are only added to a visible manager if they are not explicity hidden or hierarchically hidden.
if (lockedGrid.hidden) {
lockedGrid.show();
}
// We decide which views to refresh. Do not let the grids do it in response to column changes
normalView.blockRefresh = lockedView.blockRefresh = true;
ownerCt.remove(activeHd, false);
activeHd.locked = true;
// Flag to the locked column add listener to do nothing
if (Ext.isDefined(toIdx)) {
toCt.insert(toIdx, activeHd);
} else {
toCt.add(activeHd);
}
normalView.blockRefresh = lockedView.blockRefresh = false;
refreshFlags = me.syncLockedWidth();
if (refreshFlags[0]) {
lockedGrid.getView().refreshView();
}
if (refreshFlags[1]) {
normalGrid.getView().refreshView();
}
Ext.resumeLayouts(true);
if (hadFocus) {
activeHd.focus();
}
me.fireEvent('lockcolumn', me, activeHd);
},
// going from locked section to unlocked
/**
* Unlocks the activeHeader as determined by which menu is open OR a header
* as specified.
* @param {Ext.grid.column.Column} [header] Header to unlock from the locked section.
* Defaults to the header which has the menu open currently.
* @param {Number} [toIdx=0] The index to move the unlocked header to.
* @private
*/
unlock: function(activeHd, toIdx, toCt) {
var me = this,
normalGrid = me.normalGrid,
lockedGrid = me.lockedGrid,
normalView = normalGrid.view,
lockedView = lockedGrid.view,
lockedHCt = lockedGrid.headerCt,
refreshFlags,
hadFocus;
// Unlocking; user expectation is that the unlocked column is inserted at the beginning.
if (!Ext.isDefined(toIdx)) {
toIdx = 0;
}
activeHd = activeHd || lockedHCt.getMenu().activeHeader;
hadFocus = activeHd.hasFocus;
toCt = toCt || normalGrid.headerCt;
Ext.suspendLayouts();
// We decide which views to refresh. Do not let the grids do it in response to column changes
normalView.blockRefresh = lockedView.blockRefresh = true;
activeHd.ownerCt.remove(activeHd, false);
activeHd.locked = false;
toCt.insert(toIdx, activeHd);
normalView.blockRefresh = lockedView.blockRefresh = false;
// syncLockedWidth returns visible column counts for both grids.
// only refresh what needs refreshing
refreshFlags = me.syncLockedWidth();
if (refreshFlags[0]) {
lockedGrid.getView().refreshView();
}
if (refreshFlags[1]) {
normalGrid.getView().refreshView();
}
Ext.resumeLayouts(true);
if (hadFocus) {
activeHd.focus();
}
me.fireEvent('unlockcolumn', me, activeHd);
},
// we want to totally override the reconfigure behaviour here, since we're creating 2 sub-grids
reconfigureLockable: function(store, columns) {
var me = this,
oldStore = me.store,
lockedGrid = me.lockedGrid,
normalGrid = me.normalGrid,
view;
// Note that we need to process the store first in case one or more passed columns (if there are any)
// have active gridfilters with values which would filter the currently-bound store.
if (store && store !== oldStore) {
store = Ext.data.StoreManager.lookup(store);
me.store = store;
lockedGrid.view.blockRefresh = normalGrid.view.blockRefresh = true;
lockedGrid.bindStore(store);
// Subsidiary views have their bindStore changed because they must not
// bind listeners themselves. This view listens and relays calls to each view.
// BUT the dataSource and store properties must be set
view = lockedGrid.view;
view.store = store;
// If the dataSource being used by the View is *not* a FeatureStore
// (a modified view of the base Store injected by a Feature)
// Then we promote the store to be the dataSource.
// If it was a FeatureStore, then it must not be changed. A FeatureStore is mutated
// by the Feature to respond to changes in the underlying Store.
if (!view.dataSource.isFeatureStore) {
view.dataSource = store;
}
if (view.bufferedRenderer) {
view.bufferedRenderer.bindStore(store);
}
normalGrid.bindStore(store);
view = normalGrid.view;
view.store = store;
// If the dataSource being used by the View is *not* a FeatureStore
// (a modified view of the base Store injected by a Feature)
// Then we promote the store to be the dataSource.
// If it was a FeatureStore, then it must not be changed. A FeatureStore is mutated
// by the Feature to respond to changes in the underlying Store.
if (!view.dataSource.isFeatureStore) {
view.dataSource = store;
}
if (view.bufferedRenderer) {
view.bufferedRenderer.bindStore(store);
}
me.view.store = store;
me.view.bindStore(normalGrid.view.dataSource, false, 'dataSource');
lockedGrid.view.blockRefresh = normalGrid.view.blockRefresh = false;
}
if (columns) {
// Both grids must not react to the headers being changed (See panel/Table#onHeadersChanged)
lockedGrid.reconfiguring = normalGrid.reconfiguring = true;
lockedGrid.headerCt.removeAll();
normalGrid.headerCt.removeAll();
columns = me.processColumns(columns, lockedGrid);
// Flag to the locked column add listener to do nothing
lockedGrid.headerCt.add(columns.locked.items);
normalGrid.headerCt.add(columns.normal.items);
lockedGrid.reconfiguring = normalGrid.reconfiguring = false;
// Ensure locked grid is set up correctly with correct width and bottom border,
// and that both grids' visibility and scrollability status is correct
me.syncLockedWidth();
}
me.refreshCounter = lockedGrid.view.refreshCounter;
},
afterReconfigureLockable: function() {
var lockedView = this.lockedGrid.getView();
// If the counter hasn't changed since where we saved it previously, we haven't refreshed,
// so kick it off now.
if (this.refreshCounter === lockedView.refreshCounter) {
this.view.refresh();
}
},
constructLockableFeatures: function() {
var features = this.features,
feature,
featureClone,
lockedFeatures,
normalFeatures,
i = 0, len;
if (features) {
if (!Ext.isArray(features)) {
features = [ features ];
}
lockedFeatures = [];
normalFeatures = [];
len = features.length;
for (; i < len; i++) {
feature = features[i];
if (!feature.isFeature) {
feature = Ext.create('feature.' + feature.ftype, feature);
}
switch (feature.lockableScope) {
case 'locked':
lockedFeatures.push(feature);
break;
case 'normal':
normalFeatures.push(feature);
break;
default:
feature.lockableScope = 'both';
lockedFeatures.push(feature);
normalFeatures.push(featureClone = feature.clone());
// When cloned to either side, each gets a "lockingPartner" reference to the other
featureClone.lockingPartner = feature;
feature.lockingPartner = featureClone;
}
}
}
return {
normalFeatures: normalFeatures,
lockedFeatures: lockedFeatures
};
},
constructLockablePlugins: function() {
var plugins = this.plugins,
plugin,
normalPlugin,
lockedPlugin,
topPlugins,
lockedPlugins,
normalPlugins,
i = 0, len,
lockableScope,
pluginCls;
if (plugins) {
if (!Ext.isArray(plugins)) {
plugins = [ plugins ];
}
topPlugins = [];
lockedPlugins = [];
normalPlugins = [];
len = plugins.length;
for (; i < len; i++) {
plugin = plugins[i];
// Plugin will most likely already have been instantiated by the Component constructor
if (plugin.init) {
lockableScope = plugin.lockableScope;
}
// If not, it's because of late addition through a subclass's initComponent implementation, so we
// must ascertain the lockableScope directly from the class.
else {
pluginCls = plugin.ptype ? Ext.ClassManager.getByAlias(('plugin.' + plugin.ptype)) : Ext.ClassManager.get(plugin.xclass);
lockableScope = pluginCls.prototype.lockableScope;
}
switch (lockableScope) {
case 'both':
lockedPlugins.push(lockedPlugin = plugin.clonePlugin());
normalPlugins.push(normalPlugin = plugin.clonePlugin());
// When cloned to both sides, each gets a "lockingPartner" reference to the other
lockedPlugin.lockingPartner = normalPlugin;
normalPlugin.lockingPartner = lockedPlugin;
// If the plugin has to be propagated down to both, a new plugin config object must be given to that side
// and this plugin must be destroyed.
Ext.destroy(plugin);
break;
case 'locked':
lockedPlugins.push(plugin);
break;
case 'normal':
normalPlugins.push(plugin);
break;
default:
topPlugins.push(plugin);
}
}
}
return {
topPlugins: topPlugins,
normalPlugins: normalPlugins,
lockedPlugins: lockedPlugins
};
},
destroyLockable: function(){
// The locking view isn't a "real" view, so we need to destroy it manually
var me = this,
task = me.syncLockedWidthTask;
if (task) {
task.cancel();
me.syncLockedWidthTask = null;
}
Ext.destroy(me.view, me.headerCt);
}
}, function() {
this.borrow(Ext.Component, ['constructPlugin']);
});