@qooxdoo/framework
Version:
The JS Framework for Coders
955 lines (810 loc) • 27.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)
************************************************************************ */
/**
* Docks children to one of the edges.
*
* *Features*
*
* * Percent width for left/right/center attached children
* * Percent height for top/bottom/center attached children
* * Minimum and maximum dimensions
* * Prioritized growing/shrinking (flex)
* * Auto sizing
* * Margins and Spacings
* * Alignment in orthogonal axis (e.g. alignX of north attached)
* * Different sort options for children
*
* *Item Properties*
*
* <ul>
* <li><strong>edge</strong> <em>(String)</em>: The edge where the layout item
* should be docked. This may be one of <code>north</code>, <code>east</code>,
* <code>south</code>, <code>west</code> or <code>center</code>. (Required)</li>
* <li><strong>width</strong> <em>(String)</em>: Defines a percent
* width for the item. The percent width,
* when specified, is used instead of the width defined by the size hint.
* This is only supported for children added to the north or south edge or
* are centered in the middle of the layout.
* The minimum and maximum width still takes care of the elements limitations.
* It has no influence on the layout's size hint. Percents are mainly useful for
* widgets which are sized by the outer hierarchy.
* </li>
* <li><strong>height</strong> <em>(String)</em>: Defines a percent
* height for the item. The percent height,
* when specified, is used instead of the height defined by the size hint.
* This is only supported for children added to the west or east edge or
* are centered in the middle of the layout.
* The minimum and maximum height still takes care of the elements limitations.
* It has no influence on the layout's size hint. Percents are mainly useful for
* widgets which are sized by the outer hierarchy.
* </li>
* </ul>
*
* *Example*
*
* <pre class="javascript">
* var layout = new qx.ui.layout.Dock();
*
* var w1 = new qx.ui.core.Widget();
* var w2 = new qx.ui.core.Widget();
* var w3 = new qx.ui.core.Widget();
*
* w1.setHeight(200);
* w2.setWidth(150);
*
* var container = new qx.ui.container.Composite(layout);
* container.add(w1, {edge:"north"});
* container.add(w2, {edge:"west"});
* container.add(w3, {edge:"center"});
* </pre>
*
* *Detailed Description*
*
* Using this layout, items may be "docked" to a specific side
* of the available space. Each displayed item reduces the available space
* for the following children. Priorities depend on the position of
* the child in the internal children list.
*
* *External Documentation*
*
* <a href='https://qooxdoo.org/documentation/#/desktop/layout/dock.md'>
* Extended documentation</a> and links to demos of this layout in the qooxdoo manual.
*/
qx.Class.define("qx.ui.layout.Dock", {
extend: qx.ui.layout.Abstract,
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
/**
* @param spacingX {Integer?0} The horizontal spacing. Sets {@link #spacingX}.
* @param spacingY {Integer?0} The vertical spacing. Sets {@link #spacingY}.
* @param separatorX {String|qx.ui.decoration.IDecorator} Separator to render between columns
* @param separatorY {String|qx.ui.decoration.IDecorator} Separator to render between rows
*/
construct(spacingX, spacingY, separatorX, separatorY) {
super();
if (spacingX) {
this.setSpacingX(spacingX);
}
if (spacingY) {
this.setSpacingY(spacingY);
}
if (separatorX) {
this.setSeparatorX(separatorX);
}
if (separatorY) {
this.setSeparatorY(separatorY);
}
},
/*
*****************************************************************************
PROPERTIES
*****************************************************************************
*/
properties: {
/**
* The way the widgets should be displayed (in conjunction with their
* position in the childrens array).
*/
sort: {
check: ["auto", "y", "x"],
init: "auto",
apply: "_applySort"
},
/** Separator lines to use between the horizontal objects */
separatorX: {
check: "Decorator",
nullable: true,
apply: "_applyLayoutChange"
},
/** Separator lines to use between the vertical objects */
separatorY: {
check: "Decorator",
nullable: true,
apply: "_applyLayoutChange"
},
/**
* Whether separators should be collapsed so when a spacing is
* configured the line go over into each other
*/
connectSeparators: {
check: "Boolean",
init: false,
apply: "_applyLayoutChange"
},
/** Horizontal spacing between two children */
spacingX: {
check: "Integer",
init: 0,
apply: "_applyLayoutChange"
},
/** Vertical spacing between two children */
spacingY: {
check: "Integer",
init: 0,
apply: "_applyLayoutChange"
}
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members: {
__children: null,
__edges: null,
// overridden
verifyLayoutProperty: qx.core.Environment.select("qx.debug", {
true(item, name, value) {
this.assertInArray(
name,
["flex", "edge", "height", "width"],
"The property '" + name + "' is not supported by the Dock layout!"
);
if (name === "edge") {
this.assertInArray(value, [
"north",
"south",
"west",
"east",
"center"
]);
} else if (name === "flex") {
this.assertNumber(value);
this.assert(value >= 0);
} else {
this.assertMatch(value, qx.ui.layout.Util.PERCENT_VALUE);
}
},
false: null
}),
// property apply
_applySort() {
// easiest way is to invalidate the cache
this._invalidChildrenCache = true;
// call normal layout change
this._applyLayoutChange();
},
/**
* @type {Map} Maps edge IDs to numeric values
*
* @lint ignoreReferenceField(__edgeMap)
*/
__edgeMap: {
north: 1,
south: 2,
west: 3,
east: 4,
center: 5
},
/**
* @type {Map} Maps edges to align values
*
* @lint ignoreReferenceField(__alignMap)
*/
__alignMap: {
1: "top",
2: "bottom",
3: "left",
4: "right"
},
/**
* Rebuilds cache for sorted children list.
*
*/
__rebuildCache() {
var all = this._getLayoutChildren();
var child, center;
var length = all.length;
var high = [];
var low = [];
var edge = [];
var yfirst = this.getSort() === "y";
var xfirst = this.getSort() === "x";
for (var i = 0; i < length; i++) {
child = all[i];
edge = child.getLayoutProperties().edge;
if (edge === "center") {
if (center) {
throw new Error(
"It is not allowed to have more than one child aligned to 'center'!"
);
}
center = child;
} else if (xfirst || yfirst) {
if (edge === "north" || edge === "south") {
yfirst ? high.push(child) : low.push(child);
} else if (edge === "west" || edge === "east") {
yfirst ? low.push(child) : high.push(child);
}
} else {
high.push(child);
}
}
// Combine sorted children list
var result = high.concat(low);
if (center) {
result.push(center);
}
this.__children = result;
// Cache edges for faster access
var edges = [];
for (var i = 0; i < length; i++) {
edge = result[i].getLayoutProperties().edge;
edges[i] = this.__edgeMap[edge] || 5;
}
this.__edges = edges;
// Clear invalidation marker
delete this._invalidChildrenCache;
},
/*
---------------------------------------------------------------------------
LAYOUT INTERFACE
---------------------------------------------------------------------------
*/
// overridden
renderLayout(availWidth, availHeight, padding) {
// Rebuild flex/width caches
if (this._invalidChildrenCache) {
this.__rebuildCache();
}
var util = qx.ui.layout.Util;
var children = this.__children;
var edges = this.__edges;
var length = children.length;
var flexibles, child, hint, props, flex, grow, width, height, offset;
var widths = [];
var heights = [];
var separatorWidths = this._getSeparatorWidths();
var spacingX = this.getSpacingX();
var spacingY = this.getSpacingY();
// **************************************
// Caching children data
// **************************************
var allocatedWidth = -spacingX;
var allocatedHeight = -spacingY;
if (separatorWidths.x) {
allocatedWidth -= separatorWidths.x + spacingX;
}
if (separatorWidths.y) {
allocatedHeight -= separatorWidths.y + spacingY;
}
for (var i = 0; i < length; i++) {
child = children[i];
props = child.getLayoutProperties();
hint = child.getSizeHint();
width = hint.width;
height = hint.height;
if (props.width != null) {
width = Math.floor((availWidth * parseFloat(props.width)) / 100);
if (width < hint.minWidth) {
width = hint.minWidth;
} else if (width > hint.maxWidth) {
width = hint.maxWidth;
}
}
if (props.height != null) {
height = Math.floor((availHeight * parseFloat(props.height)) / 100);
if (height < hint.minHeight) {
height = hint.minHeight;
} else if (height > hint.maxHeight) {
height = hint.maxHeight;
}
}
widths[i] = width;
heights[i] = height;
// Update allocated width
switch (edges[i]) {
// north+south
case 1:
case 2:
allocatedHeight +=
height +
child.getMarginTop() +
child.getMarginBottom() +
spacingY;
if (separatorWidths.y) {
allocatedHeight += separatorWidths.y + spacingY;
}
break;
// west+east
case 3:
case 4:
allocatedWidth +=
width + child.getMarginLeft() + child.getMarginRight() + spacingX;
if (separatorWidths.x) {
allocatedWidth += separatorWidths.x + spacingX;
}
break;
// center
default:
allocatedWidth +=
width + child.getMarginLeft() + child.getMarginRight() + spacingX;
allocatedHeight +=
height +
child.getMarginTop() +
child.getMarginBottom() +
spacingY;
if (separatorWidths.x) {
allocatedWidth += separatorWidths.x + spacingX;
}
if (separatorWidths.y) {
allocatedHeight += separatorWidths.y + spacingY;
}
}
}
// **************************************
// Horizontal flex support
// **************************************
if (allocatedWidth != availWidth) {
flexibles = {};
grow = allocatedWidth < availWidth;
for (var i = 0; i < length; i++) {
child = children[i];
switch (edges[i]) {
case 3:
case 4:
case 5:
flex = child.getLayoutProperties().flex;
// Default flex for centered children is '1'
if (flex == null && edges[i] == 5) {
flex = 1;
}
if (flex > 0) {
hint = child.getSizeHint();
flexibles[i] = {
min: hint.minWidth,
value: widths[i],
max: hint.maxWidth,
flex: flex
};
}
}
}
var result = util.computeFlexOffsets(
flexibles,
availWidth,
allocatedWidth
);
for (var i in result) {
offset = result[i].offset;
widths[i] += offset;
allocatedWidth += offset;
}
}
// **************************************
// Vertical flex support
// **************************************
// Process height for flex stretching/shrinking
if (allocatedHeight != availHeight) {
flexibles = {};
grow = allocatedHeight < availHeight;
for (var i = 0; i < length; i++) {
child = children[i];
switch (edges[i]) {
case 1:
case 2:
case 5:
flex = child.getLayoutProperties().flex;
// Default flex for centered children is '1'
if (flex == null && edges[i] == 5) {
flex = 1;
}
if (flex > 0) {
hint = child.getSizeHint();
flexibles[i] = {
min: hint.minHeight,
value: heights[i],
max: hint.maxHeight,
flex: flex
};
}
}
}
var result = util.computeFlexOffsets(
flexibles,
availHeight,
allocatedHeight
);
for (var i in result) {
offset = result[i].offset;
heights[i] += offset;
allocatedHeight += offset;
}
}
// **************************************
// Layout children
// **************************************
// Pre configure separators
this._clearSeparators();
// Prepare loop
var separatorX = this.getSeparatorX(),
separatorY = this.getSeparatorY();
var connectSeparators = this.getConnectSeparators();
var nextTop = 0,
nextLeft = 0;
var left, top, width, height, used, edge;
var separatorLeft, separatorTop, separatorWidth, separatorHeight;
var marginTop, marginBottom, marginLeft, marginRight;
var alignMap = this.__alignMap;
for (var i = 0; i < length; i++) {
// Cache child data
child = children[i];
edge = edges[i];
hint = child.getSizeHint();
// Cache child margins
marginTop = child.getMarginTop();
marginBottom = child.getMarginBottom();
marginLeft = child.getMarginLeft();
marginRight = child.getMarginRight();
// Calculate child layout
switch (edge) {
// north + south
case 1:
case 2:
// Full available width
width = availWidth - marginLeft - marginRight;
// Limit width to min/max
if (width < hint.minWidth) {
width = hint.minWidth;
} else if (width > hint.maxWidth) {
width = hint.maxWidth;
}
// Child preferred height
height = heights[i];
// Compute position
top =
nextTop +
util.computeVerticalAlignOffset(
alignMap[edge],
height,
availHeight,
marginTop,
marginBottom
);
left =
nextLeft +
util.computeHorizontalAlignOffset(
child.getAlignX() || "left",
width,
availWidth,
marginLeft,
marginRight
);
// Render the separator
if (separatorWidths.y) {
if (edge == 1) {
separatorTop =
nextTop + height + marginTop + spacingY + marginBottom;
} else {
separatorTop =
nextTop +
availHeight -
height -
marginTop -
spacingY -
marginBottom -
separatorWidths.y;
}
separatorLeft = left;
separatorWidth = availWidth;
if (connectSeparators && separatorLeft > 0) {
separatorLeft -= spacingX + marginLeft;
separatorWidth += spacingX * 2;
} else {
separatorLeft -= marginLeft;
}
this._renderSeparator(separatorY, {
left: separatorLeft + padding.left,
top: separatorTop + padding.top,
width: separatorWidth,
height: separatorWidths.y
});
}
// Update available height
used = height + marginTop + marginBottom + spacingY;
if (separatorWidths.y) {
used += separatorWidths.y + spacingY;
}
availHeight -= used;
// Update coordinates, for next child
if (edge == 1) {
nextTop += used;
}
break;
// west + east
case 3:
case 4:
// Full available height
height = availHeight - marginTop - marginBottom;
// Limit height to min/max
if (height < hint.minHeight) {
height = hint.minHeight;
} else if (height > hint.maxHeight) {
height = hint.maxHeight;
}
// Child preferred width
width = widths[i];
// Compute position
left =
nextLeft +
util.computeHorizontalAlignOffset(
alignMap[edge],
width,
availWidth,
marginLeft,
marginRight
);
top =
nextTop +
util.computeVerticalAlignOffset(
child.getAlignY() || "top",
height,
availHeight,
marginTop,
marginBottom
);
// Render the separator
if (separatorWidths.x) {
if (edge == 3) {
separatorLeft =
nextLeft + width + marginLeft + spacingX + marginRight;
} else {
separatorLeft =
nextLeft +
availWidth -
width -
marginLeft -
spacingX -
marginRight -
separatorWidths.x;
}
separatorTop = top;
separatorHeight = availHeight;
if (connectSeparators && separatorTop > 0) {
separatorTop -= spacingY + marginTop;
separatorHeight += spacingY * 2;
} else {
separatorTop -= marginTop;
}
this._renderSeparator(separatorX, {
left: separatorLeft + padding.left,
top: separatorTop + padding.top,
width: separatorWidths.x,
height: separatorHeight
});
}
// Update available height
used = width + marginLeft + marginRight + spacingX;
if (separatorWidths.x) {
used += separatorWidths.x + spacingX;
}
availWidth -= used;
// Update coordinates, for next child
if (edge == 3) {
nextLeft += used;
}
break;
// center
default:
// Calculated width/height
width = availWidth - marginLeft - marginRight;
height = availHeight - marginTop - marginBottom;
// Limit width to min/max
if (width < hint.minWidth) {
width = hint.minWidth;
} else if (width > hint.maxWidth) {
width = hint.maxWidth;
}
// Limit height to min/max
if (height < hint.minHeight) {
height = hint.minHeight;
} else if (height > hint.maxHeight) {
height = hint.maxHeight;
}
// Compute coordinates (respect margins and alignments for both axis)
left =
nextLeft +
util.computeHorizontalAlignOffset(
child.getAlignX() || "left",
width,
availWidth,
marginLeft,
marginRight
);
top =
nextTop +
util.computeVerticalAlignOffset(
child.getAlignY() || "top",
height,
availHeight,
marginTop,
marginBottom
);
}
// Apply layout
child.renderLayout(
left + padding.left,
top + padding.top,
width,
height
);
}
},
/**
* Computes the dimensions each separator on both the <code>x</code> and
* <code>y</code> axis needs.
*
* @return {Map} Map with the keys <code>x</code> and
* <code>y</code>
*/
_getSeparatorWidths() {
var separatorX = this.getSeparatorX(),
separatorY = this.getSeparatorY();
if (separatorX || separatorY) {
var decorationManager = qx.theme.manager.Decoration.getInstance();
}
if (separatorX) {
var separatorInstanceX = decorationManager.resolve(separatorX);
var separatorInsetsX = separatorInstanceX.getInsets();
var separatorWidthX = separatorInsetsX.left + separatorInsetsX.right;
}
if (separatorY) {
var separatorInstanceY = decorationManager.resolve(separatorY);
var separatorInsetsY = separatorInstanceY.getInsets();
var separatorWidthY = separatorInsetsY.top + separatorInsetsY.bottom;
}
return {
x: separatorWidthX || 0,
y: separatorWidthY || 0
};
},
// overridden
_computeSizeHint() {
// Rebuild flex/width caches
if (this._invalidChildrenCache) {
this.__rebuildCache();
}
var children = this.__children;
var edges = this.__edges;
var length = children.length;
var hint, child;
var marginX, marginY;
var widthX = 0,
minWidthX = 0;
var heightX = 0,
minHeightX = 0;
var widthY = 0,
minWidthY = 0;
var heightY = 0,
minHeightY = 0;
var separatorWidths = this._getSeparatorWidths();
var spacingX = this.getSpacingX(),
spacingY = this.getSpacingY();
var spacingSumX = -spacingX,
spacingSumY = -spacingY;
if (separatorWidths.x) {
spacingSumX -= separatorWidths.x + spacingX;
}
if (separatorWidths.y) {
spacingSumY -= separatorWidths.y + spacingY;
}
// Detect children sizes
for (var i = 0; i < length; i++) {
child = children[i];
hint = child.getSizeHint();
// Pre-cache margin sums
marginX = child.getMarginLeft() + child.getMarginRight();
marginY = child.getMarginTop() + child.getMarginBottom();
// Ok, this part is a bit complicated :)
switch (edges[i]) {
case 1:
case 2:
// Find the maximum width used by these fully stretched items
// The recommended width used by these must add the currently
// occupied width by the orthogonal ordered children.
widthY = Math.max(widthY, hint.width + widthX + marginX);
minWidthY = Math.max(
minWidthY,
hint.minWidth + minWidthX + marginX
);
// Add the needed heights of this widget
heightY += hint.height + marginY;
minHeightY += hint.minHeight + marginY;
// Add spacing
spacingSumY += spacingY;
if (separatorWidths.y) {
spacingSumY += separatorWidths.y + spacingY;
}
break;
case 3:
case 4:
// Find the maximum height used by these fully stretched items
// The recommended height used by these must add the currently
// occupied height by the orthogonal ordered children.
heightX = Math.max(heightX, hint.height + heightY + marginY);
minHeightX = Math.max(
minHeightX,
hint.minHeight + minHeightY + marginY
);
// Add the needed widths of this widget
widthX += hint.width + marginX;
minWidthX += hint.minWidth + marginX;
// Add spacing
spacingSumX += spacingX;
if (separatorWidths.x) {
spacingSumX += separatorWidths.x + spacingX;
}
break;
default:
// A centered widget must be added to both sums as
// it stretches into the remaining available space.
widthX += hint.width + marginX;
minWidthX += hint.minWidth + marginX;
heightY += hint.height + marginY;
minHeightY += hint.minHeight + marginY;
// Add spacing
spacingSumX += spacingX;
if (separatorWidths.x) {
spacingSumX += separatorWidths.x + spacingX;
}
spacingSumY += spacingY;
if (separatorWidths.y) {
spacingSumY += separatorWidths.y + spacingY;
}
}
}
var minWidth = Math.max(minWidthX, minWidthY) + spacingSumX;
var width = Math.max(widthX, widthY) + spacingSumX;
var minHeight = Math.max(minHeightX, minHeightY) + spacingSumY;
var height = Math.max(heightX, heightY) + spacingSumY;
// Return hint
return {
minWidth: minWidth,
width: width,
minHeight: minHeight,
height: height
};
}
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct() {
this.__edges = this.__children = null;
}
});