--- /dev/null
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <title>The source code</title>
+ <link href="../prettify/prettify.css" type="text/css" rel="stylesheet" />
+ <script type="text/javascript" src="../prettify/prettify.js"></script>
+ <style type="text/css">
+ .highlight { display: block; background-color: #ddd; }
+ </style>
+ <script type="text/javascript">
+ function highlight() {
+ document.getElementById(location.hash.replace(/#/, "")).className = "highlight";
+ }
+ </script>
+</head>
+<body onload="prettyPrint(); highlight();">
+ <pre class="prettyprint lang-js"><span id='Ext-grid-Lockable'>/**
+</span> * @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'],
+
+<span id='Ext-grid-Lockable-cfg-syncRowHeight'> /**
+</span> * @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,
+
+<span id='Ext-grid-Lockable-cfg-subGridXType'> /**
+</span> * @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.
+ */
+
+<span id='Ext-grid-Lockable-cfg-lockedViewConfig'> /**
+</span> * @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.
+ */
+
+<span id='Ext-grid-Lockable-cfg-normalViewConfig'> /**
+</span> * @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) {
+ // <debug>
+ 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.");
+ }
+ // </debug>
+ 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
+<span id='Ext-grid-Lockable-method-lock'> /**
+</span> * 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
+<span id='Ext-grid-Lockable-method-unlock'> /**
+</span> * 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();
+ }
+ }
+});
+</pre>
+</body>
+</html>