@qooxdoo/framework
Version:
The JS Framework for Coders
540 lines (458 loc) • 15.1 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2008 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Sebastian Werner (wpbasti)
* Fabian Jakobs (fjakobs)
************************************************************************ */
/**
* A vertical box layout.
*
* The vertical box layout lays out widgets in a vertical column, from top
* to bottom.
*
* *Features*
*
* * Minimum and maximum dimensions
* * Prioritized growing/shrinking (flex)
* * Margins (with vertical collapsing)
* * Auto sizing (ignoring percent values)
* * Percent heights (not relevant for size hint)
* * Alignment (child property {@link qx.ui.core.LayoutItem#alignY} is ignored)
* * Vertical spacing (collapsed with margins)
* * Reversed children layout (from last to first)
* * Horizontal children stretching (respecting size hints)
*
* *Item Properties*
*
* <ul>
* <li><strong>flex</strong> <em>(Integer)</em>: The flexibility of a layout item determines how the container
* distributes remaining empty space among its children. If items are made
* flexible, they can grow or shrink accordingly. Their relative flex values
* determine how the items are being resized, i.e. the larger the flex ratio
* of two items, the larger the resizing of the first item compared to the
* second.
*
* If there is only one flex item in a layout container, its actual flex
* value is not relevant. To disallow items to become flexible, set the
* flex value to zero.
* </li>
* <li><strong>height</strong> <em>(String)</em>: Allows to define a percent
* height for the item. The height in percent, if specified, is used instead
* of the height defined by the size hint. The minimum and maximum height still
* takes care of the element's limits. It has no influence on the layout's
* size hint. Percent values are mostly useful for widgets which are sized by
* the outer hierarchy.
* </li>
* </ul>
*
* *Example*
*
* Here is a little example of how to use the vertical box layout.
*
* <pre class="javascript">
* var layout = new qx.ui.layout.VBox();
* layout.setSpacing(4); // apply spacing
*
* var container = new qx.ui.container.Composite(layout);
*
* container.add(new qx.ui.core.Widget());
* container.add(new qx.ui.core.Widget());
* container.add(new qx.ui.core.Widget());
* </pre>
*
* *External Documentation*
*
* See <a href='https://qooxdoo.org/documentation/#/desktop/layout/box.md'>extended documentation</a>
* and links to demos for this layout.
*
*/
qx.Class.define("qx.ui.layout.VBox", {
extend: qx.ui.layout.Abstract,
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
/**
* @param spacing {Integer?0} The spacing between child widgets {@link #spacing}.
* @param alignY {String?"top"} Vertical alignment of the whole children
* block {@link #alignY}.
* @param separator {String|qx.ui.decoration.IDecorator?} A separator to be rendered between the items
*/
construct(spacing, alignY, separator) {
super();
if (spacing) {
this.setSpacing(spacing);
}
if (alignY) {
this.setAlignY(alignY);
}
if (separator) {
this.setSeparator(separator);
}
},
/*
*****************************************************************************
PROPERTIES
*****************************************************************************
*/
properties: {
/**
* Vertical alignment of the whole children block. The vertical
* alignment of the child is completely ignored in VBoxes (
* {@link qx.ui.core.LayoutItem#alignY}).
*/
alignY: {
check: ["top", "middle", "bottom"],
init: "top",
apply: "_applyLayoutChange"
},
/**
* Horizontal alignment of each child. Can be overridden through
* {@link qx.ui.core.LayoutItem#alignX}.
*/
alignX: {
check: ["left", "center", "right"],
init: "left",
apply: "_applyLayoutChange"
},
/** Vertical spacing between two children */
spacing: {
check: "Integer",
init: 0,
apply: "_applyLayoutChange"
},
/** Separator lines to use between the objects */
separator: {
check: "Decorator",
nullable: true,
apply: "_applyLayoutChange"
},
/** Whether the actual children list should be laid out in reversed order. */
reversed: {
check: "Boolean",
init: false,
apply: "_applyReversed"
}
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members: {
__heights: null,
__flexs: null,
__enableFlex: null,
__children: null,
/*
---------------------------------------------------------------------------
HELPER METHODS
---------------------------------------------------------------------------
*/
// property apply
_applyReversed() {
// easiest way is to invalidate the cache
this._invalidChildrenCache = true;
// call normal layout change
this._applyLayoutChange();
},
/**
* Rebuilds caches for flex and percent layout properties
*/
__rebuildCache() {
var children = this._getLayoutChildren();
var length = children.length;
var enableFlex = false;
var reuse =
this.__heights &&
this.__heights.length != length &&
this.__flexs &&
this.__heights;
var props;
// Sparse array (keep old one if lengths has not been modified)
var heights = reuse ? this.__heights : new Array(length);
var flexs = reuse ? this.__flexs : new Array(length);
// Reverse support
if (this.getReversed()) {
children = children.concat().reverse();
}
// Loop through children to preparse values
for (var i = 0; i < length; i++) {
props = children[i].getLayoutProperties();
if (props.height != null) {
heights[i] = parseFloat(props.height) / 100;
}
if (props.flex != null) {
flexs[i] = props.flex;
enableFlex = true;
} else {
// reset (in case the index of the children changed: BUG #3131)
flexs[i] = 0;
}
}
// Store data
if (!reuse) {
this.__heights = heights;
this.__flexs = flexs;
}
this.__enableFlex = enableFlex;
this.__children = children;
// Clear invalidation marker
delete this._invalidChildrenCache;
},
/*
---------------------------------------------------------------------------
LAYOUT INTERFACE
---------------------------------------------------------------------------
*/
// overridden
verifyLayoutProperty: qx.core.Environment.select("qx.debug", {
true(item, name, value) {
if (name == "height") {
this.assertMatch(value, qx.ui.layout.Util.PERCENT_VALUE);
} else if (name == "flex") {
// flex
this.assertNumber(value);
this.assert(value >= 0);
} else if (name == "flexShrink") {
this.assertBoolean(value);
} else {
this.assert(
false,
"The property '" + name + "' is not supported by the VBox layout!"
);
}
},
false: null
}),
// overridden
renderLayout(availWidth, availHeight, padding) {
// Rebuild flex/height caches
if (this._invalidChildrenCache) {
this.__rebuildCache();
}
// Cache children
var children = this.__children;
var length = children.length;
var util = qx.ui.layout.Util;
// Compute gaps
var spacing = this.getSpacing();
var separator = this.getSeparator();
var gaps;
if (separator) {
gaps = util.computeVerticalSeparatorGaps(children, spacing, separator);
} else {
gaps = util.computeVerticalGaps(children, spacing, true);
}
// First run to cache children data and compute allocated height
var i, child, height, percent;
var heights = [],
hint;
var allocatedHeight = gaps;
for (i = 0; i < length; i += 1) {
percent = this.__heights[i];
hint = children[i].getSizeHint();
height =
percent != null
? Math.floor((availHeight - gaps) * percent)
: hint.height;
// Limit computed value
if (height < hint.minHeight) {
height = hint.minHeight;
} else if (height > hint.maxHeight) {
height = hint.maxHeight;
}
heights.push(height);
allocatedHeight += height;
}
// Flex support (growing/shrinking)
if (this.__enableFlex && allocatedHeight != availHeight) {
var flexibles = {};
var flex, offset;
var notEnoughSpace = allocatedHeight > availHeight;
for (i = 0; i < length; i += 1) {
flex = this.__flexs[i];
if (flex > 0) {
hint = children[i].getSizeHint();
flexibles[i] = {
min: hint.minHeight,
value: heights[i],
max: hint.maxHeight,
flex: flex
};
if (notEnoughSpace) {
var props = children[i].getLayoutProperties();
if (props && props.flexShrink) {
flexibles[i].min = 0;
}
}
}
}
var result = util.computeFlexOffsets(
flexibles,
availHeight,
allocatedHeight
);
for (i in result) {
offset = result[i].offset;
heights[i] += offset;
allocatedHeight += offset;
}
}
// Start with top coordinate
var top = children[0].getMarginTop();
// Alignment support
if (allocatedHeight < availHeight && this.getAlignY() != "top") {
top = availHeight - allocatedHeight;
if (this.getAlignY() === "middle") {
top = Math.round(top / 2);
}
}
// Layouting children
var hint, left, width, height, marginBottom, marginLeft, marginRight;
// Pre configure separators
this._clearSeparators();
// Compute separator height
if (separator) {
var separatorInsets = qx.theme.manager.Decoration.getInstance()
.resolve(separator)
.getInsets();
var separatorHeight = separatorInsets.top + separatorInsets.bottom;
}
// Render children and separators
for (i = 0; i < length; i += 1) {
child = children[i];
height = heights[i];
hint = child.getSizeHint();
marginLeft = child.getMarginLeft();
marginRight = child.getMarginRight();
// Find usable width
width = Math.max(
hint.minWidth,
Math.min(availWidth - marginLeft - marginRight, hint.maxWidth)
);
// Respect horizontal alignment
left = util.computeHorizontalAlignOffset(
child.getAlignX() || this.getAlignX(),
width,
availWidth,
marginLeft,
marginRight
);
// Add collapsed margin
if (i > 0) {
// Whether a separator has been configured
if (separator) {
// add margin of last child and spacing
top += marginBottom + spacing;
// then render the separator at this position
this._renderSeparator(separator, {
top: top + padding.top,
left: padding.left,
height: separatorHeight,
width: availWidth
});
// and finally add the size of the separator, the spacing (again) and the top margin
top += separatorHeight + spacing + child.getMarginTop();
} else {
// Support margin collapsing when no separator is defined
top += util.collapseMargins(
spacing,
marginBottom,
child.getMarginTop()
);
}
}
// Layout child
child.renderLayout(
left + padding.left,
top + padding.top,
width,
height
);
// Add height
top += height;
// Remember bottom margin (for collapsing)
marginBottom = child.getMarginBottom();
}
},
// overridden
_computeSizeHint() {
// Rebuild flex/height caches
if (this._invalidChildrenCache) {
this.__rebuildCache();
}
var util = qx.ui.layout.Util;
var children = this.__children;
// Initialize
var minHeight = 0,
height = 0,
percentMinHeight = 0;
var minWidth = 0,
width = 0;
var child, hint, margin;
// Iterate over children
for (var i = 0, l = children.length; i < l; i += 1) {
child = children[i];
hint = child.getSizeHint();
// Sum up heights
height += hint.height;
// Detect if child is shrinkable or has percent height and update minHeight
var flex = this.__flexs[i];
var percent = this.__heights[i];
if (flex) {
minHeight += hint.minHeight;
} else if (percent) {
percentMinHeight = Math.max(
percentMinHeight,
Math.round(hint.minHeight / percent)
);
} else {
minHeight += hint.height;
}
// Build horizontal margin sum
margin = child.getMarginLeft() + child.getMarginRight();
// Find biggest width
if (hint.width + margin > width) {
width = hint.width + margin;
}
// Find biggest minWidth
if (hint.minWidth + margin > minWidth) {
minWidth = hint.minWidth + margin;
}
}
minHeight += percentMinHeight;
// Respect gaps
var spacing = this.getSpacing();
var separator = this.getSeparator();
var gaps;
if (separator) {
gaps = util.computeVerticalSeparatorGaps(children, spacing, separator);
} else {
gaps = util.computeVerticalGaps(children, spacing, true);
}
// Return hint
return {
minHeight: minHeight + gaps,
height: height + gaps,
minWidth: minWidth,
width: width
};
}
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct() {
this.__heights = this.__flexs = this.__children = null;
}
});