ry.extjs
19 Apr 2009, 12:41 PM
UPDATE 4/28/09: Re-factored code. Fully commented code. Added license. Added all remaining TO-DO items. The only thing left to do is extensive testing. Also, updated the demo to show a little bit more advanced example. If you find a bug, please let me know.
UPDATE 4/20/09: I completely re-wrote the code that calculates the container item widths. I removed the flex-based calculations in favor of pixel widths after the layout is rendered once. If the layout width changes, I simply re-calculate widths based on current values and the old layout width. I added the ability to set a static width to container items. You may specify as many widths as you'd like, but there currently must be at least 1 flex value. Also, all of the specified flex values must add up to 1.0, or the layout will not render. This will change in a future release where flex values will be automatically calculated, if not specified. The demo has been updated to reflect these changes.
DEMO: http://www.ann0yanc3.com/Ext.layout.HBoxFitSplit
This layout combines elements of the HBox layout with SplitBar components. Simply add layout: 'hboxfitsplit' to your container, and the layout will organize the items horizontally and add a SplitBar between each item. Currently, item widths are determined by the flex value of container items, or the width. If you mix flex and width values, the width of each flex item is calculated with the remaining available width of the container (after static widths are added). Each item of this layout can also have a minWidth and maxWidth value, representing the minimum width and maximum width of the item, respectively.
The layoutconfig options for this layout are itemMinWidth and splitWidth. The first being the minimum width for each container item, and the latter being the width of each SplitBar. itemMinWidth defaults to 100, and is overriden by the minWidth value of container items.
This layout has been tested in FF3, Safari 3.2.2, and Google Chrome 1.0.154.53. IE8 RC1 seems to be working as well, currently don't have IE8 Final, or IE7 to test.
In the demo, I use a custom graphic for the splitbar, and no borders for the container items.
Ext.layout.HBoxFitSplitLayout.js
/**
* @class Ext.layout.HBoxSplitLayout
* @extends Ext.layout.BoxLayout
* @version 0.3
* @license LGPLv3 - http://www.gnu.org/licenses/lgpl.html
* @author Ry Racherbaumer
*
* A layout that arranges items horizontally with a split bar between each item
*/
Ext.layout.HBoxFitSplitLayout = Ext.extend(Ext.layout.BoxLayout, {
/**
* @cfg {Number} flex
* This configuation option is to be applied to <b>child <tt>items</tt></b> of the container managed
* by this layout. Each child item with a <tt>flex</tt> property will be flexed <b>horizontally</b>
* according to each item's <b>relative</b> <tt>flex</tt> value compared to the sum of all items with
* a <tt>flex</tt> value specified. Any child items that have either a <tt>flex = 0</tt> or
* <tt>flex = undefined</tt>, or no width value, will be assigned a flex value based on the remaining
* width of the container.
*/
/**
* @cfg {Number} minWidth
* This configuation option is to be applied to <b>child <tt>items</tt></b> of the container managed
* by this layout. This is the minimum width that the child element can be. This setting will override the
* itemMinWidth option for this layout.
*/
/**
* @cfg {Number} maxWidth
* This configuation option is to be applied to <b>child <tt>items</tt></b> of the container managed
* by this layout. This is the maximum width that the child element can be.
*/
/**
* @cfg {Number} itemMinWidth
* This is the minimum width in pixels that child items can be. Defaults to 100. This setting is overridden
* by the minWidth option on component items.
*/
itemMinWidth: 100,
/**
* @cfg {Number} splitBarWidth
* This is the width in pixels of each SplitBar. Defaults to 5.
*/
splitBarWidth: 5,
// private
splitsRendered: false,
rendered: false,
splits: [],
// private
onLayout: function(ct, target) {
// setup variables
var i, len, last, num;
var cs, c, i, cm, cw, ch, ct;
// add/render splitbar box components
if (!this.splitsRendered) {
len = ct.items.length,
last = len - 1,
num = 1,
index = 1;
for (i = 0; i < len; ++i) {
if (i < last) {
ct.insert(index, {
xtype: 'box',
id: ct.id + '-splitbar' + num,
width: this.splitBarWidth,
isSplitBar: true
}); ++num;
index += 2;
}
}
// render the new items
Ext.layout.HBoxFitSplitLayout.superclass.onLayout.call(this, ct, target);
// add splitbars
ct.items.eachKey(function(key, item, index, count) {
if (key.indexOf('split') !== -1) {
this.splits[this.splits.length] = new Ext.SplitBar(Ext.get(item.id), Ext.getCmp(ct.items.items[index - 1].id).getEl(), Ext.SplitBar.HORIZONTAL, Ext.SplitBar.LEFT);
this.splits[this.splits.length - 1].minSize = Ext.getCmp(ct.items.items[index - 1].id).minWidth || this.itemMinWidth;
this.splits[this.splits.length - 1].on('beforeapply',
function(c, newSize) {
this.fireEvent('moved', this, newSize);
// don't resize elements automatically
return false;
});
this.splits[this.splits.length - 1].on('moved', this.onSplitMove, this);
this.splits[this.splits.length - 1].on('beforeresize', this.onBeforeResize, this);
}
},
this);
// splits have been rendered
this.splitsRendered = true;
// if splits are rendered, just call parent onLayout
} else Ext.layout.HBoxFitSplitLayout.superclass.onLayout.call(this, ct, target);
// get layout components
cs = ct.items.items,
len = ct.items.items.length,
last = len - 1;
// get the size of the layout
var layoutSize = this.getTargetSize(target);
// get width, height, and left pos of layout
var layoutWidth = layoutSize.width - target.getPadding('lr') - this.scrollOffset,
layoutHeight = layoutSize.height - target.getPadding('tb'),
layoutLeft = this.padding.left,
layoutTop = this.padding.top;
// make sure width/height is greater than 0
if ((Ext.isIE && !Ext.isStrict) && (layoutWidth < 1 || layoutHeight < 1)) {
return;
} else if (layoutWidth < 1 && layoutHeight < 1) {
return;
}
// get width of all splitbars
var splitBarsWidth = this.splitBarWidth * this.splits.length;
// get minimum allowable layout width, based on component minWidth, or itemMinWidth
var minLayoutWidth = splitBarsWidth;
for (i = 0; i < len; i++) {
c = cs[i];
if (!c.isSplitBar) {
if (c.minWidth && c.minWidth > 0) minLayoutWidth += c.minWidth;
else minLayoutWidth += this.itemMinWidth;
}
}
// if layout is less than min allowable width, set to min allowable width
if (layoutWidth < minLayoutWidth) {
// set layout container to minimum allowed width
// factor in extra container width (i.e. borders)
ct.setWidth(minLayoutWidth + (ct.getWidth() - layoutWidth));
// setting the container width will call onLayout again
return;
}
// set width/height of inner container to match layout
this.innerCt.setSize(layoutWidth, layoutHeight);
// get stretch height
var stretchHeight = layoutHeight - (this.padding.top + this.padding.bottom);
// get available width for layout items
var availableWidth = layoutWidth - splitBarsWidth;
// if layout has never been rendered, make sure flex values are setup properly
if (!this.rendered) {
// total flex value
var totalFlex = 0;
// make sure total flex value is equal to 1
for (i = 0; i < len; i++) {
c = cs[i];
if (!c.isSplitBar && c.flex && c.flex > 0) totalFlex += c.flex;
}
// total flex cannot be greater than 1
if (totalFlex > 1) return;
// if total flex value is less than 1, set flex value for components without flex and width
else if (totalFlex < 1) {
var availFlex = 1 - totalFlex;
num = 0;
// count number of items without width and flex values
for (i = 0; i < len; i++) {
c = cs[i];
if (!c.isSplitBar && !c.flex && !c.width)++num;
}
// set flex values for items without width and flex values
for (i = 0; i < len; i++) {
c = cs[i];
if (!c.isSplitBar && !c.flex && !c.width) c.flex = availFlex / num;
}
}
var availableFlexWidth = availableWidth,
allocatedFlexWidth = 0;
// if component width is specified, remove flex value if specified
// subtract component width from available flex width
for (i = 0; i < len; i++) {
c = cs[i];
if (!c.isSplitBar && c.width && c.width > 0) {
// subtract component width from available width (for flex calculations)
availableFlexWidth -= c.width;
// remove flex value if it's specified
if (c.flex || c.flex > 0) delete c.flex;
// remove width value from future checks
delete c.width;
}
}
// calculate component widths for components with flex value
for (i = 0; i < len; i++) {
// get component
c = cs[i];
// get component margins
cm = c.margins;
// if component is not a splitbar
if (!c.isSplitBar) {
// calculate width of flex value
if (c.flex && c.flex > 0) {
cw = Math.round(availableFlexWidth * c.flex);
// remove flex value
delete c.flex;
// increase allocated width
allocatedFlexWidth += cw;
// if allocated width is not equal to available width for last item, subtract diff
if (i === last && (allocatedFlexWidth - availableFlexWidth !== 0)) cw -= (allocatedFlexWidth - availableFlexWidth);
// account for component margins
cw -= (cm.left + cm.right);
// set component width
c.setWidth(cw);
}
}
}
this.rendered = true;
} else {
// see if layout width has changed
var totalComponentWidth = 0;
for (i = 0; i < len; i++) {
c = cs[i];
cm = c.margins;
cw = c.getWidth();
cw += cm.left + cm.right;
if (!c.isSplitBar) totalComponentWidth += cw;
}
if (availableWidth !== totalComponentWidth) {
var allocatedWidth = 0,
nw;
// re-calculate component widths based on new layout width
for (i = 0; i < len; i++) {
c = cs[i];
cw = c.getWidth();
cm = c.margins;
cw += cm.left + cm.right;
if (!c.isSplitBar) {
nw = Math.round(cw / totalComponentWidth * availableWidth);
allocatedWidth += nw;
// if allocated width is not equal to available width for last item, subtract diff
if (i === last && (allocatedWidth - availableWidth !== 0)) nw -= allocatedWidth - availableWidth;
c.setWidth(nw - cm.left - cm.right);
}
}
}
}
// make sure component widths don't fall below min/max widths
// if they do, set their widths to min/max values
var adjustedComponents = [];
var newAvailableWidth = availableWidth;
for (i = 0; i < len; i++) {
c = cs[i];
if (!c.isSplitBar) {
cm = c.margins;
cw = c.getWidth();
// add left/right margins to component width
cw += cm.left + cm.right;
// set min/max width, accounting for margins
if ((c.minWidth && c.minWidth > 0 && cw <= c.minWidth) || cw <= this.itemMinWidth) {
if (c.minWidth && c.minWidth > 0) {
c.setWidth(c.minWidth - (cm.left + cm.right));
newAvailableWidth -= c.minWidth;
} else {
c.setWidth(this.itemMinWidth - (cm.left + cm.right))
newAvailableWidth -= this.itemMinWidth;
}
adjustedComponents[adjustedComponents.length] = i;
}
if (c.maxWidth && c.maxWidth > 0 && cw >= c.maxWidth) {
c.setWidth(c.maxWidth - (cm.left + cm.right));
newAvailableWidth -= c.maxWidth;
adjustedComponents[adjustedComponents.length] = i;
}
}
}
// if component widths were adjusted, re-calculate other item widths
if (adjustedComponents.length > 0) {
num = 0;
var allocatedWidth = 0,
oldAvailableWidth = 0,
minWidth = 0;
// get current width of all items that didn't fall below min/max widths
for (i = 0; i < len; i++) {
c = cs[i];
cw = c.getWidth();
cm = c.margins;
// add left/right margins to component width
cw += cm.left + cm.right;
if (!c.isSplitBar && adjustedComponents.indexOf(i) === -1) {
oldAvailableWidth += cw;
minWidth += (c.minWidth && c.minWidth > 0) ? c.minWidth: this.itemMinWidth;
// increase item count
++num;
}
}
var nw;
// set new widths for all items that didn't fall below min/max widths
for (i = 0; i < len; i++) {
c = cs[i];
// make sure component didn't fall below item min/max widths
if (!c.isSplitBar && adjustedComponents.indexOf(i) === -1) {
cm = c.margins;
nw = Math.round((newAvailableWidth * c.getWidth()) / oldAvailableWidth);
allocatedWidth += nw;
// decrease item count
--num;
// if last item, make sure there's no extra width
if (num === 0 && (allocatedWidth - newAvailableWidth !== 0)) nw -= allocatedWidth - newAvailableWidth;
// set component width, accounting for margins
c.setWidth(nw - (cm.left + cm.right));
}
}
// check if new available width falls below the minimum possible width
// if so, subtract needed width equally from other components that can spare it
if (newAvailableWidth < minWidth) {
var extraWidthComponents = [],
extraWidths = [],
neededWidth = minWidth - newAvailableWidth;
var currentMinWidth = 0;
num = 0;
for (i = 0; i < len; i++) {
c = cs[i];
cw = c.getWidth();
cm = c.margins;
// add left/right margins to component width
cw += cm.left + cm.right;
currentMinWidth = (c.minWidth && c.minWidth > 0) ? c.minWidth: this.itemMinWidth;
if (!c.isSplitBar && cw > currentMinWidth) {
var extraWidth = cw - currentMinWidth;
extraWidthComponents[extraWidthComponents.length] = i;
extraWidths[extraWidths.length] = extraWidth; ++num;
}
}
// get the width to subtract from each component with available width
var subtractWidth = Math.round(neededWidth / extraWidths.length),
subtractedWidth = 0;
// make sure each component has enough width to subtract
for (i = 0; i < len; i++) {
c = cs[i];
cw = c.getWidth();
cm = c.margins;
// add left/right margins to component width
cw += cm.left + cm.right;
if (extraWidthComponents.indexOf(i) !== -1 && extraWidths[extraWidthComponents.indexOf(i)] < subtractWidth) {
// set width of component
c.setWidth(cw - extraWidths[extraWidthComponents.indexOf(i)]);
// subtract width from needed width
neededWidth -= extraWidths[extraWidthComponents.indexOf(i)];
// remove component from width arrays
extraWidthComponents.splice(extraWidthComponents.indexOf(i), 1);
extraWidths.splice(extraWidthComponents.indexOf(i), 1);
// re-calculate subtract width
subtractWidth = Math.round(neededWidth / extraWidths.length); --num;
}
}
// subtract width from components with enough width
var sw;
for (i = 0; i < len; i++) {
c = cs[i];
cw = c.getWidth();
cm = c.margins;
// add left/right margins to component width
cw += cm.left + cm.right;
if (extraWidthComponents.indexOf(i) !== -1) {
// keep track of subtracted width
subtractedWidth += subtractWidth;
sw = subtractWidth; --num;
// if last item to subtract, make sure there's no extra width
if (num === 0 && (subtractedWidth - neededWidth !== 0)) sw -= subtractedWidth - neededWidth;
// set width of component
c.setWidth(cw - sw);
}
}
// loop through un-adjusted components and set their minimum width, if necessary
for (i = 0; i < len; i++) {
c = cs[i];
cw = c.getWidth();
cm = c.margins;
// add left/right margins to component width
cw += cm.left + cm.right;
currentMinWidth = (c.minWidth && c.minWidth > 0) ? c.minWidth: this.itemMinWidth;
if (!c.isSplitBar && adjustedComponents.indexOf(i) === -1 && cw < currentMinWidth) {
// set minimum width of component, factor in margins
c.setWidth(currentMinWidth - cm.left - cm.right);
}
}
}
}
// set component positions within the layout
for (i = 0; i < len; i++) {
// get component
c = cs[i];
// get component margins
cm = c.margins;
// get component width
cw = c.getWidth();
// get component height
ch = c.getHeight();
// current left position within layout
layoutLeft += cm.left;
// component top position
ct = layoutTop + cm.top;
// set position of component
c.setPosition(layoutLeft, ct);
// set component height to layout height
c.setHeight((stretchHeight - (cm.top + cm.bottom)).constrain(c.minHeight || 0, c.maxHeight || 1000000));
// increase layout left position
layoutLeft += cw + cm.right;
}
},
// execute when a splitbar has moved
onSplitMove: function(split, newSize) {
// get split resize component
var c = Ext.getCmp(split.resizingEl.id);
var cm = c.margins;
var target = this.container.getLayoutTarget();
var size = this.getTargetSize(target);
// layout width
var lw = size.width - target.getPadding('lr') - this.scrollOffset;
// get total splitbar width
var sw = this.splitBarWidth * this.splits.length;
// get total width
var w = lw - sw;
// get split adjacent component
var ac = Ext.getCmp(split.el.id).nextSibling();
var acm = ac.margins;
// get sum of resize and adjacent component widths + margins
var tcw = c.getWidth() + cm.left + cm.right + ac.getWidth() + acm.left + acm.right;
// new adjacent component width
var nacw = tcw - newSize;
// set item width
c.setWidth(newSize);
ac.setWidth(nacw);
this.layout();
},
// execute before split bar is moved
onBeforeResize: function(split) {
var c = Ext.getCmp(split.resizingEl.id);
var cm = c.margins;
var sc = Ext.getCmp(split.el.id);
var ac = Ext.getCmp(split.el.id).nextSibling();
var acm = ac.margins;
// get total component width
var tcw = c.getWidth() + ac.getWidth();
// get adjacent component min width
var acMinWidth = ac.minWidth || this.itemMinWidth;
// calculate max size of splitbar
if (c.maxWidth && c.maxWidth > 0 && c.maxWidth <= (tcw - acMinWidth)) {
split.maxSize = c.maxWidth;
} else split.maxSize = tcw - acMinWidth;
}
});
// register layout
Ext.Container.LAYOUTS['hboxfitsplit'] = Ext.layout.HBoxFitSplitLayout;
main.js
Ext.onReady(function() {
// data for grid
var myData = [['3m Co', 71.7], ['Alcoa Inc', 29.01], ['Altria Group Inc', 83.81], ['American Express Company', 52.55], ['American International Group, Inc.', 64.13], ['AT&T Inc.', 31.61], ['Boeing Co.', 75.43], ['Caterpillar Inc.', 67.27], ['Citigroup, Inc.', 49.37], ['E.I. du Pont de Nemours and Company', 40.48], ['Exxon Mobil Corp', 68.1], ['General Electric Company', 34.14], ['General Motors Corporation', 30.27], ['Hewlett-Packard Co.', 36.53], ['Honeywell Intl Inc', 38.77], ['Intel Corporation', 19.88], ['International Business Machines', 81.41], ['Johnson & Johnson', 64.72], ['JP Morgan & Chase & Co', 45.73], ['McDonald\'s Corporation', 36.76], ['Merck & Co., Inc.', 40.96], ['Microsoft Corporation', 25.84], ['Pfizer Inc', 27.96], ['The Coca-Cola Company', 45.07], ['The Home Depot, Inc.', 34.64], ['The Procter & Gamble Company', 61.91], ['United Technologies Corporation', 63.26], ['Verizon Communications', 35.57], ['Wal-Mart Stores, Inc.', 45.45]];
// create the data store
var store = new Ext.data.ArrayStore({
fields: [{
name: 'company'
},
{
name: 'price',
type: 'float'
}]
});
store.loadData(myData);
var panel = new Ext.Panel({
width: 900,
height: 350,
id: 'splitpanel',
layout: 'hboxfitsplit',
title: 'Ext.layout.HBoxFitSplit',
defaults: {
border: false
},
items: [{
title: 'Panel',
id: 'col1',
html: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean nisi nulla, semper at, pulvinar vitae, tempor gravida, arcu. In erat turpis, sodales eu, consequat at, auctor nec, diam. Cras eget risus. Nullam at tellus vitae nibh blandit rhoncus.',
width: 100,
maxWidth: 150
},
{
title: 'FormPanel',
id: 'col2',
layout: 'form',
width: 250,
minWidth: 200,
maxWidth: 250,
bodyStyle: 'padding:10px',
items: [{
xtype: 'fieldset',
title: 'Create account',
anchor: '0',
labelWidth: 60,
defaults: {
labelSeparator: ''
},
items: [{
xtype: 'textfield',
fieldLabel: 'Username',
anchor: '0'
},
{
fieldLabel: 'Password',
xtype: 'textfield',
inputType: 'password',
anchor: '0'
}]
},
{
xtype: 'fieldset',
title: 'Login',
anchor: '0',
labelWidth: 60,
defaults: {
labelSeparator: ''
},
items: [{
xtype: 'textfield',
fieldLabel: 'Username',
anchor: '0'
},
{
fieldLabel: 'Password',
xtype: 'textfield',
inputType: 'password',
anchor: '0'
},
{
xtype: 'checkbox',
boxLabel: 'Auto login'
}]
}],
buttons: [{
text: 'Login'
}]
},
{
title: 'GridPanel',
id: 'col3',
xtype: 'grid',
store: store,
columns: [{
id: 'company',
header: "Company",
width: 160,
sortable: true,
dataIndex: 'company'
},
{
header: "Price",
width: 75,
sortable: true,
renderer: 'usMoney',
dataIndex: 'price'
}],
stripeRows: true,
minWidth: 200,
viewConfig: {
forceFit: true
}
},
{
title: 'Panel',
id: 'col4',
width: 100,
html: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean nisi nulla, semper at, pulvinar vitae, tempor gravida, arcu. In erat turpis, sodales eu, consequat at, auctor nec, diam. Cras eget risus.'
},
{
id: 'col5',
minWidth: 200,
xtype: 'tabpanel',
activeTab: 0,
items: [{
title: 'Tab 1',
html: 'A simple tab'
},
{
title: 'Tab 2',
html: 'Another one'
}]
}],
renderTo: document.body
});
});
index.html
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Ext.layout.HBoxFitSplit Example</title>
<link type="text/css" rel="stylesheet" media="all" href="lib/ext-3.0-rc1/resources/css/ext-all.css" />
<style type="text/css">
body {
padding:25px;
font-family: Verdana, Tahoma, Arial, Sans-serif;
font-size: 12px;
}
#col1 .x-panel-body,
#col2 .x-panel-body,
#col4 .x-panel-body,
#col5 .x-panel-body {
padding:5px;
overflow:auto;
}
.x-splitbar-h {
background:#eee url(images/split-bg.png) repeat-y 0 0;
}
</style>
</head>
<body>
<script type="text/javascript" src="lib/ext-3.0-rc1/adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="lib/ext-3.0-rc1/ext-all.js"></script>
<script type="text/javascript" src="js/extensions/Ext.layout.HBoxFitSplit.js"></script>
<script type="text/javascript" src="js/main.js"></script>
</body>
</html>
Enjoy! Let me know about bugs or feature requests.
UPDATE 4/20/09: I completely re-wrote the code that calculates the container item widths. I removed the flex-based calculations in favor of pixel widths after the layout is rendered once. If the layout width changes, I simply re-calculate widths based on current values and the old layout width. I added the ability to set a static width to container items. You may specify as many widths as you'd like, but there currently must be at least 1 flex value. Also, all of the specified flex values must add up to 1.0, or the layout will not render. This will change in a future release where flex values will be automatically calculated, if not specified. The demo has been updated to reflect these changes.
DEMO: http://www.ann0yanc3.com/Ext.layout.HBoxFitSplit
This layout combines elements of the HBox layout with SplitBar components. Simply add layout: 'hboxfitsplit' to your container, and the layout will organize the items horizontally and add a SplitBar between each item. Currently, item widths are determined by the flex value of container items, or the width. If you mix flex and width values, the width of each flex item is calculated with the remaining available width of the container (after static widths are added). Each item of this layout can also have a minWidth and maxWidth value, representing the minimum width and maximum width of the item, respectively.
The layoutconfig options for this layout are itemMinWidth and splitWidth. The first being the minimum width for each container item, and the latter being the width of each SplitBar. itemMinWidth defaults to 100, and is overriden by the minWidth value of container items.
This layout has been tested in FF3, Safari 3.2.2, and Google Chrome 1.0.154.53. IE8 RC1 seems to be working as well, currently don't have IE8 Final, or IE7 to test.
In the demo, I use a custom graphic for the splitbar, and no borders for the container items.
Ext.layout.HBoxFitSplitLayout.js
/**
* @class Ext.layout.HBoxSplitLayout
* @extends Ext.layout.BoxLayout
* @version 0.3
* @license LGPLv3 - http://www.gnu.org/licenses/lgpl.html
* @author Ry Racherbaumer
*
* A layout that arranges items horizontally with a split bar between each item
*/
Ext.layout.HBoxFitSplitLayout = Ext.extend(Ext.layout.BoxLayout, {
/**
* @cfg {Number} flex
* This configuation option is to be applied to <b>child <tt>items</tt></b> of the container managed
* by this layout. Each child item with a <tt>flex</tt> property will be flexed <b>horizontally</b>
* according to each item's <b>relative</b> <tt>flex</tt> value compared to the sum of all items with
* a <tt>flex</tt> value specified. Any child items that have either a <tt>flex = 0</tt> or
* <tt>flex = undefined</tt>, or no width value, will be assigned a flex value based on the remaining
* width of the container.
*/
/**
* @cfg {Number} minWidth
* This configuation option is to be applied to <b>child <tt>items</tt></b> of the container managed
* by this layout. This is the minimum width that the child element can be. This setting will override the
* itemMinWidth option for this layout.
*/
/**
* @cfg {Number} maxWidth
* This configuation option is to be applied to <b>child <tt>items</tt></b> of the container managed
* by this layout. This is the maximum width that the child element can be.
*/
/**
* @cfg {Number} itemMinWidth
* This is the minimum width in pixels that child items can be. Defaults to 100. This setting is overridden
* by the minWidth option on component items.
*/
itemMinWidth: 100,
/**
* @cfg {Number} splitBarWidth
* This is the width in pixels of each SplitBar. Defaults to 5.
*/
splitBarWidth: 5,
// private
splitsRendered: false,
rendered: false,
splits: [],
// private
onLayout: function(ct, target) {
// setup variables
var i, len, last, num;
var cs, c, i, cm, cw, ch, ct;
// add/render splitbar box components
if (!this.splitsRendered) {
len = ct.items.length,
last = len - 1,
num = 1,
index = 1;
for (i = 0; i < len; ++i) {
if (i < last) {
ct.insert(index, {
xtype: 'box',
id: ct.id + '-splitbar' + num,
width: this.splitBarWidth,
isSplitBar: true
}); ++num;
index += 2;
}
}
// render the new items
Ext.layout.HBoxFitSplitLayout.superclass.onLayout.call(this, ct, target);
// add splitbars
ct.items.eachKey(function(key, item, index, count) {
if (key.indexOf('split') !== -1) {
this.splits[this.splits.length] = new Ext.SplitBar(Ext.get(item.id), Ext.getCmp(ct.items.items[index - 1].id).getEl(), Ext.SplitBar.HORIZONTAL, Ext.SplitBar.LEFT);
this.splits[this.splits.length - 1].minSize = Ext.getCmp(ct.items.items[index - 1].id).minWidth || this.itemMinWidth;
this.splits[this.splits.length - 1].on('beforeapply',
function(c, newSize) {
this.fireEvent('moved', this, newSize);
// don't resize elements automatically
return false;
});
this.splits[this.splits.length - 1].on('moved', this.onSplitMove, this);
this.splits[this.splits.length - 1].on('beforeresize', this.onBeforeResize, this);
}
},
this);
// splits have been rendered
this.splitsRendered = true;
// if splits are rendered, just call parent onLayout
} else Ext.layout.HBoxFitSplitLayout.superclass.onLayout.call(this, ct, target);
// get layout components
cs = ct.items.items,
len = ct.items.items.length,
last = len - 1;
// get the size of the layout
var layoutSize = this.getTargetSize(target);
// get width, height, and left pos of layout
var layoutWidth = layoutSize.width - target.getPadding('lr') - this.scrollOffset,
layoutHeight = layoutSize.height - target.getPadding('tb'),
layoutLeft = this.padding.left,
layoutTop = this.padding.top;
// make sure width/height is greater than 0
if ((Ext.isIE && !Ext.isStrict) && (layoutWidth < 1 || layoutHeight < 1)) {
return;
} else if (layoutWidth < 1 && layoutHeight < 1) {
return;
}
// get width of all splitbars
var splitBarsWidth = this.splitBarWidth * this.splits.length;
// get minimum allowable layout width, based on component minWidth, or itemMinWidth
var minLayoutWidth = splitBarsWidth;
for (i = 0; i < len; i++) {
c = cs[i];
if (!c.isSplitBar) {
if (c.minWidth && c.minWidth > 0) minLayoutWidth += c.minWidth;
else minLayoutWidth += this.itemMinWidth;
}
}
// if layout is less than min allowable width, set to min allowable width
if (layoutWidth < minLayoutWidth) {
// set layout container to minimum allowed width
// factor in extra container width (i.e. borders)
ct.setWidth(minLayoutWidth + (ct.getWidth() - layoutWidth));
// setting the container width will call onLayout again
return;
}
// set width/height of inner container to match layout
this.innerCt.setSize(layoutWidth, layoutHeight);
// get stretch height
var stretchHeight = layoutHeight - (this.padding.top + this.padding.bottom);
// get available width for layout items
var availableWidth = layoutWidth - splitBarsWidth;
// if layout has never been rendered, make sure flex values are setup properly
if (!this.rendered) {
// total flex value
var totalFlex = 0;
// make sure total flex value is equal to 1
for (i = 0; i < len; i++) {
c = cs[i];
if (!c.isSplitBar && c.flex && c.flex > 0) totalFlex += c.flex;
}
// total flex cannot be greater than 1
if (totalFlex > 1) return;
// if total flex value is less than 1, set flex value for components without flex and width
else if (totalFlex < 1) {
var availFlex = 1 - totalFlex;
num = 0;
// count number of items without width and flex values
for (i = 0; i < len; i++) {
c = cs[i];
if (!c.isSplitBar && !c.flex && !c.width)++num;
}
// set flex values for items without width and flex values
for (i = 0; i < len; i++) {
c = cs[i];
if (!c.isSplitBar && !c.flex && !c.width) c.flex = availFlex / num;
}
}
var availableFlexWidth = availableWidth,
allocatedFlexWidth = 0;
// if component width is specified, remove flex value if specified
// subtract component width from available flex width
for (i = 0; i < len; i++) {
c = cs[i];
if (!c.isSplitBar && c.width && c.width > 0) {
// subtract component width from available width (for flex calculations)
availableFlexWidth -= c.width;
// remove flex value if it's specified
if (c.flex || c.flex > 0) delete c.flex;
// remove width value from future checks
delete c.width;
}
}
// calculate component widths for components with flex value
for (i = 0; i < len; i++) {
// get component
c = cs[i];
// get component margins
cm = c.margins;
// if component is not a splitbar
if (!c.isSplitBar) {
// calculate width of flex value
if (c.flex && c.flex > 0) {
cw = Math.round(availableFlexWidth * c.flex);
// remove flex value
delete c.flex;
// increase allocated width
allocatedFlexWidth += cw;
// if allocated width is not equal to available width for last item, subtract diff
if (i === last && (allocatedFlexWidth - availableFlexWidth !== 0)) cw -= (allocatedFlexWidth - availableFlexWidth);
// account for component margins
cw -= (cm.left + cm.right);
// set component width
c.setWidth(cw);
}
}
}
this.rendered = true;
} else {
// see if layout width has changed
var totalComponentWidth = 0;
for (i = 0; i < len; i++) {
c = cs[i];
cm = c.margins;
cw = c.getWidth();
cw += cm.left + cm.right;
if (!c.isSplitBar) totalComponentWidth += cw;
}
if (availableWidth !== totalComponentWidth) {
var allocatedWidth = 0,
nw;
// re-calculate component widths based on new layout width
for (i = 0; i < len; i++) {
c = cs[i];
cw = c.getWidth();
cm = c.margins;
cw += cm.left + cm.right;
if (!c.isSplitBar) {
nw = Math.round(cw / totalComponentWidth * availableWidth);
allocatedWidth += nw;
// if allocated width is not equal to available width for last item, subtract diff
if (i === last && (allocatedWidth - availableWidth !== 0)) nw -= allocatedWidth - availableWidth;
c.setWidth(nw - cm.left - cm.right);
}
}
}
}
// make sure component widths don't fall below min/max widths
// if they do, set their widths to min/max values
var adjustedComponents = [];
var newAvailableWidth = availableWidth;
for (i = 0; i < len; i++) {
c = cs[i];
if (!c.isSplitBar) {
cm = c.margins;
cw = c.getWidth();
// add left/right margins to component width
cw += cm.left + cm.right;
// set min/max width, accounting for margins
if ((c.minWidth && c.minWidth > 0 && cw <= c.minWidth) || cw <= this.itemMinWidth) {
if (c.minWidth && c.minWidth > 0) {
c.setWidth(c.minWidth - (cm.left + cm.right));
newAvailableWidth -= c.minWidth;
} else {
c.setWidth(this.itemMinWidth - (cm.left + cm.right))
newAvailableWidth -= this.itemMinWidth;
}
adjustedComponents[adjustedComponents.length] = i;
}
if (c.maxWidth && c.maxWidth > 0 && cw >= c.maxWidth) {
c.setWidth(c.maxWidth - (cm.left + cm.right));
newAvailableWidth -= c.maxWidth;
adjustedComponents[adjustedComponents.length] = i;
}
}
}
// if component widths were adjusted, re-calculate other item widths
if (adjustedComponents.length > 0) {
num = 0;
var allocatedWidth = 0,
oldAvailableWidth = 0,
minWidth = 0;
// get current width of all items that didn't fall below min/max widths
for (i = 0; i < len; i++) {
c = cs[i];
cw = c.getWidth();
cm = c.margins;
// add left/right margins to component width
cw += cm.left + cm.right;
if (!c.isSplitBar && adjustedComponents.indexOf(i) === -1) {
oldAvailableWidth += cw;
minWidth += (c.minWidth && c.minWidth > 0) ? c.minWidth: this.itemMinWidth;
// increase item count
++num;
}
}
var nw;
// set new widths for all items that didn't fall below min/max widths
for (i = 0; i < len; i++) {
c = cs[i];
// make sure component didn't fall below item min/max widths
if (!c.isSplitBar && adjustedComponents.indexOf(i) === -1) {
cm = c.margins;
nw = Math.round((newAvailableWidth * c.getWidth()) / oldAvailableWidth);
allocatedWidth += nw;
// decrease item count
--num;
// if last item, make sure there's no extra width
if (num === 0 && (allocatedWidth - newAvailableWidth !== 0)) nw -= allocatedWidth - newAvailableWidth;
// set component width, accounting for margins
c.setWidth(nw - (cm.left + cm.right));
}
}
// check if new available width falls below the minimum possible width
// if so, subtract needed width equally from other components that can spare it
if (newAvailableWidth < minWidth) {
var extraWidthComponents = [],
extraWidths = [],
neededWidth = minWidth - newAvailableWidth;
var currentMinWidth = 0;
num = 0;
for (i = 0; i < len; i++) {
c = cs[i];
cw = c.getWidth();
cm = c.margins;
// add left/right margins to component width
cw += cm.left + cm.right;
currentMinWidth = (c.minWidth && c.minWidth > 0) ? c.minWidth: this.itemMinWidth;
if (!c.isSplitBar && cw > currentMinWidth) {
var extraWidth = cw - currentMinWidth;
extraWidthComponents[extraWidthComponents.length] = i;
extraWidths[extraWidths.length] = extraWidth; ++num;
}
}
// get the width to subtract from each component with available width
var subtractWidth = Math.round(neededWidth / extraWidths.length),
subtractedWidth = 0;
// make sure each component has enough width to subtract
for (i = 0; i < len; i++) {
c = cs[i];
cw = c.getWidth();
cm = c.margins;
// add left/right margins to component width
cw += cm.left + cm.right;
if (extraWidthComponents.indexOf(i) !== -1 && extraWidths[extraWidthComponents.indexOf(i)] < subtractWidth) {
// set width of component
c.setWidth(cw - extraWidths[extraWidthComponents.indexOf(i)]);
// subtract width from needed width
neededWidth -= extraWidths[extraWidthComponents.indexOf(i)];
// remove component from width arrays
extraWidthComponents.splice(extraWidthComponents.indexOf(i), 1);
extraWidths.splice(extraWidthComponents.indexOf(i), 1);
// re-calculate subtract width
subtractWidth = Math.round(neededWidth / extraWidths.length); --num;
}
}
// subtract width from components with enough width
var sw;
for (i = 0; i < len; i++) {
c = cs[i];
cw = c.getWidth();
cm = c.margins;
// add left/right margins to component width
cw += cm.left + cm.right;
if (extraWidthComponents.indexOf(i) !== -1) {
// keep track of subtracted width
subtractedWidth += subtractWidth;
sw = subtractWidth; --num;
// if last item to subtract, make sure there's no extra width
if (num === 0 && (subtractedWidth - neededWidth !== 0)) sw -= subtractedWidth - neededWidth;
// set width of component
c.setWidth(cw - sw);
}
}
// loop through un-adjusted components and set their minimum width, if necessary
for (i = 0; i < len; i++) {
c = cs[i];
cw = c.getWidth();
cm = c.margins;
// add left/right margins to component width
cw += cm.left + cm.right;
currentMinWidth = (c.minWidth && c.minWidth > 0) ? c.minWidth: this.itemMinWidth;
if (!c.isSplitBar && adjustedComponents.indexOf(i) === -1 && cw < currentMinWidth) {
// set minimum width of component, factor in margins
c.setWidth(currentMinWidth - cm.left - cm.right);
}
}
}
}
// set component positions within the layout
for (i = 0; i < len; i++) {
// get component
c = cs[i];
// get component margins
cm = c.margins;
// get component width
cw = c.getWidth();
// get component height
ch = c.getHeight();
// current left position within layout
layoutLeft += cm.left;
// component top position
ct = layoutTop + cm.top;
// set position of component
c.setPosition(layoutLeft, ct);
// set component height to layout height
c.setHeight((stretchHeight - (cm.top + cm.bottom)).constrain(c.minHeight || 0, c.maxHeight || 1000000));
// increase layout left position
layoutLeft += cw + cm.right;
}
},
// execute when a splitbar has moved
onSplitMove: function(split, newSize) {
// get split resize component
var c = Ext.getCmp(split.resizingEl.id);
var cm = c.margins;
var target = this.container.getLayoutTarget();
var size = this.getTargetSize(target);
// layout width
var lw = size.width - target.getPadding('lr') - this.scrollOffset;
// get total splitbar width
var sw = this.splitBarWidth * this.splits.length;
// get total width
var w = lw - sw;
// get split adjacent component
var ac = Ext.getCmp(split.el.id).nextSibling();
var acm = ac.margins;
// get sum of resize and adjacent component widths + margins
var tcw = c.getWidth() + cm.left + cm.right + ac.getWidth() + acm.left + acm.right;
// new adjacent component width
var nacw = tcw - newSize;
// set item width
c.setWidth(newSize);
ac.setWidth(nacw);
this.layout();
},
// execute before split bar is moved
onBeforeResize: function(split) {
var c = Ext.getCmp(split.resizingEl.id);
var cm = c.margins;
var sc = Ext.getCmp(split.el.id);
var ac = Ext.getCmp(split.el.id).nextSibling();
var acm = ac.margins;
// get total component width
var tcw = c.getWidth() + ac.getWidth();
// get adjacent component min width
var acMinWidth = ac.minWidth || this.itemMinWidth;
// calculate max size of splitbar
if (c.maxWidth && c.maxWidth > 0 && c.maxWidth <= (tcw - acMinWidth)) {
split.maxSize = c.maxWidth;
} else split.maxSize = tcw - acMinWidth;
}
});
// register layout
Ext.Container.LAYOUTS['hboxfitsplit'] = Ext.layout.HBoxFitSplitLayout;
main.js
Ext.onReady(function() {
// data for grid
var myData = [['3m Co', 71.7], ['Alcoa Inc', 29.01], ['Altria Group Inc', 83.81], ['American Express Company', 52.55], ['American International Group, Inc.', 64.13], ['AT&T Inc.', 31.61], ['Boeing Co.', 75.43], ['Caterpillar Inc.', 67.27], ['Citigroup, Inc.', 49.37], ['E.I. du Pont de Nemours and Company', 40.48], ['Exxon Mobil Corp', 68.1], ['General Electric Company', 34.14], ['General Motors Corporation', 30.27], ['Hewlett-Packard Co.', 36.53], ['Honeywell Intl Inc', 38.77], ['Intel Corporation', 19.88], ['International Business Machines', 81.41], ['Johnson & Johnson', 64.72], ['JP Morgan & Chase & Co', 45.73], ['McDonald\'s Corporation', 36.76], ['Merck & Co., Inc.', 40.96], ['Microsoft Corporation', 25.84], ['Pfizer Inc', 27.96], ['The Coca-Cola Company', 45.07], ['The Home Depot, Inc.', 34.64], ['The Procter & Gamble Company', 61.91], ['United Technologies Corporation', 63.26], ['Verizon Communications', 35.57], ['Wal-Mart Stores, Inc.', 45.45]];
// create the data store
var store = new Ext.data.ArrayStore({
fields: [{
name: 'company'
},
{
name: 'price',
type: 'float'
}]
});
store.loadData(myData);
var panel = new Ext.Panel({
width: 900,
height: 350,
id: 'splitpanel',
layout: 'hboxfitsplit',
title: 'Ext.layout.HBoxFitSplit',
defaults: {
border: false
},
items: [{
title: 'Panel',
id: 'col1',
html: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean nisi nulla, semper at, pulvinar vitae, tempor gravida, arcu. In erat turpis, sodales eu, consequat at, auctor nec, diam. Cras eget risus. Nullam at tellus vitae nibh blandit rhoncus.',
width: 100,
maxWidth: 150
},
{
title: 'FormPanel',
id: 'col2',
layout: 'form',
width: 250,
minWidth: 200,
maxWidth: 250,
bodyStyle: 'padding:10px',
items: [{
xtype: 'fieldset',
title: 'Create account',
anchor: '0',
labelWidth: 60,
defaults: {
labelSeparator: ''
},
items: [{
xtype: 'textfield',
fieldLabel: 'Username',
anchor: '0'
},
{
fieldLabel: 'Password',
xtype: 'textfield',
inputType: 'password',
anchor: '0'
}]
},
{
xtype: 'fieldset',
title: 'Login',
anchor: '0',
labelWidth: 60,
defaults: {
labelSeparator: ''
},
items: [{
xtype: 'textfield',
fieldLabel: 'Username',
anchor: '0'
},
{
fieldLabel: 'Password',
xtype: 'textfield',
inputType: 'password',
anchor: '0'
},
{
xtype: 'checkbox',
boxLabel: 'Auto login'
}]
}],
buttons: [{
text: 'Login'
}]
},
{
title: 'GridPanel',
id: 'col3',
xtype: 'grid',
store: store,
columns: [{
id: 'company',
header: "Company",
width: 160,
sortable: true,
dataIndex: 'company'
},
{
header: "Price",
width: 75,
sortable: true,
renderer: 'usMoney',
dataIndex: 'price'
}],
stripeRows: true,
minWidth: 200,
viewConfig: {
forceFit: true
}
},
{
title: 'Panel',
id: 'col4',
width: 100,
html: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean nisi nulla, semper at, pulvinar vitae, tempor gravida, arcu. In erat turpis, sodales eu, consequat at, auctor nec, diam. Cras eget risus.'
},
{
id: 'col5',
minWidth: 200,
xtype: 'tabpanel',
activeTab: 0,
items: [{
title: 'Tab 1',
html: 'A simple tab'
},
{
title: 'Tab 2',
html: 'Another one'
}]
}],
renderTo: document.body
});
});
index.html
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Ext.layout.HBoxFitSplit Example</title>
<link type="text/css" rel="stylesheet" media="all" href="lib/ext-3.0-rc1/resources/css/ext-all.css" />
<style type="text/css">
body {
padding:25px;
font-family: Verdana, Tahoma, Arial, Sans-serif;
font-size: 12px;
}
#col1 .x-panel-body,
#col2 .x-panel-body,
#col4 .x-panel-body,
#col5 .x-panel-body {
padding:5px;
overflow:auto;
}
.x-splitbar-h {
background:#eee url(images/split-bg.png) repeat-y 0 0;
}
</style>
</head>
<body>
<script type="text/javascript" src="lib/ext-3.0-rc1/adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="lib/ext-3.0-rc1/ext-all.js"></script>
<script type="text/javascript" src="js/extensions/Ext.layout.HBoxFitSplit.js"></script>
<script type="text/javascript" src="js/main.js"></script>
</body>
</html>
Enjoy! Let me know about bugs or feature requests.