Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / layout / container / Border.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.layout.container.Border
17  * @extends Ext.layout.container.Container
18  * <p>This is a multi-pane, application-oriented UI layout style that supports multiple
19  * nested panels, automatic bars between regions and built-in
20  * {@link Ext.panel.Panel#collapsible expanding and collapsing} of regions.</p>
21  * <p>This class is intended to be extended or created via the <code>layout:'border'</code>
22  * {@link Ext.container.Container#layout} config, and should generally not need to be created directly
23  * via the new keyword.</p>
24  * {@img Ext.layout.container.Border/Ext.layout.container.Border.png Ext.layout.container.Border container layout}
25  * <p>Example usage:</p>
26  * <pre><code>
27      Ext.create('Ext.panel.Panel', {
28         width: 500,
29         height: 400,
30         title: 'Border Layout',
31         layout: 'border',
32         items: [{
33             title: 'South Region is resizable',
34             region: 'south',     // position for region
35             xtype: 'panel',
36             height: 100,
37             split: true,         // enable resizing
38             margins: '0 5 5 5'
39         },{
40             // xtype: 'panel' implied by default
41             title: 'West Region is collapsible',
42             region:'west',
43             xtype: 'panel',
44             margins: '5 0 0 5',
45             width: 200,
46             collapsible: true,   // make collapsible
47             id: 'west-region-container',
48             layout: 'fit'
49         },{
50             title: 'Center Region',
51             region: 'center',     // center region is required, no width/height specified
52             xtype: 'panel',
53             layout: 'fit',
54             margins: '5 5 0 0'
55         }],
56         renderTo: Ext.getBody()
57     });
58 </code></pre>
59  * <p><b><u>Notes</u></b>:</p><div class="mdetail-params"><ul>
60  * <li>Any Container using the Border layout <b>must</b> have a child item with <code>region:'center'</code>.
61  * The child item in the center region will always be resized to fill the remaining space not used by
62  * the other regions in the layout.</li>
63  * <li>Any child items with a region of <code>west</code> or <code>east</code> may be configured with either
64  * an initial <code>width</code>, or a {@link Ext.layout.container.Box#flex} value, or an initial percentage width <b>string</b> (Which is simply divided by 100 and used as a flex value). The 'center' region has a flex value of <code>1</code>.</li>
65  * <li>Any child items with a region of <code>north</code> or <code>south</code> may be configured with either
66  * an initial <code>height</code>, or a {@link Ext.layout.container.Box#flex} value, or an initial percentage height <b>string</b> (Which is simply divided by 100 and used as a flex value). The 'center' region has a flex value of <code>1</code>.</li>
67  * <li>The regions of a BorderLayout are <b>fixed at render time</b> and thereafter, its child Components may not be removed or added</b>.To add/remove
68  * Components within a BorderLayout, have them wrapped by an additional Container which is directly
69  * managed by the BorderLayout.  If the region is to be collapsible, the Container used directly
70  * by the BorderLayout manager should be a Panel.  In the following example a Container (an Ext.panel.Panel)
71  * is added to the west region:<pre><code>
72 wrc = {@link Ext#getCmp Ext.getCmp}('west-region-container');
73 wrc.{@link Ext.container.Container#removeAll removeAll}();
74 wrc.{@link Ext.container.Container#add add}({
75     title: 'Added Panel',
76     html: 'Some content'
77 });
78  * </code></pre>
79  * </li>
80  * <li><b>There is no BorderLayout.Region class in ExtJS 4.0+</b></li>
81  * </ul></div>
82  */
83 Ext.define('Ext.layout.container.Border', {
84
85     alias: ['layout.border'],
86     extend: 'Ext.layout.container.Container',
87     requires: ['Ext.resizer.Splitter', 'Ext.container.Container', 'Ext.fx.Anim'],
88     alternateClassName: 'Ext.layout.BorderLayout',
89
90     targetCls: Ext.baseCSSPrefix + 'border-layout-ct',
91
92     itemCls: Ext.baseCSSPrefix + 'border-item',
93
94     bindToOwnerCtContainer: true,
95
96     percentageRe: /(\d+)%/,
97
98     slideDirection: {
99         north: 't',
100         south: 'b',
101         west: 'l',
102         east: 'r'
103     },
104
105     constructor: function(config) {
106         this.initialConfig = config;
107         this.callParent(arguments);
108     },
109
110     onLayout: function() {
111         var me = this;
112         if (!me.borderLayoutInitialized) {
113             me.initializeBorderLayout();
114         }
115
116         // Delegate this operation to the shadow "V" or "H" box layout, and then down to any embedded layout.
117         me.fixHeightConstraints();
118         me.shadowLayout.onLayout();
119         if (me.embeddedContainer) {
120             me.embeddedContainer.layout.onLayout();
121         }
122
123         // If the panel was originally configured with collapsed: true, it will have
124         // been initialized with a "borderCollapse" flag: Collapse it now before the first layout.
125         if (!me.initialCollapsedComplete) {
126             Ext.iterate(me.regions, function(name, region){
127                 if (region.borderCollapse) {
128                     me.onBeforeRegionCollapse(region, region.collapseDirection, false, 0);
129                 }
130             });
131             me.initialCollapsedComplete = true;
132         }
133     },
134
135     isValidParent : function(item, target, position) {
136         if (!this.borderLayoutInitialized) {
137             this.initializeBorderLayout();
138         }
139
140         // Delegate this operation to the shadow "V" or "H" box layout.
141         return this.shadowLayout.isValidParent(item, target, position);
142     },
143
144     beforeLayout: function() {
145         if (!this.borderLayoutInitialized) {
146             this.initializeBorderLayout();
147         }
148
149         // Delegate this operation to the shadow "V" or "H" box layout.
150         this.shadowLayout.beforeLayout();
151     },
152
153     renderItems: function(items, target) {
154         //<debug>
155         Ext.Error.raise('This should not be called');
156         //</debug>
157     },
158
159     renderItem: function(item) {
160         //<debug>
161         Ext.Error.raise('This should not be called');
162         //</debug>
163     },
164
165     /*
166      * Gathers items for a layout operation. Injected into child Box layouts through configuration.
167      * We must not include child items which are floated over the layout (are primed with a slide out animation)
168      */
169     getVisibleItems: function() {
170         return Ext.ComponentQuery.query(':not([slideOutAnim])', this.callParent(arguments));
171     },
172
173     initializeBorderLayout: function() {
174         var me = this,
175             i = 0,
176             items = me.getLayoutItems(),
177             ln = items.length,
178             regions = (me.regions = {}),
179             vBoxItems = [],
180             hBoxItems = [],
181             horizontalFlex = 0,
182             verticalFlex = 0,
183             comp, percentage;
184
185         // Map of Splitters for each region
186         me.splitters = {};
187
188         // Map of regions
189         for (; i < ln; i++) {
190             comp = items[i];
191             regions[comp.region] = comp;
192
193             // Intercept collapsing to implement showing an alternate Component as a collapsed placeholder
194             if (comp.region != 'center' && comp.collapsible && comp.collapseMode != 'header') {
195
196                 // This layout intercepts any initial collapsed state. Panel must not do this itself.
197                 comp.borderCollapse = comp.collapsed;
198                 delete comp.collapsed;
199
200                 comp.on({
201                     beforecollapse: me.onBeforeRegionCollapse,
202                     beforeexpand: me.onBeforeRegionExpand,
203                     destroy: me.onRegionDestroy,
204                     scope: me
205                 });
206                 me.setupState(comp);
207             }
208         }
209         //<debug>
210         if (!regions.center) {
211             Ext.Error.raise("You must specify a center region when defining a BorderLayout.");
212         }
213         //</debug>
214         comp = regions.center;
215         if (!comp.flex) {
216             comp.flex = 1;
217         }
218         delete comp.width;
219         comp.maintainFlex = true;
220
221         // Begin the VBox and HBox item list.
222         comp = regions.west;
223         if (comp) {
224             comp.collapseDirection = Ext.Component.DIRECTION_LEFT;
225             hBoxItems.push(comp);
226             if (comp.split) {
227                 hBoxItems.push(me.splitters.west = me.createSplitter(comp));
228             }
229             percentage = Ext.isString(comp.width) && comp.width.match(me.percentageRe);
230             if (percentage) {
231                 horizontalFlex += (comp.flex = parseInt(percentage[1], 10) / 100);
232                 delete comp.width;
233             }
234         }
235         comp = regions.north;
236         if (comp) {
237             comp.collapseDirection = Ext.Component.DIRECTION_TOP;
238             vBoxItems.push(comp);
239             if (comp.split) {
240                 vBoxItems.push(me.splitters.north = me.createSplitter(comp));
241             }
242             percentage = Ext.isString(comp.height) && comp.height.match(me.percentageRe);
243             if (percentage) {
244                 verticalFlex += (comp.flex = parseInt(percentage[1], 10) / 100);
245                 delete comp.height;
246             }
247         }
248
249         // Decide into which Collection the center region goes.
250         if (regions.north || regions.south) {
251             if (regions.east || regions.west) {
252
253                 // Create the embedded center. Mark it with the region: 'center' property so that it can be identified as the center.
254                 vBoxItems.push(me.embeddedContainer = Ext.create('Ext.container.Container', {
255                     xtype: 'container',
256                     region: 'center',
257                     id: me.owner.id + '-embedded-center',
258                     cls: Ext.baseCSSPrefix + 'border-item',
259                     flex: regions.center.flex,
260                     maintainFlex: true,
261                     layout: {
262                         type: 'hbox',
263                         align: 'stretch',
264                         getVisibleItems: me.getVisibleItems
265                     }
266                 }));
267                 hBoxItems.push(regions.center);
268             }
269             // No east or west: the original center goes straight into the vbox
270             else {
271                 vBoxItems.push(regions.center);
272             }
273         }
274         // If we have no north or south, then the center is part of the HBox items
275         else {
276             hBoxItems.push(regions.center);
277         }
278
279         // Finish off the VBox and HBox item list.
280         comp = regions.south;
281         if (comp) {
282             comp.collapseDirection = Ext.Component.DIRECTION_BOTTOM;
283             if (comp.split) {
284                 vBoxItems.push(me.splitters.south = me.createSplitter(comp));
285             }
286             percentage = Ext.isString(comp.height) && comp.height.match(me.percentageRe);
287             if (percentage) {
288                 verticalFlex += (comp.flex = parseInt(percentage[1], 10) / 100);
289                 delete comp.height;
290             }
291             vBoxItems.push(comp);
292         }
293         comp = regions.east;
294         if (comp) {
295             comp.collapseDirection = Ext.Component.DIRECTION_RIGHT;
296             if (comp.split) {
297                 hBoxItems.push(me.splitters.east = me.createSplitter(comp));
298             }
299             percentage = Ext.isString(comp.width) && comp.width.match(me.percentageRe);
300             if (percentage) {
301                 horizontalFlex += (comp.flex = parseInt(percentage[1], 10) / 100);
302                 delete comp.width;
303             }
304             hBoxItems.push(comp);
305         }
306
307         // Create the injected "items" collections for the Containers.
308         // If we have north or south, then the shadow Container will be a VBox.
309         // If there are also east or west regions, its center will be a shadow HBox.
310         // If there are *only* east or west regions, then the shadow layout will be an HBox (or Fit).
311         if (regions.north || regions.south) {
312
313             me.shadowContainer = Ext.create('Ext.container.Container', {
314                 ownerCt: me.owner,
315                 el: me.getTarget(),
316                 layout: Ext.applyIf({
317                     type: 'vbox',
318                     align: 'stretch',
319                     getVisibleItems: me.getVisibleItems
320                 }, me.initialConfig)
321             });
322             me.createItems(me.shadowContainer, vBoxItems);
323
324             // Allow the Splitters to orientate themselves
325             if (me.splitters.north) {
326                 me.splitters.north.ownerCt = me.shadowContainer;
327             }
328             if (me.splitters.south) {
329                 me.splitters.south.ownerCt = me.shadowContainer;
330             }
331
332             // Inject items into the HBox Container if there is one - if there was an east or west.
333             if (me.embeddedContainer) {
334                 me.embeddedContainer.ownerCt = me.shadowContainer;
335                 me.createItems(me.embeddedContainer, hBoxItems);
336
337                 // Allow the Splitters to orientate themselves
338                 if (me.splitters.east) {
339                     me.splitters.east.ownerCt = me.embeddedContainer;
340                 }
341                 if (me.splitters.west) {
342                     me.splitters.west.ownerCt = me.embeddedContainer;
343                 }
344
345                 // These spliiters need to be constrained by components one-level below
346                 // the component in their vobx. We update the min/maxHeight on the helper
347                 // (embeddedContainer) prior to starting the split/drag. This has to be
348                 // done on-the-fly to allow min/maxHeight of the E/C/W regions to be set
349                 // dynamically.
350                 Ext.each([me.splitters.north, me.splitters.south], function (splitter) {
351                     if (splitter) {
352                         splitter.on('beforedragstart', me.fixHeightConstraints, me);
353                     }
354                 });
355
356                 // The east or west region wanted a percentage
357                 if (horizontalFlex) {
358                     regions.center.flex -= horizontalFlex;
359                 }
360                 // The north or south region wanted a percentage
361                 if (verticalFlex) {
362                     me.embeddedContainer.flex -= verticalFlex;
363                 }
364             } else {
365                 // The north or south region wanted a percentage
366                 if (verticalFlex) {
367                     regions.center.flex -= verticalFlex;
368                 }
369             }
370         }
371         // If we have no north or south, then there's only one Container, and it's
372         // an HBox, or, if only a center region was specified, a Fit.
373         else {
374             me.shadowContainer = Ext.create('Ext.container.Container', {
375                 ownerCt: me.owner,
376                 el: me.getTarget(),
377                 layout: Ext.applyIf({
378                     type: (hBoxItems.length == 1) ? 'fit' : 'hbox',
379                     align: 'stretch'
380                 }, me.initialConfig)
381             });
382             me.createItems(me.shadowContainer, hBoxItems);
383
384             // Allow the Splitters to orientate themselves
385             if (me.splitters.east) {
386                 me.splitters.east.ownerCt = me.shadowContainer;
387             }
388             if (me.splitters.west) {
389                 me.splitters.west.ownerCt = me.shadowContainer;
390             }
391
392             // The east or west region wanted a percentage
393             if (horizontalFlex) {
394                 regions.center.flex -= verticalFlex;
395             }
396         }
397
398         // Create upward links from the region Components to their shadow ownerCts
399         for (i = 0, items = me.shadowContainer.items.items, ln = items.length; i < ln; i++) {
400             items[i].shadowOwnerCt = me.shadowContainer;
401         }
402         if (me.embeddedContainer) {
403             for (i = 0, items = me.embeddedContainer.items.items, ln = items.length; i < ln; i++) {
404                 items[i].shadowOwnerCt = me.embeddedContainer;
405             }
406         }
407
408         // This is the layout that we delegate all operations to
409         me.shadowLayout = me.shadowContainer.getLayout();
410
411         me.borderLayoutInitialized = true;
412     },
413
414     setupState: function(comp){
415         var getState = comp.getState;
416         comp.getState = function(){
417             // call the original getState
418             var state = getState.call(comp) || {},
419                 region = comp.region;
420
421             state.collapsed = !!comp.collapsed;
422             if (region == 'west' || region == 'east') {
423                 state.width = comp.getWidth();
424             } else {
425                 state.height = comp.getHeight();
426             }
427             return state;
428         };
429         comp.addStateEvents(['collapse', 'expand', 'resize']);
430     },
431
432     /**
433      * Create the items collection for our shadow/embedded containers
434      * @private
435      */
436     createItems: function(container, items){
437         // Have to inject an items Collection *after* construction.
438         // The child items of the shadow layout must retain their original, user-defined ownerCt
439         delete container.items;
440         container.initItems();
441         container.items.addAll(items);
442     },
443
444     // Private
445     // Create a splitter for a child of the layout.
446     createSplitter: function(comp) {
447         var me = this,
448             interceptCollapse = (comp.collapseMode != 'header'),
449             resizer;
450
451         resizer = Ext.create('Ext.resizer.Splitter', {
452             hidden: !!comp.hidden,
453             collapseTarget: comp,
454             performCollapse: !interceptCollapse,
455             listeners: interceptCollapse ? {
456                 click: {
457                     fn: Ext.Function.bind(me.onSplitterCollapseClick, me, [comp]),
458                     element: 'collapseEl'
459                 }
460             } : null
461         });
462
463         // Mini collapse means that the splitter is the placeholder Component
464         if (comp.collapseMode == 'mini') {
465             comp.placeholder = resizer;
466             resizer.collapsedCls = comp.collapsedCls;
467         }
468
469         // Arrange to hide/show a region's associated splitter when the region is hidden/shown
470         comp.on({
471             hide: me.onRegionVisibilityChange,
472             show: me.onRegionVisibilityChange,
473             scope: me
474         });
475         return resizer;
476     },
477
478     // Private
479     // Propagates the min/maxHeight values from the inner hbox items to its container.
480     fixHeightConstraints: function () {
481         var me = this,
482             ct = me.embeddedContainer,
483             maxHeight = 1e99, minHeight = -1;
484
485         if (!ct) {
486             return;
487         }
488
489         ct.items.each(function (item) {
490             if (Ext.isNumber(item.maxHeight)) {
491                 maxHeight = Math.max(maxHeight, item.maxHeight);
492             }
493             if (Ext.isNumber(item.minHeight)) {
494                 minHeight = Math.max(minHeight, item.minHeight);
495             }
496         });
497
498         ct.maxHeight = maxHeight;
499         ct.minHeight = minHeight;
500     },
501
502     // Hide/show a region's associated splitter when the region is hidden/shown
503     onRegionVisibilityChange: function(comp){
504         this.splitters[comp.region][comp.hidden ? 'hide' : 'show']();
505         this.layout();
506     },
507
508     // Called when a splitter mini-collapse tool is clicked on.
509     // The listener is only added if this layout is controlling collapsing,
510     // not if the component's collapseMode is 'mini' or 'header'.
511     onSplitterCollapseClick: function(comp) {
512         if (comp.collapsed) {
513             this.onPlaceHolderToolClick(null, null, null, {client: comp});
514         } else {
515             comp.collapse();
516         }
517     },
518
519     /**
520      * <p>Return the {@link Ext.panel.Panel#placeholder placeholder} Component to which the passed child Panel of the layout will collapse.
521      * By default, this will be a {@link Ext.panel.Header Header} component (Docked to the appropriate border). See {@link Ext.panel.Panel#placeholder placeholder}.
522      * config to customize this.</p>
523      * <p><b>Note that this will be a fully instantiated Component, but will only be <i>rendered</i> when the Panel is first collapsed.</b></p>
524      * @param {Panel} panel The child Panel of the layout for which to return the {@link Ext.panel.Panel#placeholder placeholder}.
525      * @returns {Component} The Panel's {@link Ext.panel.Panel#placeholder placeholder} unless the {@link Ext.panel.Panel#collapseMode collapseMode} is
526      * <code>'header'</code>, in which case <i>undefined</i> is returned.
527      */
528     getPlaceholder: function(comp) {
529         var me = this,
530             placeholder = comp.placeholder,
531             shadowContainer = comp.shadowOwnerCt,
532             shadowLayout = shadowContainer.layout,
533             oppositeDirection = Ext.panel.Panel.prototype.getOppositeDirection(comp.collapseDirection),
534             horiz = (comp.region == 'north' || comp.region == 'south');
535
536         // No placeholder if the collapse mode is not the Border layout default
537         if (comp.collapseMode == 'header') {
538             return;
539         }
540
541         // Provide a replacement Container with an expand tool
542         if (!placeholder) {
543             if (comp.collapseMode == 'mini') {
544                 placeholder = Ext.create('Ext.resizer.Splitter', {
545                     id: 'collapse-placeholder-' + comp.id,
546                     collapseTarget: comp,
547                     performCollapse: false,
548                     listeners: {
549                         click: {
550                             fn: Ext.Function.bind(me.onSplitterCollapseClick, me, [comp]),
551                             element: 'collapseEl'
552                         }
553                     }
554                 });
555                 placeholder.addCls(placeholder.collapsedCls);
556             } else {
557                 placeholder = {
558                     id: 'collapse-placeholder-' + comp.id,
559                     margins: comp.initialConfig.margins || Ext.getClass(comp).prototype.margins,
560                     xtype: 'header',
561                     orientation: horiz ? 'horizontal' : 'vertical',
562                     title: comp.title,
563                     textCls: comp.headerTextCls,
564                     iconCls: comp.iconCls,
565                     baseCls: comp.baseCls + '-header',
566                     ui: comp.ui,
567                     indicateDrag: comp.draggable,
568                     cls: Ext.baseCSSPrefix + 'region-collapsed-placeholder ' + Ext.baseCSSPrefix + 'region-collapsed-' + comp.collapseDirection + '-placeholder ' + comp.collapsedCls,
569                     listeners: comp.floatable ? {
570                         click: {
571                             fn: function(e) {
572                                 me.floatCollapsedPanel(e, comp);
573                             },
574                             element: 'el'
575                         }
576                     } : null
577                 };
578                 // Hack for IE6/7/IEQuirks's inability to display an inline-block
579                 if ((Ext.isIE6 || Ext.isIE7 || (Ext.isIEQuirks)) && !horiz) {
580                     placeholder.width = 25;
581                 }
582                 if (!comp.hideCollapseTool) {
583                     placeholder[horiz ? 'tools' : 'items'] = [{
584                         xtype: 'tool',
585                         client: comp,
586                         type: 'expand-' + oppositeDirection,
587                         handler: me.onPlaceHolderToolClick,
588                         scope: me
589                     }];
590                 }
591             }
592             placeholder = me.owner.createComponent(placeholder);
593             if (comp.isXType('panel')) {
594                 comp.on({
595                     titlechange: me.onRegionTitleChange,
596                     iconchange: me.onRegionIconChange,
597                     scope: me
598                 });
599             }
600         }
601
602         // The collapsed Component holds a reference to its placeholder and vice versa
603         comp.placeholder = placeholder;
604         placeholder.comp = comp;
605
606         return placeholder;
607     },
608
609     /**
610      * @private
611      * Update the placeholder title when panel title has been set or changed.
612      */
613     onRegionTitleChange: function(comp, newTitle) {
614         comp.placeholder.setTitle(newTitle);
615     },
616
617     /**
618      * @private
619      * Update the placeholder iconCls when panel iconCls has been set or changed.
620      */
621     onRegionIconChange: function(comp, newIconCls) {
622         comp.placeholder.setIconCls(newIconCls);
623     },
624
625     /**
626      * @private
627      * Calculates the size and positioning of the passed child item. Must be present because Panel's expand,
628      * when configured with a flex, calls this method on its ownerCt's layout.
629      * @param {Component} child The child Component to calculate the box for
630      * @return {Object} Object containing box measurements for the child. Properties are left,top,width,height.
631      */
632     calculateChildBox: function(comp) {
633         var me = this;
634         if (me.shadowContainer.items.contains(comp)) {
635             return me.shadowContainer.layout.calculateChildBox(comp);
636         }
637         else if (me.embeddedContainer && me.embeddedContainer.items.contains(comp)) {
638             return me.embeddedContainer.layout.calculateChildBox(comp);
639         }
640     },
641
642     /**
643      * @private
644      * Intercepts the Panel's own collapse event and perform's substitution of the Panel
645      * with a placeholder Header orientated in the appropriate dimension.
646      * @param comp The Panel being collapsed.
647      * @param direction
648      * @param animate
649      * @returns {Boolean} false to inhibit the Panel from performing its own collapse.
650      */
651     onBeforeRegionCollapse: function(comp, direction, animate) {
652         var me = this,
653             compEl = comp.el,
654             width,
655             miniCollapse = comp.collapseMode == 'mini',
656             shadowContainer = comp.shadowOwnerCt,
657             shadowLayout = shadowContainer.layout,
658             placeholder = comp.placeholder,
659             sl = me.owner.suspendLayout,
660             scsl = shadowContainer.suspendLayout,
661             isNorthOrWest = (comp.region == 'north' || comp.region == 'west'); // Flag to keep the placeholder non-adjacent to any Splitter
662
663         // Do not trigger a layout during transition to collapsed Component
664         me.owner.suspendLayout = true;
665         shadowContainer.suspendLayout = true;
666
667         // Prevent upward notifications from downstream layouts
668         shadowLayout.layoutBusy = true;
669         if (shadowContainer.componentLayout) {
670             shadowContainer.componentLayout.layoutBusy = true;
671         }
672         me.shadowContainer.layout.layoutBusy = true;
673         me.layoutBusy = true;
674         me.owner.componentLayout.layoutBusy = true;
675
676         // Provide a replacement Container with an expand tool
677         if (!placeholder) {
678             placeholder = me.getPlaceholder(comp);
679         }
680
681         // placeholder already in place; show it.
682         if (placeholder.shadowOwnerCt === shadowContainer) {
683             placeholder.show();
684         }
685         // Insert the collapsed placeholder Component into the appropriate Box layout shadow Container
686         // It must go next to its client Component, but non-adjacent to the splitter so splitter can find its collapse client.
687         // Inject an ownerCt value pointing to the owner, border layout Container as the user will expect.
688         else {
689             shadowContainer.insert(shadowContainer.items.indexOf(comp) + (isNorthOrWest ? 0 : 1), placeholder);
690             placeholder.shadowOwnerCt = shadowContainer;
691             placeholder.ownerCt = me.owner;
692         }
693
694         // Flag the collapsing Component as hidden and show the placeholder.
695         // This causes the shadow Box layout's calculateChildBoxes to calculate the correct new arrangement.
696         // We hide or slideOut the Component's element
697         comp.hidden = true;
698
699         if (!placeholder.rendered) {
700             shadowLayout.renderItem(placeholder, shadowLayout.innerCt);
701
702             // The inserted placeholder does not have the proper size, so copy the width
703             // for N/S or the height for E/W from the component. This fixes EXTJSIV-1562
704             // without recursive layouts. This is only an issue initially. After this time,
705             // placeholder will have the correct width/height set by the layout (which has
706             // already happened when we get here initially).
707             if (comp.region == 'north' || comp.region == 'south') {
708                 placeholder.setCalculatedSize(comp.getWidth());
709             } else {
710                 placeholder.setCalculatedSize(undefined, comp.getHeight());
711             }
712         }
713
714         // Jobs to be done after the collapse has been done
715         function afterCollapse() {
716             // Reinstate automatic laying out.
717             me.owner.suspendLayout = sl;
718             shadowContainer.suspendLayout = scsl;
719             delete shadowLayout.layoutBusy;
720             if (shadowContainer.componentLayout) {
721                 delete shadowContainer.componentLayout.layoutBusy;
722             }
723             delete me.shadowContainer.layout.layoutBusy;
724             delete me.layoutBusy;
725             delete me.owner.componentLayout.layoutBusy;
726
727             // Fire the collapse event: The Panel has in fact been collapsed, but by substitution of an alternative Component
728             comp.collapsed = true;
729             comp.fireEvent('collapse', comp);
730         }
731
732         /*
733          * Set everything to the new positions. Note that we
734          * only want to animate the collapse if it wasn't configured
735          * initially with collapsed: true
736          */
737         if (comp.animCollapse && me.initialCollapsedComplete) {
738             shadowLayout.layout();
739             compEl.dom.style.zIndex = 100;
740
741             // If we're mini-collapsing, the placholder is a Splitter. We don't want it to "bounce in"
742             if (!miniCollapse) {
743                 placeholder.el.hide();
744             }
745             compEl.slideOut(me.slideDirection[comp.region], {
746                 duration: Ext.Number.from(comp.animCollapse, Ext.fx.Anim.prototype.duration),
747                 listeners: {
748                     afteranimate: function() {
749                         compEl.show().setLeftTop(-10000, -10000);
750                         compEl.dom.style.zIndex = '';
751
752                         // If we're mini-collapsing, the placholder is a Splitter. We don't want it to "bounce in"
753                        if (!miniCollapse) {
754                             placeholder.el.slideIn(me.slideDirection[comp.region], {
755                                 easing: 'linear',
756                                 duration: 100
757                             });
758                         }
759                         afterCollapse();
760                     }
761                 }
762             });
763         } else {
764             compEl.setLeftTop(-10000, -10000);
765             shadowLayout.layout();
766             afterCollapse();
767         }
768
769         return false;
770     },
771
772     // Hijack the expand operation to remove the placeholder and slide the region back in.
773     onBeforeRegionExpand: function(comp, animate) {
774         this.onPlaceHolderToolClick(null, null, null, {client: comp});
775         return false;
776     },
777
778     // Called when the collapsed placeholder is clicked to reinstate a "collapsed" (in reality hidden) Panel.
779     onPlaceHolderToolClick: function(e, target, owner, tool) {
780         var me = this,
781             comp = tool.client,
782
783             // Hide the placeholder unless it was the Component's preexisting splitter
784             hidePlaceholder = (comp.collapseMode != 'mini') || !comp.split,
785             compEl = comp.el,
786             toCompBox,
787             placeholder = comp.placeholder,
788             placeholderEl = placeholder.el,
789             shadowContainer = comp.shadowOwnerCt,
790             shadowLayout = shadowContainer.layout,
791             curSize,
792             sl = me.owner.suspendLayout,
793             scsl = shadowContainer.suspendLayout,
794             isFloating;
795
796         // If the slide in is still going, stop it.
797         // This will either leave the Component in its fully floated state (which is processed below)
798         // or in its collapsed state. Either way, we expand it..
799         if (comp.getActiveAnimation()) {
800             comp.stopAnimation();
801         }
802
803         // If the Component is fully floated when they click the placeholder Tool,
804         // it will be primed with a slide out animation object... so delete that
805         // and remove the mouseout listeners
806         if (comp.slideOutAnim) {
807             // Remove mouse leave monitors
808             compEl.un(comp.panelMouseMon);
809             placeholderEl.un(comp.placeholderMouseMon);
810
811             delete comp.slideOutAnim;
812             delete comp.panelMouseMon;
813             delete comp.placeholderMouseMon;
814
815             // If the Panel was floated and primed with a slideOut animation, we don't want to animate its layout operation.
816             isFloating = true;
817         }
818
819         // Do not trigger a layout during transition to expanded Component
820         me.owner.suspendLayout = true;
821         shadowContainer.suspendLayout = true;
822
823         // Prevent upward notifications from downstream layouts
824         shadowLayout.layoutBusy = true;
825         if (shadowContainer.componentLayout) {
826             shadowContainer.componentLayout.layoutBusy = true;
827         }
828         me.shadowContainer.layout.layoutBusy = true;
829         me.layoutBusy = true;
830         me.owner.componentLayout.layoutBusy = true;
831
832         // Unset the hidden and collapsed flags set in onBeforeRegionCollapse. The shadowLayout will now take it into account
833         // Find where the shadow Box layout plans to put the expanding Component.
834         comp.hidden = false;
835         comp.collapsed = false;
836         if (hidePlaceholder) {
837             placeholder.hidden = true;
838         }
839         toCompBox = shadowLayout.calculateChildBox(comp);
840
841         // Show the collapse tool in case it was hidden by the slide-in
842         if (comp.collapseTool) {
843             comp.collapseTool.show();
844         }
845
846         // If we're going to animate, we need to hide the component before moving it back into position
847         if (comp.animCollapse && !isFloating) {
848             compEl.setStyle('visibility', 'hidden');
849         }
850         compEl.setLeftTop(toCompBox.left, toCompBox.top);
851
852         // Equalize the size of the expanding Component prior to animation
853         // in case the layout area has changed size during the time it was collapsed.
854         curSize = comp.getSize();
855         if (curSize.height != toCompBox.height || curSize.width != toCompBox.width) {
856             me.setItemSize(comp, toCompBox.width, toCompBox.height);
857         }
858
859         // Jobs to be done after the expand has been done
860         function afterExpand() {
861             // Reinstate automatic laying out.
862             me.owner.suspendLayout = sl;
863             shadowContainer.suspendLayout = scsl;
864             delete shadowLayout.layoutBusy;
865             if (shadowContainer.componentLayout) {
866                 delete shadowContainer.componentLayout.layoutBusy;
867             }
868             delete me.shadowContainer.layout.layoutBusy;
869             delete me.layoutBusy;
870             delete me.owner.componentLayout.layoutBusy;
871
872             // In case it was floated out and they clicked the re-expand tool
873             comp.removeCls(Ext.baseCSSPrefix + 'border-region-slide-in');
874
875             // Fire the expand event: The Panel has in fact been expanded, but by removal of an alternative Component
876             comp.fireEvent('expand', comp);
877         }
878
879         // Hide the placeholder
880         if (hidePlaceholder) {
881             placeholder.el.hide();
882         }
883
884         // Slide the expanding Component to its new position.
885         // When that is done, layout the layout.
886         if (comp.animCollapse && !isFloating) {
887             compEl.dom.style.zIndex = 100;
888             compEl.slideIn(me.slideDirection[comp.region], {
889                 duration: Ext.Number.from(comp.animCollapse, Ext.fx.Anim.prototype.duration),
890                 listeners: {
891                     afteranimate: function() {
892                         compEl.dom.style.zIndex = '';
893                         comp.hidden = false;
894                         shadowLayout.onLayout();
895                         afterExpand();
896                     }
897                 }
898             });
899         } else {
900             shadowLayout.onLayout();
901             afterExpand();
902         }
903     },
904
905     floatCollapsedPanel: function(e, comp) {
906
907         if (comp.floatable === false) {
908             return;
909         }
910
911         var me = this,
912             compEl = comp.el,
913             placeholder = comp.placeholder,
914             placeholderEl = placeholder.el,
915             shadowContainer = comp.shadowOwnerCt,
916             shadowLayout = shadowContainer.layout,
917             placeholderBox = shadowLayout.getChildBox(placeholder),
918             scsl = shadowContainer.suspendLayout,
919             curSize, toCompBox, compAnim;
920
921         // Ignore clicks on tools.
922         if (e.getTarget('.' + Ext.baseCSSPrefix + 'tool')) {
923             return;
924         }
925
926         // It's *being* animated, ignore the click.
927         // Possible future enhancement: Stop and *reverse* the current active Fx.
928         if (compEl.getActiveAnimation()) {
929             return;
930         }
931
932         // If the Component is already fully floated when they click the placeholder,
933         // it will be primed with a slide out animation object... so slide it out.
934         if (comp.slideOutAnim) {
935             me.slideOutFloatedComponent(comp);
936             return;
937         }
938
939         // Function to be called when the mouse leaves the floated Panel
940         // Slide out when the mouse leaves the region bounded by the slid Component and its placeholder.
941         function onMouseLeaveFloated(e) {
942             var slideRegion = compEl.getRegion().union(placeholderEl.getRegion()).adjust(1, -1, -1, 1);
943
944             // If mouse is not within slide Region, slide it out
945             if (!slideRegion.contains(e.getPoint())) {
946                 me.slideOutFloatedComponent(comp);
947             }
948         }
949
950         // Monitor for mouseouting of the placeholder. Hide it if they exit for half a second or more
951         comp.placeholderMouseMon = placeholderEl.monitorMouseLeave(500, onMouseLeaveFloated);
952
953         // Do not trigger a layout during slide out of the Component
954         shadowContainer.suspendLayout = true;
955
956         // Prevent upward notifications from downstream layouts
957         me.layoutBusy = true;
958         me.owner.componentLayout.layoutBusy = true;
959
960         // The collapse tool is hidden while slid.
961         // It is re-shown on expand.
962         if (comp.collapseTool) {
963             comp.collapseTool.hide();
964         }
965
966         // Set flags so that the layout will calculate the boxes for what we want
967         comp.hidden = false;
968         comp.collapsed = false;
969         placeholder.hidden = true;
970
971         // Recalculate new arrangement of the Component being floated.
972         toCompBox = shadowLayout.calculateChildBox(comp);
973         placeholder.hidden = false;
974
975         // Component to appear just after the placeholder, whatever "after" means in the context of the shadow Box layout.
976         if (comp.region == 'north' || comp.region == 'west') {
977             toCompBox[shadowLayout.parallelBefore] += placeholderBox[shadowLayout.parallelPrefix] - 1;
978         } else {
979             toCompBox[shadowLayout.parallelBefore] -= (placeholderBox[shadowLayout.parallelPrefix] - 1);
980         }
981         compEl.setStyle('visibility', 'hidden');
982         compEl.setLeftTop(toCompBox.left, toCompBox.top);
983
984         // Equalize the size of the expanding Component prior to animation
985         // in case the layout area has changed size during the time it was collapsed.
986         curSize = comp.getSize();
987         if (curSize.height != toCompBox.height || curSize.width != toCompBox.width) {
988             me.setItemSize(comp, toCompBox.width, toCompBox.height);
989         }
990
991         // This animation slides the collapsed Component's el out to just beyond its placeholder
992         compAnim = {
993             listeners: {
994                 afteranimate: function() {
995                     shadowContainer.suspendLayout = scsl;
996                     delete me.layoutBusy;
997                     delete me.owner.componentLayout.layoutBusy;
998
999                     // Prime the Component with an Anim config object to slide it back out
1000                     compAnim.listeners = {
1001                         afterAnimate: function() {
1002                             compEl.show().removeCls(Ext.baseCSSPrefix + 'border-region-slide-in').setLeftTop(-10000, -10000);
1003
1004                             // Reinstate the correct, current state after slide out animation finishes
1005                             comp.hidden = true;
1006                             comp.collapsed = true;
1007                             delete comp.slideOutAnim;
1008                             delete comp.panelMouseMon;
1009                             delete comp.placeholderMouseMon;
1010                         }
1011                     };
1012                     comp.slideOutAnim = compAnim;
1013                 }
1014             },
1015             duration: 500
1016         };
1017
1018         // Give the element the correct class which places it at a high z-index
1019         compEl.addCls(Ext.baseCSSPrefix + 'border-region-slide-in');
1020
1021         // Begin the slide in
1022         compEl.slideIn(me.slideDirection[comp.region], compAnim);
1023
1024         // Monitor for mouseouting of the slid area. Hide it if they exit for half a second or more
1025         comp.panelMouseMon = compEl.monitorMouseLeave(500, onMouseLeaveFloated);
1026
1027     },
1028
1029     slideOutFloatedComponent: function(comp) {
1030         var compEl = comp.el,
1031             slideOutAnim;
1032
1033         // Remove mouse leave monitors
1034         compEl.un(comp.panelMouseMon);
1035         comp.placeholder.el.un(comp.placeholderMouseMon);
1036
1037         // Slide the Component out
1038         compEl.slideOut(this.slideDirection[comp.region], comp.slideOutAnim);
1039
1040         delete comp.slideOutAnim;
1041         delete comp.panelMouseMon;
1042         delete comp.placeholderMouseMon;
1043     },
1044
1045     /*
1046      * @private
1047      * Ensure any collapsed placeholder Component is destroyed along with its region.
1048      * Can't do this in onDestroy because they may remove a Component and use it elsewhere.
1049      */
1050     onRegionDestroy: function(comp) {
1051         var placeholder = comp.placeholder;
1052         if (placeholder) {
1053             delete placeholder.ownerCt;
1054             placeholder.destroy();
1055         }
1056     },
1057
1058     /*
1059      * @private
1060      * Ensure any shadow Containers are destroyed.
1061      * Ensure we don't keep references to Components.
1062      */
1063     onDestroy: function() {
1064         var me = this,
1065             shadowContainer = me.shadowContainer,
1066             embeddedContainer = me.embeddedContainer;
1067
1068         if (shadowContainer) {
1069             delete shadowContainer.ownerCt;
1070             Ext.destroy(shadowContainer);
1071         }
1072
1073         if (embeddedContainer) {
1074             delete embeddedContainer.ownerCt;
1075             Ext.destroy(embeddedContainer);
1076         }
1077         delete me.regions;
1078         delete me.splitters;
1079         delete me.shadowContainer;
1080         delete me.embeddedContainer;
1081         me.callParent(arguments);
1082     }
1083 });
1084