Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / docs / source / Lockable.html
diff --git a/docs/source/Lockable.html b/docs/source/Lockable.html
new file mode 100644 (file)
index 0000000..c02bb7b
--- /dev/null
@@ -0,0 +1,617 @@
+<!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 &lt; me.normalCfgCopy.length; i++) {
+            delete me[me.normalCfgCopy[i]];
+        }
+        for (i = 0; i &lt; 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 &lt; 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) {
+                // &lt;debug&gt;
+                if (column.flex) {
+                    Ext.Error.raise(&quot;Columns which are locked do NOT support a flex width. You must set a width on the &quot; + columns[i].text + &quot;column.&quot;);
+                }
+                // &lt;/debug&gt;
+                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 &lt; 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 &lt; 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 &amp;&amp; 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 &lt; ln; i++) {
+                // ensure both are numbers
+                if (!isNaN(lockedHeights[i]) &amp;&amp; !isNaN(normalHeights[i])) {
+                    if (lockedHeights[i] &gt; normalHeights[i]) {
+                        Ext.fly(normalRowEls[i]).setHeight(lockedHeights[i]);
+                    } else if (lockedHeights[i] &lt; 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 &amp;&amp; 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>