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