UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

540 lines (458 loc) 15.1 kB
/* ************************************************************************ 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; } });