/** * @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', 'Ext.scroll.LockingScroller' ], /** * @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 {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/Object} [split=false] * Configure as `true` to place a resizing {@link Ext.resizer.Splitter splitter} * between the locked and unlocked columns. May also be a configuration object for the Splitter. */ /** * @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. */ /** * @cfg stateEvents * @inheritdoc Ext.state.Stateful#cfg-stateEvents * @localdoc Adds the following stateEvents: * * - {@link #event-lockcolumn} * - {@link #event-unlockcolumn} */ lockedGridCls: Ext.baseCSSPrefix + 'grid-inner-locked', normalGridCls: Ext.baseCSSPrefix + 'grid-inner-normal', lockingBodyCls: Ext.baseCSSPrefix + 'grid-locking-body', scrollContainerCls: Ext.baseCSSPrefix + 'grid-scroll-container', scrollBodyCls: Ext.baseCSSPrefix + 'grid-scroll-body', scrollbarClipperCls: Ext.baseCSSPrefix + 'grid-scrollbar-clipper', scrollbarCls: Ext.baseCSSPrefix + 'grid-scrollbar', scrollbarVisibleCls: Ext.baseCSSPrefix + 'grid-scrollbar-visible', /** * @cfg {String} lockText * The text to display on the column menu to lock a column. * @locale */ lockText: 'Lock', /** * @cfg {String} unlockText * The text to display on the column menu to unlock a column. * @locale */ unlockText: 'Unlock', // Required for the Lockable Mixin. These are the configurations which will be copied to the // normal and locked sub tablepanels bothCfgCopy: [ 'hideHeaders', 'enableColumnHide', 'enableColumnMove', 'enableColumnResize', 'sortableColumns', 'multiColumnSort', 'columnLines', 'rowLines', 'variableRowHeight', 'numFromEdge', 'trailingBufferZone', 'leadingBufferZone', 'scrollToLoadBuffer', 'syncRowHeight' ], normalCfgCopy: [ '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; if (me.subGridXType) { return me.subGridXType; } else if (!lockedSide) { // Tree columns only moves down into the locked side. // The normal side is always just a grid return 'gridpanel'; } return me.isXType('treepanel') ? 'treepanel' : 'gridpanel'; }, // 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; // eslint-disable-next-line vars-on-top var me = this, store = me.store = Ext.StoreManager.lookup(me.store), lockedViewConfig = me.lockedViewConfig, normalViewConfig = me.normalViewConfig, 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, // Hash of {lockedFeatures:[],normalFeatures:[]} allFeatures, // Hash of {topPlugins:[],lockedPlugins:[],normalPlugins:[]} allPlugins, lockedGrid, normalGrid, columns, lockedHeaderCt, normalHeaderCt, setWidth, i; allFeatures = me.constructLockableFeatures(); // Must be available early. The BufferedRenderer needs access to it to add // scroll listeners. me.scrollable = new Ext.scroll.LockingScroller({ component: me, x: false, y: true }); // 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', $initParent: me, isLocked: true, bufferedRenderer: bufferedRenderer, ownerGrid: me, ownerLockable: me, xtype: me.determineXTypeToCreate(true), store: store, 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 || this.ownerGrid.normalGrid.floatedFromCollapse; }, features: allFeatures.lockedFeatures, plugins: allPlugins.lockedPlugins }; normalGrid = { id: me.id + '-normal', $initParent: me, 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 || this.ownerGrid.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.copy(normalGrid, me, me.bothCfgCopy, true); Ext.copy(lockedGrid, me, me.bothCfgCopy, true); Ext.copy(normalGrid, me, me.normalCfgCopy, true); Ext.copy(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 = Ext.apply({ usageBitMask: 2 }, lockedViewConfig); normalGrid.viewConfig = normalViewConfig = Ext.apply({}, normalViewConfig); lockedViewConfig.loadingUseMsg = false; lockedViewConfig.loadMask = false; 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.layout === Ext.panel.Table.prototype.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 = me.split; } 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 }); me.view.relayEvents(me.scrollable, ['scroll']); // after creating the locking view we now have Grid instances for both locked and // unlocked sides lockedGrid = me.lockedGrid; normalGrid = me.normalGrid; // View has to be moved back into the panel during float lockedGrid.on({ beginfloat: me.onBeginLockedFloat, endfloat: me.onEndLockedFloat, scope: me }); setWidth = lockedGrid.setWidth; // Intercept setWidth here so we can tell the difference between // our own calls to setWidth vs user calls lockedGrid.setWidth = function() { lockedGrid.shrinkWrapColumns = false; setWidth.apply(lockedGrid, arguments); }; // Account for initially hidden columns, or user hide of columns in handlers // called during grid construction if (!lockedGrid.getVisibleColumnManager().getColumns().length) { lockedGrid.hide(); } if (!normalGrid.getVisibleColumnManager().getColumns().length) { normalGrid.hide(); } // 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. lockedHeaderCt = lockedGrid.headerCt; normalHeaderCt = normalGrid.headerCt; // 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] = Ext.apply({ xtype: 'splitter' }, me.split); } 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. // // TreePanel also relays the beforeload and load events, so me.storeRelayers = me.relayEvents(store, [ /** * @event filterchange * @inheritdoc Ext.data.Store#filterchange */ 'filterchange', /** * @event groupchange * @inheritdoc Ext.data.Store#groupchange */ 'groupchange', /** * @event beforeload * @inheritdoc Ext.data.Store#beforeload */ 'beforeload', /** * @event load * @inheritdoc Ext.data.Store#load */ 'load' ]); // 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' ]); }, afterInjectLockable: function() { var me = this; // Here we should set the maskElement to scrollContainer so the loadMask cover both views // but not the headers and grid title bar. me.maskElement = 'scrollContainer'; if (me.disableOnRender) { me.on('afterrender', function() { me.unmask(); }, { single: true }); } delete me.lockedGrid.$initParent; delete me.normalGrid.$initParent; }, syncLockableHeaderVisibility: function() { var me = this, hideHeaders = me.hideHeaders, locked = me.lockedGrid, normal = me.normalGrid; if (hideHeaders === null) { hideHeaders = locked.shouldAutoHideHeaders() && normal.shouldAutoHideHeaders(); } locked.hideHeaders = normal.hideHeaders = hideHeaders; locked.syncHeaderVisibility(); normal.syncHeaderVisibility(); }, getLockingViewConfig: function() { return { xclass: 'Ext.grid.locking.View', locked: this.lockedGrid, normal: this.normalGrid, panel: this }; }, onBeginLockedFloat: function(locked) { var el = locked.getContentTarget().dom, lockedHeaderCt = this.lockedGrid.headerCt, normalHeaderCt = this.normalGrid.headerCt, headerCtHeight = Math.max(normalHeaderCt.getHeight(), lockedHeaderCt.getHeight()); // The two layouts are seperated and no longer share stretchmax height data upon // layout, so for the duration of float, force them to be at least the current // matching height. lockedHeaderCt.minHeight = headerCtHeight; normalHeaderCt.minHeight = headerCtHeight; locked.el.addCls(Ext.panel.Panel.floatCls); // Move view into the grid unless it's already there. // We fire a beginfloat event when expanding or collapsing from // floated out state. if (el.firstChild !== locked.view.el.dom) { el.appendChild(locked.view.el.dom); } locked.body.dom.scrollTop = this.getScrollable().getPosition().y; }, onEndLockedFloat: function() { var locked = this.lockedGrid; // The two headerCts are connected now, allow them to stretchmax each other if (locked.collapsed) { locked.el.removeCls(Ext.panel.Panel.floatCls); } else { this.lockedGrid.headerCt.minHeight = this.normalGrid.headerCt.minHeight = null; } this.lockedScrollbarClipper.appendChild(locked.view.el.dom); this.doSyncLockableLayout(); }, beforeLayout: function() { var me = this, lockedGrid = me.lockedGrid, normalGrid = me.normalGrid, totalColumnWidth; if (lockedGrid && normalGrid) { // The locked side of a grid, if it is shrinkwrapping fixed size columns, // must take into account the column widths plus the border widths of the grid element // and the headerCt element. // This must happen at this late stage so that all relevant classes are added // which affect what borders are applied to what elements. if (lockedGrid.getSizeModel().width.shrinkWrap) { lockedGrid.gridPanelBorderWidth = lockedGrid.el.getBorderWidth('lr'); lockedGrid.shrinkWrapColumns = true; } if (lockedGrid.shrinkWrapColumns) { totalColumnWidth = lockedGrid.headerCt.getTableWidth(); //<debug> if (isNaN(totalColumnWidth)) { Ext.raise("Locked columns in an unsized locked side do NOT support " + "a flex width."); } //</debug> lockedGrid.setWidth(totalColumnWidth + lockedGrid.gridPanelBorderWidth); // setWidth will clear shrinkWrapColumns, so force it again here lockedGrid.shrinkWrapColumns = true; } if (!me.scrollContainer) { me.initScrollContainer(); } me.lastScrollPos = Ext.clone(me.getScrollable().getPosition()); // Undo margin styles set by afterLayout lockedGrid.view.el.setStyle('margin-bottom', ''); normalGrid.view.el.setStyle('margin-bottom', ''); } }, syncLockableLayout: function() { var me = this; // This is called directly from child TableView#afterComponentLayout // So we might get two calls if both are visible, and both lay out. // Schedule a single sync on the tail end of the current layout. if (!me.afterLayoutListener) { me.afterLayoutListener = Ext.on({ afterlayout: me.doSyncLockableLayout, scope: me, single: true }); } }, doSyncLockableLayout: function() { var me = this, collapseExpand = me.isCollapsingOrExpanding, lockedGrid = me.lockedGrid, normalGrid = me.normalGrid, lockedViewEl, normalViewEl, lockedViewRegion, normalViewRegion, scrollbarSize, scrollbarWidth, scrollbarHeight, normalViewWidth, normalViewX, hasVerticalScrollbar, hasHorizontalScrollbar, scrollContainerHeight, scrollBodyHeight, lockedScrollbar, normalScrollbar, scrollbarVisibleCls, scrollHeight, lockedGridVisible, normalGridVisible, scrollBodyDom, viewRegion, scrollerElHeight, scrollable; me.afterLayoutListener = null; if (collapseExpand) { // Expand if (collapseExpand === 2) { me.on('expand', 'doSyncLockableLayout', me, { single: true }); } return; } /* eslint-disable max-len */ if (lockedGrid && normalGrid) { lockedGridVisible = lockedGrid.isVisible(true) && !lockedGrid.collapsed; normalGridVisible = normalGrid.isVisible(true); lockedViewEl = lockedGrid.view.el; normalViewEl = normalGrid.view.el; scrollBodyDom = me.scrollBody.dom; lockedViewRegion = lockedGridVisible ? lockedGrid.body.getRegion(true) : new Ext.util.Region(0, 0, 0, 0); normalViewRegion = normalGridVisible ? normalGrid.body.getRegion(true) : new Ext.util.Region(0, 0, 0, 0); scrollbarSize = Ext.scrollbar.size(); scrollbarWidth = scrollbarSize.width; scrollbarHeight = scrollerElHeight = scrollbarSize.height; normalViewWidth = normalGridVisible ? normalViewRegion.width : 0; normalViewX = lockedGridVisible ? (normalGridVisible ? normalViewRegion.x - lockedViewRegion.x : lockedViewRegion.width) : 0; hasHorizontalScrollbar = (normalGrid.headerCt.tooNarrow || lockedGrid.headerCt.tooNarrow) ? scrollbarHeight : 0; scrollContainerHeight = normalViewRegion.height || lockedViewRegion.height; scrollBodyHeight = scrollContainerHeight; lockedScrollbar = me.lockedScrollbar; normalScrollbar = me.normalScrollbar; scrollbarVisibleCls = me.scrollbarVisibleCls; scrollable = me.getScrollable(); // EXTJS-23301 IE10/11 does not allow an overflowing element to scroll // if the element height is the same as the scrollbar height. This // affects the horizontal normal scrollbar only as the vertical // scrollbar container will always have a width larger due to content. if (Ext.supports.CannotScrollExactHeight) { scrollerElHeight += 1; } if (hasHorizontalScrollbar) { lockedViewEl.setStyle('margin-bottom', -scrollbarHeight + 'px'); normalViewEl.setStyle('margin-bottom', -scrollbarHeight + 'px'); scrollBodyHeight -= scrollbarHeight; if (lockedGridVisible && lockedGrid.view.body.dom) { me.lockedScrollbarScroller.setSize({ x: lockedGrid.headerCt.getTableWidth() }); } if (normalGrid.view.body.dom) { me.normalScrollbarScroller.setSize({ x: normalGrid.headerCt.getTableWidth() }); } } me.scrollBody.setHeight(scrollBodyHeight); lockedViewEl.dom.style.height = normalViewEl.dom.style.height = ''; scrollHeight = (me.scrollable.getSize().y + hasHorizontalScrollbar); normalGrid.view.stretchHeight(scrollHeight); lockedGrid.view.stretchHeight(scrollHeight); hasVerticalScrollbar = scrollbarWidth && scrollBodyDom.scrollHeight > scrollBodyDom.clientHeight; if (hasVerticalScrollbar && normalViewWidth) { normalViewWidth -= scrollbarWidth; normalViewEl.setStyle('width', normalViewWidth + 'px'); } lockedScrollbar.toggleCls(scrollbarVisibleCls, lockedGridVisible && !!hasHorizontalScrollbar); normalScrollbar.toggleCls(scrollbarVisibleCls, !!hasHorizontalScrollbar); // Floated from collapsed views must overlay. THis raises them up. me.normalScrollbarClipper.toggleCls(me.scrollbarClipperCls + '-floated', !!me.normalGrid.floatedFromCollapse); me.normalScrollbar.toggleCls(me.scrollbarCls + '-floated', !!me.normalGrid.floatedFromCollapse); me.lockedScrollbarClipper.toggleCls(me.scrollbarClipperCls + '-floated', !!me.lockedGrid.floatedFromCollapse); me.lockedScrollbar.toggleCls(me.scrollbarCls + '-floated', !!me.lockedGrid.floatedFromCollapse); lockedScrollbar.setSize(me.lockedScrollbarClipper.dom.offsetWidth, scrollerElHeight); normalScrollbar.setSize(normalViewWidth, scrollerElHeight); me.setNormalScrollerX(normalViewX); if (lockedGridVisible && normalGridVisible) { viewRegion = lockedViewRegion.union(normalViewRegion); } else if (lockedGridVisible) { viewRegion = lockedViewRegion; } else { viewRegion = normalViewRegion; } me.scrollContainer.setBox(viewRegion); me.onSyncLockableLayout(hasVerticalScrollbar, viewRegion.width); // We should only scroll if necessary if (!Ext.Object.equals(scrollable.getPosition(), me.lastScrollPos)) { scrollable.scrollTo(me.lastScrollPos); } } /* eslint-enable max-len */ }, onSyncLockableLayout: Ext.emptyFn, setNormalScrollerX: function(x) { this.normalScrollbar.setLocalX(x); this.normalScrollbarClipper.setLocalX(x); }, getScrollExtraCls: function() { return ''; }, initScrollContainer: function() { var me = this, extraCls = me.getScrollExtraCls(), scrollContainer = me.scrollContainer = me.body.insertFirst({ cls: [me.scrollContainerCls, extraCls] }), scrollBody = me.scrollBody = scrollContainer.appendChild({ cls: me.scrollBodyCls }), lockedScrollbar = me.lockedScrollbar = scrollContainer.appendChild({ cls: [me.scrollbarCls, me.scrollbarCls + '-locked', extraCls] }), normalScrollbar = me.normalScrollbar = scrollContainer.appendChild({ cls: [me.scrollbarCls, extraCls] }), lockedView = me.lockedGrid.view, normalView = me.normalGrid.view, lockedScroller = lockedView.getScrollable(), normalScroller = normalView.getScrollable(), Scroller = Ext.scroll.Scroller, lockedScrollbarScroller, normalScrollbarScroller, lockedScrollbarClipper, normalScrollbarClipper; lockedView.stretchHeight(0); normalView.stretchHeight(0); me.scrollable.setConfig({ element: scrollBody, lockedScroller: lockedScroller, normalScroller: normalScroller }); lockedScrollbarClipper = me.lockedScrollbarClipper = scrollBody.appendChild({ cls: [me.scrollbarClipperCls, me.scrollbarClipperCls + '-locked', extraCls] }); normalScrollbarClipper = me.normalScrollbarClipper = scrollBody.appendChild({ cls: [me.scrollbarClipperCls, extraCls] }); lockedScrollbarClipper.appendChild(lockedView.el); normalScrollbarClipper.appendChild(normalView.el); // We just moved the view elements into a containing element that is not the same // as their container's target element (grid body). Setting the ignoreDomPosition // flag instructs the layout system not to move them back. lockedView.ignoreDomPosition = true; normalView.ignoreDomPosition = true; lockedScrollbarScroller = me.lockedScrollbarScroller = new Scroller({ element: lockedScrollbar, x: 'scroll', y: false, rtl: lockedScroller.getRtl && lockedScroller.getRtl() }); normalScrollbarScroller = me.normalScrollbarScroller = new Scroller({ element: normalScrollbar, x: 'scroll', y: false, rtl: normalScroller.getRtl && normalScroller.getRtl() }); me.initScrollers(); lockedScrollbarScroller.addPartner(lockedScroller, 'x'); normalScrollbarScroller.addPartner(normalScroller, 'x'); // Tell the lockable.View that it has been rendered. me.view.onPanelRender(scrollBody); }, initScrollers: Ext.emptyFn, processColumns: function(columns, lockedGrid) { // split apart normal and locked var me = this, i, len, column, cp = new Ext.grid.header.Container({ "$initParent": me }), lockedHeaders = [], normalHeaders = [], lockedHeaderCt = { itemId: 'lockedHeaderCt', stretchMaxPartner: '^^>>#normalHeaderCt', items: lockedHeaders }, normalHeaderCt = { itemId: 'normalHeaderCt', stretchMaxPartner: '^^>>#lockedHeaderCt', items: normalHeaders }, result = { locked: lockedHeaderCt, normal: normalHeaderCt }, copy; // 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.$initParent = cp; column = cp.lookupComponent(column); delete column.$initParent; } // 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) { lockedHeaders.push(column); } else { normalHeaders.push(column); } } me.fireEvent('processcolumns', me, lockedHeaders, normalHeaders); cp.destroy(); return result; }, ensureLockedVisible: function(record, options) { var column = options && options.column, lockedGrid = this.lockedGrid, // eslint-disable-next-line max-len grid = column ? column.getView().ownerCt : lockedGrid.isVisible() ? lockedGrid : this.normalGrid; // Just ask the appropriate grid to scroll. There is only one Y scroller. grid.ensureVisible.apply(grid, arguments); }, /** * 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.destroyed) { // eslint-disable-next-line vars-on-top 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); me.syncRowHeightOnNextLayout = false; } }, // 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 || (me.syncLockedWidthTask = new Ext.util.DelayedTask(me.syncLockedWidth, me)); if (me.reconfiguring) { return; } // Do not delay if we are in suspension or configured to not delay if (!Ext.Component.layoutSuspendCount || me.syncTaskDelay === 0) { me.syncLockedWidth(); } else { task.delay(1); } }, /** * @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 {Object} A pair of flags indicating which views need to be cleared then refreshed. * this contains two properties, `locked` and `normal` which are `true` if the view needs * to be cleared and refreshed. */ syncLockedWidth: function() { var me = this, rendered = me.rendered, locked = me.lockedGrid, 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(); } if (me.reconfiguring) { return; } 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) { // Revert locked grid to original region now it's not the only child grid. if (me.layout.type === 'border') { locked.region = locked.initialConfig.region; } else { locked.flex = locked.initialConfig.flex; } // 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 && locked.shrinkWrapColumns && !locked.headerCt.forceFit) { delete locked.flex; // Just set the property here and update the layout. // Size settings assume it's NOT the layout root. // If the locked has been floated, it might well be! // Use gridPanelBorderWidth as measured in Ext.grid.ColumnLayout#beginLayout // TODO: Use shrinkWrapDock on the locked grid when it works. locked.width = locked.headerCt.getTableWidth() + locked.gridPanelBorderWidth; locked.updateLayout(); } locked.addCls(me.lockedGridCls); locked.show(); if (locked.split) { me.child('splitter').show(); me.addCls(Ext.baseCSSPrefix + 'grid-locked-split'); } } else { // Hide before clearing to avoid DOM layout from clearing // the content and to avoid scroll syncing. TablePanel // disables scroll syncing on hide. locked.hide(); // 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); } if (locked.split) { me.child('splitter').hide(); me.removeCls(Ext.baseCSSPrefix + 'grid-locked-split'); } } } // 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(); // The locked now becomes *the* grid and has to flex to occupy the full view width delete locked.width; if (me.layout.type === 'border') { locked.region = 'center'; normal.region = 'west'; } else { locked.flex = 1; } locked.removeCls(me.lockedGridCls); locked.show(); } Ext.resumeLayouts(true); // Flag object indicating which views need to be cleared and refreshed. return { locked: !!lockedColCount, normal: !!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} [activeHd] 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. * @param toCt * @private */ lock: function(activeHd, toIdx, toCt) { var me = this, normalGrid = me.normalGrid, lockedGrid = me.lockedGrid, normalView = normalGrid.view, lockedView = lockedGrid.view, normalScroller = normalView.getScrollable(), lockedScroller = lockedView.getScrollable(), normalHCt = normalGrid.headerCt, refreshFlags, ownerCt, lbr; activeHd = activeHd || normalHCt.getMenu().activeHeader; activeHd.unlockedWidth = activeHd.width; // If moving a flexed header back into a side where we can't know // whether the flex value will be invalid, revert it either to // its original width or actual width. if (activeHd.flex) { if (activeHd.lockedWidth) { activeHd.width = activeHd.lockedWidth; activeHd.lockedWidth = null; } else { activeHd.width = activeHd.lastBox.width; } activeHd.flex = null; } toCt = toCt || lockedGrid.headerCt; ownerCt = activeHd.ownerCt; // isLockable will test for making the locked side too wide. // The header we're locking may be to be added, and have no ownerCt. // For instance, a checkbox column being moved into the correct side if (ownerCt && !activeHd.isLockable()) { return; } Ext.suspendLayouts(); if (normalScroller) { normalScroller.suspendPartnerSync(); lockedScroller.suspendPartnerSync(); } // If hidden, we need to show it now or the locked headerCt's VisibleColumnManager // 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) { // The locked side's BufferedRenderer has never has a resize passed in, // so its viewSize will be the default viewSize, out of sync with the normal side. // Synchronize the viewSize before the two sides are refreshed. if (!lockedGrid.componentLayoutCounter) { lockedGrid.height = normalGrid.lastBox.height; lbr = lockedView.bufferedRenderer; if (lbr) { lbr.rowHeight = normalView.bufferedRenderer.rowHeight; lbr.onViewResize(lockedView, 0, normalGrid.body.lastBox.height); } } lockedGrid.show(); } // TablePanel#onHeadersChanged does not respond if reconfiguring set. // We programatically refresh views which need it below. lockedGrid.reconfiguring = normalGrid.reconfiguring = true; // Keep the column in the hierarchy during the move. // So that grid.isAncestor(column) still returns true, and SpreadsheetModel // does not deselect activeHd.ownerCmp = activeHd.ownerCt; 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); } lockedGrid.reconfiguring = normalGrid.reconfiguring = false; activeHd.ownerCmp = null; activeHd.rootHeaderCt = null; activeHd.view = lockedView; refreshFlags = me.syncLockedWidth(); // Refresh locked view second, so that if it's refreshing from empty (can start // with no locked columns), the buffered renderer can look to its partner // to get the correct range to refresh. normalGrid.getView().refreshNeeded = refreshFlags.normal; lockedGrid.getView().refreshNeeded = refreshFlags.locked; activeHd.onLock(activeHd); me.fireEvent('lockcolumn', me, activeHd); Ext.resumeLayouts(true); if (normalScroller) { normalScroller.resumePartnerSync(true); lockedScroller.resumePartnerSync(); } }, // 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} [activeHd] 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. * @param toCt * @private */ unlock: function(activeHd, toIdx, toCt) { var me = this, normalGrid = me.normalGrid, lockedGrid = me.lockedGrid, normalView = normalGrid.view, lockedView = lockedGrid.view, startIndex = normalView.all.startIndex, lockedHCt = lockedGrid.headerCt, refreshFlags; // Unlocking; user expectation is that the unlocked column is inserted at the beginning. if (!Ext.isDefined(toIdx)) { toIdx = 0; } activeHd = activeHd || lockedHCt.getMenu().activeHeader; activeHd.lockedWidth = activeHd.width; // If moving a flexed header back into a side where we can't know // whether the flex value will be invalid, revert it either to // its original width or actual width. if (activeHd.flex) { if (activeHd.unlockedWidth) { activeHd.width = activeHd.unlockedWidth; activeHd.unlockedWidth = null; } else { activeHd.width = activeHd.lastBox.width; } activeHd.flex = null; } toCt = toCt || normalGrid.headerCt; Ext.suspendLayouts(); // TablePanel#onHeadersChanged does not respond if reconfiguring set. // We programatically refresh views which need it below. lockedGrid.reconfiguring = normalGrid.reconfiguring = true; // Keep the column in the hierarchy during the move. // So that grid.isAncestor(column) still returns true, and SpreadsheetModel // does not deselect activeHd.ownerCmp = activeHd.ownerCt; if (activeHd.ownerCt) { activeHd.ownerCt.remove(activeHd, false); } activeHd.locked = false; toCt.insert(toIdx, activeHd); lockedGrid.reconfiguring = normalGrid.reconfiguring = false; activeHd.ownerCmp = null; activeHd.rootHeaderCt = null; activeHd.view = normalView; // syncLockedWidth returns visible column counts for both grids. // only refresh what needs refreshing refreshFlags = me.syncLockedWidth(); // Clear both views first so that any widgets are cached // before reuse. If we refresh the grid which just had a widget column added // first, the clear of the view which had the widget column in removes the widgets // from their new place. if (refreshFlags.locked) { lockedView.clearViewEl(true); } if (refreshFlags.normal) { normalView.clearViewEl(true); } // Refresh locked view second, so that if it's refreshing from empty (can start // with no locked columns), the buffered renderer can look to its partner to get // the correct range to refresh. if (refreshFlags.normal) { normalGrid.getView().refreshView(startIndex); } if (refreshFlags.locked) { lockedGrid.getView().refreshView(startIndex); } activeHd.onUnlock(activeHd); me.fireEvent('unlockcolumn', me, activeHd); Ext.resumeLayouts(true); }, /** * @private */ reconfigureLockable: function(store, columns, allowUnbind) { // we want to totally override the reconfigure behaviour here, // since we're creating 2 sub-grids var me = this, oldStore = me.store, lockedGrid = me.lockedGrid, normalGrid = me.normalGrid, lockedView = lockedGrid.view, normalView = normalGrid.view, hadLockedColumns, loadMask, lockedItems; if (!store && allowUnbind) { store = Ext.StoreManager.lookup('ext-empty-store'); } // 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; lockedView.blockRefresh = normalView.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 lockedView.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 (!lockedView.dataSource.isFeatureStore) { lockedView.dataSource = store; } if (lockedView.bufferedRenderer) { lockedView.bufferedRenderer.bindStore(store); } normalGrid.bindStore(store); normalView.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 (!normalView.dataSource.isFeatureStore) { normalView.dataSource = store; } if (normalView.bufferedRenderer) { normalView.bufferedRenderer.bindStore(store); } me.view.store = store; // binding mask to new store loadMask = me.view.loadMask; if (loadMask && loadMask.isLoadMask) { loadMask.bindStore(store); } me.view.bindStore(normalView.dataSource, false, 'dataSource'); lockedView.blockRefresh = normalView.blockRefresh = false; } if (columns) { // Both grids must not react to the headers being changed // (See panel/Table#onHeadersChanged) lockedGrid.reconfiguring = normalGrid.reconfiguring = true; hadLockedColumns = lockedGrid.getVisibleColumnManager().getColumns().length; lockedGrid.headerCt.removeAll(); normalGrid.headerCt.removeAll(); columns = me.processColumns(columns, lockedGrid); lockedItems = columns.locked.items; if (lockedItems.length) { if (!hadLockedColumns) { // If we are adding columns for the first time, the view will need to // be refreshed because it will have skipped this when it had 0 columns lockedGrid.view.refreshNeeded = true; } lockedGrid.headerCt.add(lockedItems); } normalGrid.headerCt.add(columns.normal.items); // Remove flag telling the locked column add listener to do nothing 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 = normalView.refreshCounter; }, afterReconfigureLockable: function() { // Ensure width are set up, and visibility of sides are synced with whether // they have columns or not. this.syncLockedWidth(); // If the counter hasn't changed since where we saved it previously, we haven't refreshed, // so kick it off now. if (this.refreshCounter === this.normalGrid.getView().refreshCounter) { this.view.refreshView(); } }, constructLockableFeatures: function() { var features = this.features, feature, featureClone, lockedFeatures, normalFeatures, i, len; if (features) { if (!Ext.isArray(features)) { features = [ features ]; } lockedFeatures = []; normalFeatures = []; for (i = 0, len = features.length; 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, lockableScope, pluginCls, i, len; if (plugins) { if (!Ext.isArray(plugins)) { plugins = [ plugins ]; } topPlugins = []; lockedPlugins = []; normalPlugins = []; for (i = 0, len = plugins.length; 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; } // Release interceptors created in modifyHeaderCt if (me.lockedGrid && me.lockedGrid.headerCt) { me.lockedGrid.headerCt.showMenuBy = null; } if (me.normalGrid && me.normalGrid.headerCt) { me.normalGrid.headerCt.showMenuBy = null; } Ext.destroy( me.normalScrollbarClipper, me.lockedScrollbarClipper, me.normalScrollbar, me.lockedScrollbar, me.scrollBody, me.scrollContainer, me.normalScrollbarScroller, me.lockedScrollbarScroller, me.view, me.headerCt ); }}, function() { this.borrow(Ext.Component, ['constructPlugin']);});