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