Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / src / layout / component / AbstractDock.js
1 /**
2  * @class Ext.layout.component.AbstractDock
3  * @extends Ext.layout.component.Component
4  * @private
5  * This ComponentLayout handles docking for Panels. It takes care of panels that are
6  * part of a ContainerLayout that sets this Panel's size and Panels that are part of
7  * an AutoContainerLayout in which this panel get his height based of the CSS or
8  * or its content.
9  */
10
11 Ext.define('Ext.layout.component.AbstractDock', {
12
13     /* Begin Definitions */
14
15     extend: 'Ext.layout.component.Component',
16
17     /* End Definitions */
18
19     type: 'dock',
20
21     /**
22      * @private
23      * @property autoSizing
24      * @type boolean
25      * This flag is set to indicate this layout may have an autoHeight/autoWidth.
26      */
27     autoSizing: true,
28
29     beforeLayout: function() {
30         var returnValue = this.callParent(arguments);
31         if (returnValue !== false && (!this.initializedBorders || this.childrenChanged) && (!this.owner.border || this.owner.manageBodyBorders)) {
32             this.handleItemBorders();
33             this.initializedBorders = true;
34         }
35         return returnValue;
36     },
37     
38     handleItemBorders: function() {
39         var owner = this.owner,
40             body = owner.body,
41             docked = this.getLayoutItems(),
42             borders = {
43                 top: [],
44                 right: [],
45                 bottom: [],
46                 left: []
47             },
48             oldBorders = this.borders,
49             opposites = {
50                 top: 'bottom',
51                 right: 'left',
52                 bottom: 'top',
53                 left: 'right'
54             },
55             i, ln, item, dock, side;
56
57         for (i = 0, ln = docked.length; i < ln; i++) {
58             item = docked[i];
59             dock = item.dock;
60             
61             if (item.ignoreBorderManagement) {
62                 continue;
63             }
64             
65             if (!borders[dock].satisfied) {
66                 borders[dock].push(item);
67                 borders[dock].satisfied = true;
68             }
69             
70             if (!borders.top.satisfied && opposites[dock] !== 'top') {
71                 borders.top.push(item);
72             }
73             if (!borders.right.satisfied && opposites[dock] !== 'right') {
74                 borders.right.push(item);
75             }            
76             if (!borders.bottom.satisfied && opposites[dock] !== 'bottom') {
77                 borders.bottom.push(item);
78             }            
79             if (!borders.left.satisfied && opposites[dock] !== 'left') {
80                 borders.left.push(item);
81             }
82         }
83
84         if (oldBorders) {
85             for (side in oldBorders) {
86                 if (oldBorders.hasOwnProperty(side)) {
87                     ln = oldBorders[side].length;
88                     if (!owner.manageBodyBorders) {
89                         for (i = 0; i < ln; i++) {
90                             oldBorders[side][i].removeCls(Ext.baseCSSPrefix + 'docked-noborder-' + side);
91                         }
92                         if (!oldBorders[side].satisfied && !owner.bodyBorder) {
93                             body.removeCls(Ext.baseCSSPrefix + 'docked-noborder-' + side);                   
94                         }                    
95                     }
96                     else if (oldBorders[side].satisfied) {
97                         body.setStyle('border-' + side + '-width', '');
98                     }
99                 }
100             }
101         }
102                 
103         for (side in borders) {
104             if (borders.hasOwnProperty(side)) {
105                 ln = borders[side].length;
106                 if (!owner.manageBodyBorders) {
107                     for (i = 0; i < ln; i++) {
108                         borders[side][i].addCls(Ext.baseCSSPrefix + 'docked-noborder-' + side);
109                     }
110                     if ((!borders[side].satisfied && !owner.bodyBorder) || owner.bodyBorder === false) {
111                         body.addCls(Ext.baseCSSPrefix + 'docked-noborder-' + side);                   
112                     }                    
113                 }
114                 else if (borders[side].satisfied) {
115                     body.setStyle('border-' + side + '-width', '1px');
116                 }
117             }
118         }
119         
120         this.borders = borders;
121     },
122     
123     /**
124      * @protected
125      * @param {Ext.Component} owner The Panel that owns this DockLayout
126      * @param {Ext.core.Element} target The target in which we are going to render the docked items
127      * @param {Array} args The arguments passed to the ComponentLayout.layout method
128      */
129     onLayout: function(width, height) {
130         var me = this,
131             owner = me.owner,
132             body = owner.body,
133             layout = owner.layout,
134             target = me.getTarget(),
135             autoWidth = false,
136             autoHeight = false,
137             padding, border, frameSize;
138
139         // We start of by resetting all the layouts info
140         var info = me.info = {
141             boxes: [],
142             size: {
143                 width: width,
144                 height: height
145             },
146             bodyBox: {}
147         };
148
149         Ext.applyIf(info, me.getTargetInfo());
150
151         // We need to bind to the ownerCt whenever we do not have a user set height or width.
152         if (owner && owner.ownerCt && owner.ownerCt.layout && owner.ownerCt.layout.isLayout) {
153             if (!Ext.isNumber(owner.height) || !Ext.isNumber(owner.width)) {
154                 owner.ownerCt.layout.bindToOwnerCtComponent = true;
155             }
156             else {
157                 owner.ownerCt.layout.bindToOwnerCtComponent = false;
158             }
159         }
160
161         // Determine if we have an autoHeight or autoWidth.
162         if (height === undefined || height === null || width === undefined || width === null) {
163             padding = info.padding;
164             border = info.border;
165             frameSize = me.frameSize;
166
167             // Auto-everything, clear out any style height/width and read from css
168             if ((height === undefined || height === null) && (width === undefined || width === null)) {
169                 autoHeight = true;
170                 autoWidth = true;
171                 me.setTargetSize(null);
172                 me.setBodyBox({width: null, height: null});
173             }
174             // Auto-height
175             else if (height === undefined || height === null) {
176                 autoHeight = true;
177                 // Clear any sizing that we already set in a previous layout
178                 me.setTargetSize(width);
179                 me.setBodyBox({width: width - padding.left - border.left - padding.right - border.right - frameSize.left - frameSize.right, height: null});
180             // Auto-width
181             }
182             else {
183                 autoWidth = true;
184                 // Clear any sizing that we already set in a previous layout
185                 me.setTargetSize(null, height);
186                 me.setBodyBox({width: null, height: height - padding.top - padding.bottom - border.top - border.bottom - frameSize.top - frameSize.bottom});
187             }
188
189             // Run the container
190             if (layout && layout.isLayout) {
191                 // Auto-Sized so have the container layout notify the component layout.
192                 layout.bindToOwnerCtComponent = true;
193                 layout.layout();
194
195                 // If this is an autosized container layout, then we must compensate for a
196                 // body that is being autosized.  We do not want to adjust the body's size
197                 // to accommodate the dock items, but rather we will want to adjust the
198                 // target's size.
199                 //
200                 // This is necessary because, particularly in a Box layout, all child items
201                 // are set with absolute dimensions that are not flexible to the size of its
202                 // innerCt/target.  So once they are laid out, they are sized for good. By
203                 // shrinking the body box to accommodate dock items, we're merely cutting off
204                 // parts of the body.  Not good.  Instead, the target's size should expand
205                 // to fit the dock items in.  This is valid because the target container is
206                 // suppose to be autosized to fit everything accordingly.
207                 info.autoSizedCtLayout = layout.autoSize === true;
208             }
209
210             // The dockItems method will add all the top and bottom docked items height
211             // to the info.panelSize height. That's why we have to call setSize after
212             // we dock all the items to actually set the panel's width and height.
213             // We have to do this because the panel body and docked items will be position
214             // absolute which doesn't stretch the panel.
215             me.dockItems(autoWidth, autoHeight);
216             me.setTargetSize(info.size.width, info.size.height);
217         }
218         else {
219             me.setTargetSize(width, height);
220             me.dockItems();
221         }
222         me.callParent(arguments);
223     },
224
225     /**
226      * @protected
227      * This method will first update all the information about the docked items,
228      * body dimensions and position, the panel's total size. It will then
229      * set all these values on the docked items and panel body.
230      * @param {Array} items Array containing all the docked items
231      * @param {Boolean} autoBoxes Set this to true if the Panel is part of an
232      * AutoContainerLayout
233      */
234     dockItems : function(autoWidth, autoHeight) {
235         this.calculateDockBoxes(autoWidth, autoHeight);
236
237         // Both calculateAutoBoxes and calculateSizedBoxes are changing the
238         // information about the body, panel size, and boxes for docked items
239         // inside a property called info.
240         var info = this.info,
241             boxes = info.boxes,
242             ln = boxes.length,
243             dock, i;
244
245         // We are going to loop over all the boxes that were calculated
246         // and set the position of each item the box belongs to.
247         for (i = 0; i < ln; i++) {
248             dock = boxes[i];
249             dock.item.setPosition(dock.x, dock.y);
250             if ((autoWidth || autoHeight) && dock.layout && dock.layout.isLayout) {
251                 // Auto-Sized so have the container layout notify the component layout.
252                 dock.layout.bindToOwnerCtComponent = true;
253             }
254         }
255
256         // Don't adjust body width/height if the target is using an auto container layout.
257         // But, we do want to adjust the body size if the container layout is auto sized.
258         if (!info.autoSizedCtLayout) {
259             if (autoWidth) {
260                 info.bodyBox.width = null;
261             }
262             if (autoHeight) {
263                 info.bodyBox.height = null;
264             }
265         }
266
267         // If the bodyBox has been adjusted because of the docked items
268         // we will update the dimensions and position of the panel's body.
269         this.setBodyBox(info.bodyBox);
270     },
271
272     /**
273      * @protected
274      * This method will set up some initial information about the panel size and bodybox
275      * and then loop over all the items you pass it to take care of stretching, aligning,
276      * dock position and all calculations involved with adjusting the body box.
277      * @param {Array} items Array containing all the docked items we have to layout
278      */
279     calculateDockBoxes : function(autoWidth, autoHeight) {
280         // We want to use the Panel's el width, and the Panel's body height as the initial
281         // size we are going to use in calculateDockBoxes. We also want to account for
282         // the border of the panel.
283         var me = this,
284             target = me.getTarget(),
285             items = me.getLayoutItems(),
286             owner = me.owner,
287             bodyEl = owner.body,
288             info = me.info,
289             size = info.size,
290             ln = items.length,
291             padding = info.padding,
292             border = info.border,
293             frameSize = me.frameSize,
294             item, i, box, rect;
295
296         // If this Panel is inside an AutoContainerLayout, we will base all the calculations
297         // around the height of the body and the width of the panel.
298         if (autoHeight) {
299             size.height = bodyEl.getHeight() + padding.top + border.top + padding.bottom + border.bottom + frameSize.top + frameSize.bottom;
300         }
301         else {
302             size.height = target.getHeight();
303         }
304         if (autoWidth) {
305             size.width = bodyEl.getWidth() + padding.left + border.left + padding.right + border.right + frameSize.left + frameSize.right;
306         }
307         else {
308             size.width = target.getWidth();
309         }
310
311         info.bodyBox = {
312             x: padding.left + frameSize.left,
313             y: padding.top + frameSize.top,
314             width: size.width - padding.left - border.left - padding.right - border.right - frameSize.left - frameSize.right,
315             height: size.height - border.top - padding.top - border.bottom - padding.bottom - frameSize.top - frameSize.bottom
316         };
317
318         // Loop over all the docked items
319         for (i = 0; i < ln; i++) {
320             item = items[i];
321             // The initBox method will take care of stretching and alignment
322             // In some cases it will also layout the dock items to be able to
323             // get a width or height measurement
324             box = me.initBox(item);
325
326             if (autoHeight === true) {
327                 box = me.adjustAutoBox(box, i);
328             }
329             else {
330                 box = me.adjustSizedBox(box, i);
331             }
332
333             // Save our box. This allows us to loop over all docked items and do all
334             // calculations first. Then in one loop we will actually size and position
335             // all the docked items that have changed.
336             info.boxes.push(box);
337         }
338     },
339
340     /**
341      * @protected
342      * This method will adjust the position of the docked item and adjust the body box
343      * accordingly.
344      * @param {Object} box The box containing information about the width and height
345      * of this docked item
346      * @param {Number} index The index position of this docked item
347      * @return {Object} The adjusted box
348      */
349     adjustSizedBox : function(box, index) {
350         var bodyBox = this.info.bodyBox,
351             frameSize = this.frameSize,
352             info = this.info,
353             padding = info.padding,
354             pos = box.type,
355             border = info.border;
356
357         switch (pos) {
358             case 'top':
359                 box.y = bodyBox.y;
360                 break;
361
362             case 'left':
363                 box.x = bodyBox.x;
364                 break;
365
366             case 'bottom':
367                 box.y = (bodyBox.y + bodyBox.height) - box.height;
368                 break;
369
370             case 'right':
371                 box.x = (bodyBox.x + bodyBox.width) - box.width;
372                 break;
373         }
374
375         if (box.ignoreFrame) {
376             if (pos == 'bottom') {
377                 box.y += (frameSize.bottom + padding.bottom + border.bottom);
378             }
379             else {
380                 box.y -= (frameSize.top + padding.top + border.top);
381             }
382             if (pos == 'right') {
383                 box.x += (frameSize.right + padding.right + border.right);
384             }
385             else {
386                 box.x -= (frameSize.left + padding.left + border.left);
387             }
388         }
389
390         // If this is not an overlaying docked item, we have to adjust the body box
391         if (!box.overlay) {
392             switch (pos) {
393                 case 'top':
394                     bodyBox.y += box.height;
395                     bodyBox.height -= box.height;
396                     break;
397
398                 case 'left':
399                     bodyBox.x += box.width;
400                     bodyBox.width -= box.width;
401                     break;
402
403                 case 'bottom':
404                     bodyBox.height -= box.height;
405                     break;
406
407                 case 'right':
408                     bodyBox.width -= box.width;
409                     break;
410             }
411         }
412         return box;
413     },
414
415     /**
416      * @protected
417      * This method will adjust the position of the docked item inside an AutoContainerLayout
418      * and adjust the body box accordingly.
419      * @param {Object} box The box containing information about the width and height
420      * of this docked item
421      * @param {Number} index The index position of this docked item
422      * @return {Object} The adjusted box
423      */
424     adjustAutoBox : function (box, index) {
425         var info = this.info,
426             bodyBox = info.bodyBox,
427             size = info.size,
428             boxes = info.boxes,
429             boxesLn = boxes.length,
430             pos = box.type,
431             frameSize = this.frameSize,
432             padding = info.padding,
433             border = info.border,
434             autoSizedCtLayout = info.autoSizedCtLayout,
435             ln = (boxesLn < index) ? boxesLn : index,
436             i, adjustBox;
437
438         if (pos == 'top' || pos == 'bottom') {
439             // This can affect the previously set left and right and bottom docked items
440             for (i = 0; i < ln; i++) {
441                 adjustBox = boxes[i];
442                 if (adjustBox.stretched && adjustBox.type == 'left' || adjustBox.type == 'right') {
443                     adjustBox.height += box.height;
444                 }
445                 else if (adjustBox.type == 'bottom') {
446                     adjustBox.y += box.height;
447                 }
448             }
449         }
450
451         switch (pos) {
452             case 'top':
453                 box.y = bodyBox.y;
454                 if (!box.overlay) {
455                     bodyBox.y += box.height;
456                 }
457                 size.height += box.height;
458                 break;
459
460             case 'bottom':
461                 box.y = (bodyBox.y + bodyBox.height);
462                 size.height += box.height;
463                 break;
464
465             case 'left':
466                 box.x = bodyBox.x;
467                 if (!box.overlay) {
468                     bodyBox.x += box.width;
469                     if (autoSizedCtLayout) {
470                         size.width += box.width;
471                     } else {
472                         bodyBox.width -= box.width;
473                     }
474                 }
475                 break;
476
477             case 'right':
478                 if (!box.overlay) {
479                     if (autoSizedCtLayout) {
480                         size.width += box.width;
481                     } else {
482                         bodyBox.width -= box.width;
483                     }
484                 }
485                 box.x = (bodyBox.x + bodyBox.width);
486                 break;
487         }
488
489         if (box.ignoreFrame) {
490             if (pos == 'bottom') {
491                 box.y += (frameSize.bottom + padding.bottom + border.bottom);
492             }
493             else {
494                 box.y -= (frameSize.top + padding.top + border.top);
495             }
496             if (pos == 'right') {
497                 box.x += (frameSize.right + padding.right + border.right);
498             }
499             else {
500                 box.x -= (frameSize.left + padding.left + border.left);
501             }
502         }
503         return box;
504     },
505
506     /**
507      * @protected
508      * This method will create a box object, with a reference to the item, the type of dock
509      * (top, left, bottom, right). It will also take care of stretching and aligning of the
510      * docked items.
511      * @param {Ext.Component} item The docked item we want to initialize the box for
512      * @return {Object} The initial box containing width and height and other useful information
513      */
514     initBox : function(item) {
515         var me = this,
516             bodyBox = me.info.bodyBox,
517             horizontal = (item.dock == 'top' || item.dock == 'bottom'),
518             owner = me.owner,
519             frameSize = me.frameSize,
520             info = me.info,
521             padding = info.padding,
522             border = info.border,
523             box = {
524                 item: item,
525                 overlay: item.overlay,
526                 type: item.dock,
527                 offsets: Ext.core.Element.parseBox(item.offsets || {}),
528                 ignoreFrame: item.ignoreParentFrame
529             };
530         // First we are going to take care of stretch and align properties for all four dock scenarios.
531         if (item.stretch !== false) {
532             box.stretched = true;
533             if (horizontal) {
534                 box.x = bodyBox.x + box.offsets.left;
535                 box.width = bodyBox.width - (box.offsets.left + box.offsets.right);
536                 if (box.ignoreFrame) {
537                     box.width += (frameSize.left + frameSize.right + border.left + border.right + padding.left + padding.right);
538                 }
539                 item.setCalculatedSize(box.width - item.el.getMargin('lr'), undefined, owner);
540             }
541             else {
542                 box.y = bodyBox.y + box.offsets.top;
543                 box.height = bodyBox.height - (box.offsets.bottom + box.offsets.top);
544                 if (box.ignoreFrame) {
545                     box.height += (frameSize.top + frameSize.bottom + border.top + border.bottom + padding.top + padding.bottom);
546                 }
547                 item.setCalculatedSize(undefined, box.height - item.el.getMargin('tb'), owner);
548
549                 // At this point IE will report the left/right-docked toolbar as having a width equal to the
550                 // container's full width. Forcing a repaint kicks it into shape so it reports the correct width.
551                 if (!Ext.supports.ComputedStyle) {
552                     item.el.repaint();
553                 }
554             }
555         }
556         else {
557             item.doComponentLayout();
558             box.width = item.getWidth() - (box.offsets.left + box.offsets.right);
559             box.height = item.getHeight() - (box.offsets.bottom + box.offsets.top);
560             box.y += box.offsets.top;
561             if (horizontal) {
562                 box.x = (item.align == 'right') ? bodyBox.width - box.width : bodyBox.x;
563                 box.x += box.offsets.left;
564             }
565         }
566
567         // If we haven't calculated the width or height of the docked item yet
568         // do so, since we need this for our upcoming calculations
569         if (box.width == undefined) {
570             box.width = item.getWidth() + item.el.getMargin('lr');
571         }
572         if (box.height == undefined) {
573             box.height = item.getHeight() + item.el.getMargin('tb');
574         }
575
576         return box;
577     },
578
579     /**
580      * @protected
581      * Returns an array containing all the <b>visible</b> docked items inside this layout's owner Panel
582      * @return {Array} An array containing all the <b>visible</b> docked items of the Panel
583      */
584     getLayoutItems : function() {
585         var it = this.owner.getDockedItems(),
586             ln = it.length,
587             i = 0,
588             result = [];
589         for (; i < ln; i++) {
590             if (it[i].isVisible(true)) {
591                 result.push(it[i]);
592             }
593         }
594         return result;
595     },
596
597     /**
598      * @protected
599      * Render the top and left docked items before any existing DOM nodes in our render target,
600      * and then render the right and bottom docked items after. This is important, for such things
601      * as tab stops and ARIA readers, that the DOM nodes are in a meaningful order.
602      * Our collection of docked items will already be ordered via Panel.getDockedItems().
603      */
604     renderItems: function(items, target) {
605         var cns = target.dom.childNodes,
606             cnsLn = cns.length,
607             ln = items.length,
608             domLn = 0,
609             i, j, cn, item;
610
611         // Calculate the number of DOM nodes in our target that are not our docked items
612         for (i = 0; i < cnsLn; i++) {
613             cn = Ext.get(cns[i]);
614             for (j = 0; j < ln; j++) {
615                 item = items[j];
616                 if (item.rendered && (cn.id == item.el.id || cn.down('#' + item.el.id))) {
617                     break;
618                 }
619             }
620
621             if (j === ln) {
622                 domLn++;
623             }
624         }
625
626         // Now we go through our docked items and render/move them
627         for (i = 0, j = 0; i < ln; i++, j++) {
628             item = items[i];
629
630             // If we're now at the right/bottom docked item, we jump ahead in our
631             // DOM position, just past the existing DOM nodes.
632             //
633             // TODO: This is affected if users provide custom weight values to their
634             // docked items, which puts it out of (t,l,r,b) order. Avoiding a second
635             // sort operation here, for now, in the name of performance. getDockedItems()
636             // needs the sort operation not just for this layout-time rendering, but
637             // also for getRefItems() to return a logical ordering (FocusManager, CQ, et al).
638             if (i === j && (item.dock === 'right' || item.dock === 'bottom')) {
639                 j += domLn;
640             }
641
642             // Same logic as Layout.renderItems()
643             if (item && !item.rendered) {
644                 this.renderItem(item, target, j);
645             }
646             else if (!this.isValidParent(item, target, j)) {
647                 this.moveItem(item, target, j);
648             }
649         }
650     },
651
652     /**
653      * @protected
654      * This function will be called by the dockItems method. Since the body is positioned absolute,
655      * we need to give it dimensions and a position so that it is in the middle surrounded by
656      * docked items
657      * @param {Object} box An object containing new x, y, width and height values for the
658      * Panel's body
659      */
660     setBodyBox : function(box) {
661         var me = this,
662             owner = me.owner,
663             body = owner.body,
664             info = me.info,
665             bodyMargin = info.bodyMargin,
666             padding = info.padding,
667             border = info.border,
668             frameSize = me.frameSize;
669         
670         // Panel collapse effectively hides the Panel's body, so this is a no-op.
671         if (owner.collapsed) {
672             return;
673         }
674         
675         if (Ext.isNumber(box.width)) {
676             box.width -= bodyMargin.left + bodyMargin.right;
677         }
678         
679         if (Ext.isNumber(box.height)) {
680             box.height -= bodyMargin.top + bodyMargin.bottom;
681         }
682         
683         me.setElementSize(body, box.width, box.height);
684         if (Ext.isNumber(box.x)) {
685             body.setLeft(box.x - padding.left - frameSize.left);
686         }
687         if (Ext.isNumber(box.y)) {
688             body.setTop(box.y - padding.top - frameSize.top);
689         }
690     },
691
692     /**
693      * @protected
694      * We are overriding the Ext.layout.Layout configureItem method to also add a class that
695      * indicates the position of the docked item. We use the itemCls (x-docked) as a prefix.
696      * An example of a class added to a dock: right item is x-docked-right
697      * @param {Ext.Component} item The item we are configuring
698      */
699     configureItem : function(item, pos) {
700         this.callParent(arguments);
701         
702         item.addCls(Ext.baseCSSPrefix + 'docked');
703         item.addClsWithUI('docked-' + item.dock);
704     },
705
706     afterRemove : function(item) {
707         this.callParent(arguments);
708         if (this.itemCls) {
709             item.el.removeCls(this.itemCls + '-' + item.dock);
710         }
711         var dom = item.el.dom;
712
713         if (!item.destroying && dom) {
714             dom.parentNode.removeChild(dom);
715         }
716         this.childrenChanged = true;
717     }
718 });