X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/grid/Lockable.js diff --git a/src/grid/Lockable.js b/src/grid/Lockable.js new file mode 100644 index 00000000..88d7bffe --- /dev/null +++ b/src/grid/Lockable.js @@ -0,0 +1,597 @@ +/** + * @class Ext.grid.Lockable + * @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.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 tablepanel's that will be generated + * to note what configurations should be copied. These are named normalCfgCopy and + * lockedCfgCopy respectively. + * + * Columns which are locked must specify a fixed width. They do *NOT* support a + * flex width. + * + * Configurations which are specified in this class will be available on any grid or + * tree which is using the lockable functionality. + */ +Ext.define('Ext.grid.Lockable', { + + requires: ['Ext.grid.LockingView'], + + /** + * @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. + */ + + // private variable to track whether or not the spacer is hidden/visible + spacerHidden: true, + + // i8n text + unlockText: 'Unlock', + lockText: 'Lock', + + determineXTypeToCreate: function() { + var me = this, + typeToCreate; + + if (me.subGridXType) { + typeToCreate = me.subGridXType; + } else { + var 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() { + // 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, + // xtype of this class, 'treepanel' or 'gridpanel' + // (Note: this makes it a requirement that any subclass that wants to use lockable functionality needs to register an + // alias.) + xtype = me.determineXTypeToCreate(), + // share the selection model + selModel = me.getSelectionModel(), + lockedGrid = { + xtype: xtype, + // Lockable does NOT support animations for Tree + enableAnimations: false, + scroll: false, + scrollerOwner: false, + selModel: selModel, + border: false, + cls: Ext.baseCSSPrefix + 'grid-inner-locked' + }, + normalGrid = { + xtype: xtype, + enableAnimations: false, + scrollerOwner: false, + selModel: selModel, + border: false + }, + i = 0, + columns, + lockedHeaderCt, + normalHeaderCt; + + me.addCls(Ext.baseCSSPrefix + 'grid-locked'); + + // copy appropriate configurations to the respective + // aggregated tablepanel instances and then delete them + // from the master tablepanel. + Ext.copyTo(normalGrid, me, me.normalCfgCopy); + Ext.copyTo(lockedGrid, me, me.lockedCfgCopy); + for (; i < me.normalCfgCopy.length; i++) { + delete me[me.normalCfgCopy[i]]; + } + for (i = 0; i < me.lockedCfgCopy.length; i++) { + delete me[me.lockedCfgCopy[i]]; + } + + me.lockedHeights = []; + me.normalHeights = []; + + columns = me.processColumns(me.columns); + + lockedGrid.width = columns.lockedWidth; + lockedGrid.columns = columns.locked; + normalGrid.columns = columns.normal; + + me.store = Ext.StoreManager.lookup(me.store); + lockedGrid.store = me.store; + normalGrid.store = me.store; + + // normal grid should flex the rest of the width + normalGrid.flex = 1; + lockedGrid.viewConfig = me.lockedViewConfig || {}; + lockedGrid.viewConfig.loadingUseMsg = false; + normalGrid.viewConfig = me.normalViewConfig || {}; + + Ext.applyIf(lockedGrid.viewConfig, me.viewConfig); + Ext.applyIf(normalGrid.viewConfig, me.viewConfig); + + me.normalGrid = Ext.ComponentManager.create(normalGrid); + me.lockedGrid = Ext.ComponentManager.create(lockedGrid); + + me.view = Ext.create('Ext.grid.LockingView', { + locked: me.lockedGrid, + normal: me.normalGrid, + panel: me + }); + + if (me.syncRowHeight) { + me.lockedGrid.getView().on({ + refresh: me.onLockedGridAfterRefresh, + itemupdate: me.onLockedGridAfterUpdate, + scope: me + }); + + me.normalGrid.getView().on({ + refresh: me.onNormalGridAfterRefresh, + itemupdate: me.onNormalGridAfterUpdate, + scope: me + }); + } + + lockedHeaderCt = me.lockedGrid.headerCt; + normalHeaderCt = me.normalGrid.headerCt; + + lockedHeaderCt.lockedCt = true; + lockedHeaderCt.lockableInjected = true; + normalHeaderCt.lockableInjected = true; + + lockedHeaderCt.on({ + columnshow: me.onLockedHeaderShow, + columnhide: me.onLockedHeaderHide, + columnmove: me.onLockedHeaderMove, + sortchange: me.onLockedHeaderSortChange, + columnresize: me.onLockedHeaderResize, + scope: me + }); + + normalHeaderCt.on({ + columnmove: me.onNormalHeaderMove, + sortchange: me.onNormalHeaderSortChange, + scope: me + }); + + me.normalGrid.on({ + scrollershow: me.onScrollerShow, + scrollerhide: me.onScrollerHide, + scope: me + }); + + me.lockedGrid.on('afterlayout', me.onLockedGridAfterLayout, me, {single: true}); + + me.modifyHeaderCt(); + me.items = [me.lockedGrid, me.normalGrid]; + + me.layout = { + type: 'hbox', + align: 'stretch' + }; + }, + + processColumns: function(columns){ + // split apart normal and lockedWidths + var i = 0, + len = columns.length, + lockedWidth = 0, + lockedHeaders = [], + normalHeaders = [], + column; + + for (; i < len; ++i) { + column = columns[i]; + // mark the column as processed so that the locked attribute does not + // trigger trying to aggregate the columns again. + column.processed = true; + if (column.locked) { + // + if (column.flex) { + Ext.Error.raise("Columns which are locked do NOT support a flex width. You must set a width on the " + columns[i].text + "column."); + } + // + lockedWidth += column.width; + lockedHeaders.push(column); + } else { + normalHeaders.push(column); + } + } + return { + lockedWidth: lockedWidth, + locked: lockedHeaders, + normal: normalHeaders + }; + }, + + // create a new spacer after the table is refreshed + onLockedGridAfterLayout: function() { + var me = this, + lockedView = me.lockedGrid.getView(); + lockedView.on({ + refresh: me.createSpacer, + beforerefresh: me.destroySpacer, + scope: me + }); + }, + + // trigger a pseudo refresh on the normal side + onLockedHeaderMove: function() { + if (this.syncRowHeight) { + this.onNormalGridAfterRefresh(); + } + }, + + // trigger a pseudo refresh on the locked side + onNormalHeaderMove: function() { + if (this.syncRowHeight) { + this.onLockedGridAfterRefresh(); + } + }, + + // create a spacer in lockedsection and store a reference + // TODO: Should destroy before refreshing content + createSpacer: function() { + var me = this, + // This affects scrolling all the way to the bottom of a locked grid + // additional test, sort a column and make sure it synchronizes + w = Ext.getScrollBarWidth() + (Ext.isIE ? 2 : 0), + view = me.lockedGrid.getView(), + el = view.el; + + me.spacerEl = Ext.core.DomHelper.append(el, { + cls: me.spacerHidden ? (Ext.baseCSSPrefix + 'hidden') : '', + style: 'height: ' + w + 'px;' + }, true); + }, + + destroySpacer: function() { + var me = this; + if (me.spacerEl) { + me.spacerEl.destroy(); + delete me.spacerEl; + } + }, + + // cache the heights of all locked rows and sync rowheights + onLockedGridAfterRefresh: function() { + var me = this, + view = me.lockedGrid.getView(), + el = view.el, + rowEls = el.query(view.getItemSelector()), + ln = rowEls.length, + i = 0; + + // reset heights each time. + me.lockedHeights = []; + + for (; i < ln; i++) { + me.lockedHeights[i] = rowEls[i].clientHeight; + } + me.syncRowHeights(); + }, + + // cache the heights of all normal rows and sync rowheights + onNormalGridAfterRefresh: function() { + var me = this, + view = me.normalGrid.getView(), + el = view.el, + rowEls = el.query(view.getItemSelector()), + ln = rowEls.length, + i = 0; + + // reset heights each time. + me.normalHeights = []; + + for (; i < ln; i++) { + me.normalHeights[i] = rowEls[i].clientHeight; + } + me.syncRowHeights(); + }, + + // rows can get bigger/smaller + onLockedGridAfterUpdate: function(record, index, node) { + this.lockedHeights[index] = node.clientHeight; + this.syncRowHeights(); + }, + + // rows can get bigger/smaller + onNormalGridAfterUpdate: function(record, index, node) { + this.normalHeights[index] = node.clientHeight; + this.syncRowHeights(); + }, + + // match the rowheights to the biggest rowheight on either + // side + syncRowHeights: function() { + var me = this, + lockedHeights = me.lockedHeights, + normalHeights = me.normalHeights, + calcHeights = [], + ln = lockedHeights.length, + i = 0, + lockedView, normalView, + lockedRowEls, normalRowEls, + vertScroller = me.getVerticalScroller(), + scrollTop; + + // ensure there are an equal num of locked and normal + // rows before synchronization + if (lockedHeights.length && normalHeights.length) { + lockedView = me.lockedGrid.getView(); + normalView = me.normalGrid.getView(); + lockedRowEls = lockedView.el.query(lockedView.getItemSelector()); + normalRowEls = normalView.el.query(normalView.getItemSelector()); + + // loop thru all of the heights and sync to the other side + for (; i < ln; i++) { + // ensure both are numbers + if (!isNaN(lockedHeights[i]) && !isNaN(normalHeights[i])) { + if (lockedHeights[i] > normalHeights[i]) { + Ext.fly(normalRowEls[i]).setHeight(lockedHeights[i]); + } else if (lockedHeights[i] < normalHeights[i]) { + Ext.fly(lockedRowEls[i]).setHeight(normalHeights[i]); + } + } + } + + // invalidate the scroller and sync the scrollers + me.normalGrid.invalidateScroller(); + + // synchronize the view with the scroller, if we have a virtualScrollTop + // then the user is using a PagingScroller + if (vertScroller && vertScroller.setViewScrollTop) { + vertScroller.setViewScrollTop(me.virtualScrollTop); + } else { + // We don't use setScrollTop here because if the scrollTop is + // set to the exact same value some browsers won't fire the scroll + // event. Instead, we directly set the scrollTop. + scrollTop = normalView.el.dom.scrollTop; + normalView.el.dom.scrollTop = scrollTop; + lockedView.el.dom.scrollTop = scrollTop; + } + + // reset the heights + me.lockedHeights = []; + me.normalHeights = []; + } + }, + + // track when scroller is shown + onScrollerShow: function(scroller, direction) { + if (direction === 'horizontal') { + this.spacerHidden = false; + this.spacerEl.removeCls(Ext.baseCSSPrefix + 'hidden'); + } + }, + + // track when scroller is hidden + onScrollerHide: function(scroller, direction) { + if (direction === 'horizontal') { + this.spacerHidden = true; + this.spacerEl.addCls(Ext.baseCSSPrefix + 'hidden'); + } + }, + + + // inject Lock and Unlock text + modifyHeaderCt: function() { + var me = this; + me.lockedGrid.headerCt.getMenuItems = me.getMenuItems(true); + me.normalGrid.headerCt.getMenuItems = me.getMenuItems(false); + }, + + onUnlockMenuClick: function() { + this.unlock(); + }, + + onLockMenuClick: function() { + this.lock(); + }, + + getMenuItems: function(locked) { + var me = this, + unlockText = me.unlockText, + lockText = me.lockText, + // TODO: Refactor to use Ext.baseCSSPrefix + unlockCls = 'xg-hmenu-unlock', + lockCls = 'xg-hmenu-lock', + unlockHandler = Ext.Function.bind(me.onUnlockMenuClick, me), + lockHandler = Ext.Function.bind(me.onLockMenuClick, me); + + // runs in the scope of headerCt + return function() { + var o = Ext.grid.header.Container.prototype.getMenuItems.call(this); + o.push('-',{ + cls: unlockCls, + text: unlockText, + handler: unlockHandler, + disabled: !locked + }); + o.push({ + cls: lockCls, + text: lockText, + handler: lockHandler, + disabled: locked + }); + return o; + }; + }, + + // 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 (Optional) Header to unlock from the locked section. Defaults to the header which has the menu open currently. + * @param {Number} toIdx (Optional) The index to move the unlocked header to. Defaults to appending as the last item. + * @private + */ + lock: function(activeHd, toIdx) { + var me = this, + normalGrid = me.normalGrid, + lockedGrid = me.lockedGrid, + normalHCt = normalGrid.headerCt, + lockedHCt = lockedGrid.headerCt; + + activeHd = activeHd || normalHCt.getMenu().activeHeader; + + // if column was previously flexed, get/set current width + // and remove the flex + if (activeHd.flex) { + activeHd.width = activeHd.getWidth(); + delete activeHd.flex; + } + + normalHCt.remove(activeHd, false); + lockedHCt.suspendLayout = true; + if (Ext.isDefined(toIdx)) { + lockedHCt.insert(toIdx, activeHd); + } else { + lockedHCt.add(activeHd); + } + lockedHCt.suspendLayout = false; + me.syncLockedSection(); + }, + + syncLockedSection: function() { + var me = this; + me.syncLockedWidth(); + me.lockedGrid.getView().refresh(); + me.normalGrid.getView().refresh(); + }, + + // adjust the locked section to the width of its respective + // headerCt + syncLockedWidth: function() { + var me = this, + width = me.lockedGrid.headerCt.getFullWidth(true); + me.lockedGrid.setWidth(width); + }, + + onLockedHeaderResize: function() { + this.syncLockedWidth(); + }, + + onLockedHeaderHide: function() { + this.syncLockedWidth(); + }, + + onLockedHeaderShow: function() { + this.syncLockedWidth(); + }, + + onLockedHeaderSortChange: function(headerCt, header, sortState) { + if (sortState) { + // no real header, and silence the event so we dont get into an + // infinite loop + this.normalGrid.headerCt.clearOtherSortStates(null, true); + } + }, + + onNormalHeaderSortChange: function(headerCt, header, sortState) { + if (sortState) { + // no real header, and silence the event so we dont get into an + // infinite loop + this.lockedGrid.headerCt.clearOtherSortStates(null, true); + } + }, + + // 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 (Optional) Header to unlock from the locked section. Defaults to the header which has the menu open currently. + * @param {Number} toIdx (Optional) The index to move the unlocked header to. Defaults to 0. + * @private + */ + unlock: function(activeHd, toIdx) { + var me = this, + normalGrid = me.normalGrid, + lockedGrid = me.lockedGrid, + normalHCt = normalGrid.headerCt, + lockedHCt = lockedGrid.headerCt; + + if (!Ext.isDefined(toIdx)) { + toIdx = 0; + } + activeHd = activeHd || lockedHCt.getMenu().activeHeader; + + lockedHCt.remove(activeHd, false); + me.syncLockedWidth(); + me.lockedGrid.getView().refresh(); + normalHCt.insert(toIdx, activeHd); + me.normalGrid.getView().refresh(); + }, + + // we want to totally override the reconfigure behaviour here, since we're creating 2 sub-grids + reconfigureLockable: function(store, columns) { + var me = this, + lockedGrid = me.lockedGrid, + normalGrid = me.normalGrid; + + if (columns) { + lockedGrid.headerCt.removeAll(); + normalGrid.headerCt.removeAll(); + + columns = me.processColumns(columns); + lockedGrid.setWidth(columns.lockedWidth); + lockedGrid.headerCt.add(columns.locked); + normalGrid.headerCt.add(columns.normal); + } + + if (store) { + store = Ext.data.StoreManager.lookup(store); + me.store = store; + lockedGrid.bindStore(store); + normalGrid.bindStore(store); + } else { + lockedGrid.getView().refresh(); + normalGrid.getView().refresh(); + } + } +});