Hybrid View
-
4 Nov 2007 11:19 AM #1
Ext.ux.layout.RowFitLayout
Ext.ux.layout.RowFitLayout
I wanted to have a layout that distributes multiple components so they fill 100% of the container height. For sure, I can do that with AnchorLayout or TableLayout, but it seems some effort is required to manage these when there are a bunch of child elements.
My idea was that child elements can have height given in percent, and that value should be relative to the "unallocated" height (which is the difference between container height and the sum of the heights of all elements that are set in pixels).
Analogous layout in Ext is ColumnLayout, where you can give width or columnWidth and layout will manage that automatically.Code:+-------------------------------------------------------------------+ | { height: 100 } | | panel with a fixed height of 100px (height will remain the same | | when container size container size changes) | +-------------------------------------------------------------------+ | | | { height: "50%" } | | Will fill half of the remaining space | | (container.height - 100) / 2 | +-------------------------------------------------------------------+ | | | ( no height in config) | | Will fill all the space that is unallocated (50% in that case) | | | +-------------------------------------------------------------------+
I had some hours to spare and decided, "Why don't make that?"
Here's what I came up to:
There's also an SplitBar adapter as a bonus.PHP Code:Ext.namespace('Ext.ux.layout');
/**
* @class Ext.ux.layout.RowFitLayout
* @extends Ext.layout.ContainerLayout
* <p>Layout that distributes heights of elements so they take 100% of the
* container height.</p>
* <p>Height of the child element can be given in pixels (as an integer) or
* in percent. All elements with absolute height (i.e. in pixels) always will
* have the given height. All "free" space (that is not filled with elements
* with 'absolute' height) will be distributed among other elements in
* proportion of their height percentage. Elements without 'height' in the
* config will take equal portions of the "unallocated" height.</p>
* <p>Supports panel collapsing, hiding, removal/addition. The adapter is provided
* to use with Ext.SplitBar: <b>Ext.ux.layout.RowFitLayout.SplitAdapter</b>.</p>
* <p>Example usage:</p>
* <pre><code>
var vp = new Ext.Viewport({
layout: 'row-fit',
items: [
{ xtype: 'panel', height: 100, title: 'Height in pixels', html: 'panel height = 100px' },
{ xtype: 'panel', height: "50%", title: '1/2', html: 'Will take half of remaining height' },
{ xtype: 'panel', title: 'No height 1', html: 'Panel without given height' },
{ xtype: 'panel', title: 'No height 2', html: 'Another panel' }
]
});
* </code></pre>
* Usage of the split bar adapter:
* <pre><code>
var split = new Ext.SplitBar("elementToDrag", "elementToSize", Ext.SplitBar.VERTICAL, Ext.SplitBar.TOP);
// note the Ext.SplitBar object is passed to the adapter constructor to set
// correct minSize and maxSize:
split.setAdapter(new Ext.ux.layout.RowFitLayout.SplitAdapter(split));
* </code></pre>
*/
Ext.ux.layout.RowFitLayout = Ext.extend(Ext.layout.ContainerLayout, {
// private
monitorResize: true,
// private
trackChildEvents: ['collapse', 'expand', 'hide', 'show'],
// private
renderAll: function(ct, target) {
Ext.ux.layout.RowFitLayout.superclass.renderAll.apply(this, arguments);
// add event listeners on addition/removal of children
ct.on('add', this.containerListener);
ct.on('remove', this.containerListener);
},
// private
renderItem: function(c, position, target) {
Ext.ux.layout.RowFitLayout.superclass.renderItem.apply(this, arguments);
// add event listeners
for (var i=0, n = this.trackChildEvents.length; i < n; i++) {
c.on(this.trackChildEvents[i], this.itemListener);
}
c.animCollapse = false; // looks ugly together with row-fit layout
// store some layout-specific calculations
c.rowFit = {
hasAbsHeight: false, // whether the component has absolute height (in pixels)
relHeight: 0, // relative height, in pixels (if applicable)
calcRelHeight: 0, // calculated relative height (used when element is resized)
calcAbsHeight: 0 // calculated absolute height
};
// process height config option
if (c.height) {
// store relative (given in percent) height
if (typeof c.height == "string" && c.height.indexOf("%")) {
c.rowFit.relHeight = parseInt(c.height);
}
else { // set absolute height
c.setHeight(c.height);
c.rowFit.hasAbsHeight = true;
}
}
},
// private
onLayout: function(ct, target) {
Ext.ux.layout.RowFitLayout.superclass.onLayout.call(this, ct, target);
if (this.container.collapsed || !ct.items || !ct.items.length) { return; }
// first loop: determine how many elements with relative height are there,
// sums of absolute and relative heights etc.
var absHeightSum = 0, // sum of elements' absolute heights
relHeightSum = 0, // sum of all percent heights given in children configs
relHeightRatio = 1, // "scale" ratio used in case sum <> 100%
relHeightElements = [], // array of elements with 'relative' height for the second loop
noHeightCount = 0; // number of elements with no height given
for (var i=0, n = ct.items.length; i < n; i++) {
var c = ct.items.itemAt(i);
if (!c.isVisible()) { continue; }
// collapsed panel is treated as an element with absolute height
if (c.collapsed) { absHeightSum += c.getFrameHeight(); }
// element that has an absolute height
else if (c.rowFit.hasAbsHeight) {
absHeightSum += c.height;
}
// 'relative-heighted'
else {
if (!c.rowFit.relHeight) { noHeightCount++; } // element with no height given
else { relHeightSum += c.rowFit.relHeight; }
relHeightElements.push(c);
}
}
// if sum of relative heights <> 100% (e.g. error in config or consequence
// of collapsing/removing panels), scale 'em so it becomes 100%
if (noHeightCount == 0 && relHeightSum != 100) {
relHeightRatio = 100 / relHeightSum;
}
var freeHeight = target.getStyleSize().height - absHeightSum, // "unallocated" height we have
absHeightLeft = freeHeight; // track how much free space we have
while (relHeightElements.length) {
var c = relHeightElements.shift(), // element we're working with
relH = c.rowFit.relHeight * relHeightRatio, // height of this element in percent
absH = 0; // height in pixels
// no height in config
if (!relH) {
relH = (100 - relHeightSum) / noHeightCount;
}
// last element takes all remaining space
if (!relHeightElements.length) { absH = absHeightLeft; }
else { absH = Math.round(freeHeight * relH / 100); }
// anyway, height can't be negative
if (absH < 0) { absH = 0; }
c.rowFit.calcAbsHeight = absH;
c.rowFit.calcRelHeight = relH;
c.setHeight(absH);
absHeightLeft -= absH;
}
},
/**
* Event listener for container's children
* @private
*/
itemListener: function(item) {
item.ownerCt.doLayout();
},
/**
* Event listener for the container (on add, remove)
* @private
*/
containerListener: function(ct) {
ct.doLayout();
}
});
// Split adapter
if (Ext.SplitBar.BasicLayoutAdapter) {
/**
* @param {Ext.SplitBar} splitbar to which adapter is applied.
* If supplied, will set correct minSize and maxSize.
*/
Ext.ux.layout.RowFitLayout.SplitAdapter = function(splitbar) {
if (splitbar && splitbar.el.dom.nextSibling) {
var next = Ext.getCmp( splitbar.el.dom.nextSibling.id ),
resized = Ext.getCmp(splitbar.resizingEl.id);
if (next) {
splitbar.maxSize = (resized.height || resized.rowFit.calcAbsHeight) +
next.getInnerHeight() - 1; // seems can't set height=0 in IE, "1" works fine
}
splitbar.minSize = resized.getFrameHeight() + 1;
}
}
Ext.extend(Ext.ux.layout.RowFitLayout.SplitAdapter, Ext.SplitBar.BasicLayoutAdapter, {
setElementSize: function(splitbar, newSize, onComplete) {
var resized = Ext.getCmp(splitbar.resizingEl.id);
// can't resize absent, collapsed or hidden panel
if (!resized || resized.collapsed || !resized.isVisible()) return;
// resizingEl has absolute height: just change it
if (resized.rowFit.hasAbsHeight) {
resized.setHeight(newSize);
}
// resizingEl has relative height: affects next sibling
else {
if (splitbar.el.dom.nextSibling) {
var nextSibling = Ext.getCmp( splitbar.el.dom.nextSibling.id ),
deltaAbsHeight = newSize - resized.rowFit.calcAbsHeight, // pixels
nsRf = nextSibling.rowFit, // shortcut
rzRf = resized.rowFit,
// pixels in a percent
pctPxRatio = rzRf.calcRelHeight / rzRf.calcAbsHeight,
deltaRelHeight = pctPxRatio * deltaAbsHeight; // change in height in percent
rzRf.relHeight = rzRf.calcRelHeight + deltaRelHeight;
if (nsRf.hasAbsHeight) {
var newHeight = nextSibling.height - deltaAbsHeight;
nextSibling.height = newHeight;
nextSibling.setHeight(newHeight);
}
else {
nsRf.relHeight = nsRf.calcRelHeight - deltaRelHeight;
}
}
}
// recalculate heights
resized.ownerCt.doLayout();
} // of setElementSize
}); // of SplitAdapter
}
Ext.Container.LAYOUTS['row-fit'] = Ext.ux.layout.RowFitLayout;
I haven't tested that a lot, was in hurry to share my excitement about Ext 2.0
(Actually the free time ran out...)
There are some known issues and limitations, e.g. collapsed panels are considered elements with absolute height regardless their initial settings, and that can produce somewhat strange visual effect when there are a lot of collapsed panels.
Hope that will help someone, but even if I had just re-invented the wheel, that exercise helped me to dive into the depths of the Ext. Gee-whiz! The whole Ext 2.0 layout system is so-o-o damned cool!!! No doubt Jack and his team are geniuses!
Here you can see the layout in action.
-
12 Nov 2007 2:27 PM #2
Hey, i don't know why nobody has responded to this.. but.. this is great !
I'm not so experienced in Ext so for me this extension is the perfect solution to all my problems !
Thanks a lot, i will test it very soon !
-
13 Nov 2007 6:14 AM #3
-
15 Nov 2007 5:12 AM #4
this is exactly what i have searched for.
it work very well the only limitation is the height value only except px ;(
-
16 Nov 2007 6:39 AM #5
-
29 Nov 2007 6:21 AM #6
I have to thank you very mach
Your code of row-fit just solve my problem in 3 minutes
-
27 Dec 2007 12:54 AM #7
Thanks for sharing your work, I've tried to improve a bit on it and extended it to automatically add the needed sliders and splitbars. Just add a property 'split' to the items to be resizable.
Here we go:
still missing: better handling of add- and remove-events on the container to add/remove sliders and splitbars as well.Code:Ext.namespace('Ext.ux.layout'); /** * @class Ext.ux.layout.RowFitLayout * @extends Ext.layout.ContainerLayout * <p>Layout that distributes heights of elements so they take 100% of the * container height.</p> * <p>Height of the child element can be given in pixels (as an integer) or * in percent. All elements with absolute height (i.e. in pixels) always will * have the given height. All "free" space (that is not filled with elements * with 'absolute' height) will be distributed among other elements in * proportion of their height percentage. Elements without 'height' in the * config will take equal portions of the "unallocated" height.</p> * <p>Supports panel collapsing, hiding, removal/addition. The adapter is provided * to use with Ext.SplitBar: <b>Ext.ux.layout.RowFitLayout.SplitAdapter</b>.</p> * <p>Example usage:</p> * <pre><code> var vp = new Ext.Viewport({ layout: 'row-fit', items: [ { xtype: 'panel', height: 100, title: 'Height in pixels', html: 'panel height = 100px' }, { xtype: 'panel', height: "50%", title: '1/2', html: 'Will take half of remaining height' }, { xtype: 'panel', title: 'No height 1', html: 'Panel without given height', id: '' }, { xtype: 'panel', title: 'No height 2', html: 'Another panel' } ] }); * </code></pre> * Usage of the split bar adapter: * <pre><code> var split = new Ext.SplitBar("elementToDrag", "elementToSize", Ext.SplitBar.VERTICAL, Ext.SplitBar.TOP); // note the Ext.SplitBar object is passed to the adapter constructor to set // correct minSize and maxSize: split.setAdapter(new Ext.ux.layout.RowFitLayout.SplitAdapter(split)); * </code></pre> */ Ext.ux.layout.RowFitLayout = Ext.extend(Ext.layout.ContainerLayout, { // private monitorResize: true, // private trackChildEvents: ['collapse', 'expand', 'hide', 'show'], // private splitHeight: 5, rendered: false, // private renderItem: function(c, position, target) { Ext.ux.layout.RowFitLayout.superclass.renderItem.apply(this, arguments); // add event listeners for (var i=0, n = this.trackChildEvents.length; i < n; i++) { var ev = this.trackChildEvents[i]; //c.on(this.trackChildEvents[i], this.itemListener, this); c.on(ev, this['_item_' + ev], this); } c.animCollapse = false; // looks ugly together with row-fit layout this.checkRelHeight(c); }, checkRelHeight: function(c) { // store some layout-specific calculations if(!c.rowFit) c.rowFit = { hasAbsHeight: false, // whether the component has absolute height (in pixels) relHeight: 0, // relative height, in pixels (if applicable) calcRelHeight: 0, // calculated relative height (used when element is resized) calcAbsHeight: 0, // calculated absolute height height: c.height // save height config }; // process height config option if (c.height) { var height = c.rowFit.height || c.height; // store relative (given in percent) height if (typeof height == "string" && height.indexOf("%")) { c.rowFit.relHeight = parseInt(height); } else { // set absolute height c.setHeight(c.height); //c.rowFit.hasAbsHeight = true; c.rowFit.hasAbsHeight = !Boolean(c.split); } } //if(c.split) c.rowFit.hasAbsHeight = false; c.isResizable = c.isResizable || Boolean(c.split) || !c.rowFit.hasAbsHeight; }, // private onLayout: function(ct, target) { Ext.ux.layout.RowFitLayout.superclass.onLayout.call(this, ct, target); if (this.container.collapsed || !ct.items || !ct.items.length) { return; } // first loop: determine how many elements with relative height are there, // sums of absolute and relative heights etc. var absHeightSum = 0, // sum of elements' absolute heights relHeightSum = 0, // sum of all percent heights given in children configs relHeightRatio = 1, // "scale" ratio used in case sum <> 100% noHeightCount = 0, // number of elements with no height given relHeightElements = []; // array of elements with 'relative' height for the second loop for (var i=0, n = ct.items.length; i < n; i++) { var c = ct.items.itemAt(i); if (!c.isVisible()) { continue; } // collapsed panel is treated as an element with absolute height if (c.collapsed) { absHeightSum += c.getFrameHeight(); } else if (c.rowFit.hasAbsHeight) { // element that has an absolute height absHeightSum += c.height; } else { // 'relative-heighted' if (!c.rowFit.relHeight) { // element with no height given noHeightCount++; } else { relHeightSum += c.rowFit.relHeight; } relHeightElements.push(c); } } // if sum of relative heights <> 100% (e.g. error in config or consequence // of collapsing/removing panels), scale 'em so it becomes 100% if (noHeightCount == 0 && relHeightSum != 100) { relHeightRatio = 100 / relHeightSum; } var freeHeight = target.getStyleSize().height - absHeightSum, // "unallocated" height we have absHeightLeft = freeHeight; // track how much free space we have while (relHeightElements.length) { var c = relHeightElements.shift(), // element we're working with relH = c.rowFit.relHeight * relHeightRatio, // height of this element in percent absH = 0; // height in pixels // no height in config if (!relH) { relH = (100 - relHeightSum) / noHeightCount; } // last element takes all remaining space if (!relHeightElements.length) { absH = absHeightLeft; } else { absH = Math.round(freeHeight * relH / 100); } // anyway, height can't be negative if (absH < 0) { absH = 0; } c.rowFit.calcAbsHeight = absH; c.rowFit.calcRelHeight = relH; c.setHeight(absH); absHeightLeft -= absH; } for (var i=0, n = ct.items.length; i < n; i++) { var c = ct.items.itemAt(i); if(c.isSlider && c.el2resize) { // this.checkRelHeight(c); var split = new Ext.SplitBar(c.el, c.el2resize.el, Ext.SplitBar.VERTICAL, Ext.SplitBar.TOP); split.setAdapter(new Ext.ux.layout.RowFitLayout.SplitAdapter(split)); c.el2resize.sliderId = c.getId(); c.el2resize = false; } } if(!this.rendered) { // keep watching for changes of items ct.on('add', this.ctAddItem, this); ct.on('remove', this.ctDelItem, this); this.rendered = true; } }, // private - called from Ext.Container setContainer : function(ct){ Ext.ux.layout.RowFitLayout.superclass.setContainer.call(this, ct); this._addSliders(ct); }, // private _addSliders: function(ct) { var sh = ct.splitHeight || this.splitHeight; var skip1 = true; var n = ct.items.length; for(var i = n-1; i >= 0; i--) { var c = ct.items.itemAt(i); this.checkRelHeight(c); if(c.isResizable) { // !c.rowFit.hasAbsHeight) { if(skip1) { skip1 = false; continue; } if(c.split) { var slider = new Ext.Panel({height:sh, isSlider: true}); slider.el2resize = c; slider.addClass('x-splitbar-y'); ct.insert(i+1, slider); } } } }, /** * Add event listener for container children * @private */ itemListener: function(item) { item.ownerCt.doLayout(); }, _item_show: function(comp) { if(!comp.isSlider && comp.sliderId) { var sl = comp.ownerCt.findById(comp.sliderId); if(!sl.isVisible()) { sl.show(); return; } } comp.ownerCt.doLayout(); }, _item_hide: function(comp) { if(!comp.isSlider && comp.sliderId) { var sl = comp.ownerCt.findById(comp.sliderId); if(sl.isVisible()) { sl.hide(); return; } } comp.ownerCt.doLayout(); }, _item_expand: function(comp) { this._item_show(comp); }, _item_collapse: function(comp) { this._item_hide(comp); }, /** * Event listener for the container (on add, remove) * @private */ ctAddItem: function(ct, comp, idx) { // TODO: ev. add slider & splitbar ct.doLayout(); }, ctDelItem: function(ct, comp) { // TODO: ev. remove slider & splitbar ct.doLayout(); } }); // Split adapter if (Ext.SplitBar.BasicLayoutAdapter) { /** * @param {Ext.SplitBar} splitbar to which adapter is applied. * If supplied, will set correct minSize and maxSize. */ Ext.ux.layout.RowFitLayout.SplitAdapter = function(splitbar) { if (splitbar && splitbar.el.dom.nextSibling) { var next = Ext.getCmp( splitbar.el.dom.nextSibling.id ), resized = Ext.getCmp(splitbar.resizingEl.id); // skip abs-height non-resizable components while(next && (next.collapsed || !next.isVisible() || !next.isResizable)) { next = Ext.getCmp(next.el.dom.nextSibling.id); } if (next) { //splitbar.maxSize = (resized.height || resized.rowFit.calcAbsHeight) + splitbar.maxSize = (resized.rowFit.hasAbsHeight ? resized.rowFit.calcAbsHeight : resized.getSize().height) + next.getInnerHeight() - 1; // seems can't set height=0 in IE, "1" works fine } splitbar.minSize = resized.getFrameHeight() + 1; } } Ext.extend(Ext.ux.layout.RowFitLayout.SplitAdapter, Ext.SplitBar.BasicLayoutAdapter, { setElementSize: function(splitbar, newSize, onComplete) { var resized = Ext.getCmp(splitbar.resizingEl.id); // can't resize absent, collapsed or hidden panel if (!resized || resized.collapsed || !resized.isVisible()) return; // resizingEl has absolute height: just change it if (resized.rowFit.hasAbsHeight) { resized.setHeight(newSize); } // resizingEl has relative height: affects next sibling else { if (splitbar.el.dom.nextSibling) { var nextSibling = Ext.getCmp( splitbar.el.dom.nextSibling.id ); // skip abs-height non-resizable components while(nextSibling && (nextSibling.collapsed || !nextSibling.isVisible() || !nextSibling.isResizable)) { nextSibling = Ext.getCmp(nextSibling.el.dom.nextSibling.id); } var deltaAbsHeight = newSize - resized.rowFit.calcAbsHeight, // pixels nsRf = nextSibling.rowFit, // shortcut rzRf = resized.rowFit, // pixels in a percent pctPxRatio = rzRf.calcRelHeight / rzRf.calcAbsHeight, deltaRelHeight = pctPxRatio * deltaAbsHeight; // change in height in percent rzRf.relHeight = rzRf.calcRelHeight + deltaRelHeight; if (nsRf.hasAbsHeight) { var newHeight = nextSibling.height - deltaAbsHeight; nextSibling.height = newHeight; nextSibling.setHeight(newHeight); } else { nsRf.relHeight = nsRf.calcRelHeight - deltaRelHeight; } } } // recalculate heights resized.ownerCt.doLayout(); } // of setElementSize }); // of SplitAdapter } Ext.Container.LAYOUTS['row-fit'] = Ext.ux.layout.RowFitLayout;
Now if only someone extended the ColumnLayout to have resizable columns ;-)
Comments?
-
27 Dec 2007 6:03 AM #8
I just found a little problem, probably also with the original version and not only with my modified one:
internet explorer doesn't scroll the content of the items in the container if too large, while firefox and opera are doing just fine here. I'm getting a rendering error in a different place with those 2 browsers, but that's another story ...
Does anybody have an idea what's going wrong here?
-
5 Jan 2008 8:37 AM #9
Ext.ViewPort
Ext.ViewPort
Hi,
I need to add a Ext.ViewPort to an existing jsp, basically render the viewport to a div which resides in the existing jsp page.
I found the Ext.viewport(with all its panels within) always rendered starting at leftmost and topmost point of the browser(FireFox specifically).Is it correct to try to render a Ext.ViewPort into portion of JSP? For the viewport part seems to take over the whole screen.
Thanks a lot,
-
5 Jan 2008 3:16 PM #10
You could've posted in help, but anyways to answer, no ViewPort is descendant of document.body, it can not reside in a div. For this you'll need to use a panel.
ps - Isn't JSP irrelevant to the browser/discussion?


Reply With Quote

