@phosphor/widgets
Version:
PhosphorJS - Widgets
723 lines (722 loc) • 26.2 kB
JavaScript
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
}
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
var algorithm_1 = require("@phosphor/algorithm");
var domutils_1 = require("@phosphor/domutils");
var messaging_1 = require("@phosphor/messaging");
var properties_1 = require("@phosphor/properties");
var boxengine_1 = require("./boxengine");
var layout_1 = require("./layout");
var widget_1 = require("./widget");
/**
* A layout which arranges its widgets in a grid.
*/
var GridLayout = /** @class */ (function (_super) {
__extends(GridLayout, _super);
/**
* Construct a new grid layout.
*
* @param options - The options for initializing the layout.
*/
function GridLayout(options) {
if (options === void 0) { options = {}; }
var _this = _super.call(this, options) || this;
_this._dirty = false;
_this._rowSpacing = 4;
_this._columnSpacing = 4;
_this._items = [];
_this._rowStarts = [];
_this._columnStarts = [];
_this._rowSizers = [new boxengine_1.BoxSizer()];
_this._columnSizers = [new boxengine_1.BoxSizer()];
_this._box = null;
if (options.rowCount !== undefined) {
Private.reallocSizers(_this._rowSizers, options.rowCount);
}
if (options.columnCount !== undefined) {
Private.reallocSizers(_this._columnSizers, options.columnCount);
}
if (options.rowSpacing !== undefined) {
_this._rowSpacing = Private.clampValue(options.rowSpacing);
}
if (options.columnSpacing !== undefined) {
_this._columnSpacing = Private.clampValue(options.columnSpacing);
}
return _this;
}
/**
* Dispose of the resources held by the layout.
*/
GridLayout.prototype.dispose = function () {
// Dispose of the widgets and layout items.
algorithm_1.each(this._items, function (item) {
var widget = item.widget;
item.dispose();
widget.dispose();
});
// Clear the layout state.
this._box = null;
this._items.length = 0;
this._rowStarts.length = 0;
this._rowSizers.length = 0;
this._columnStarts.length = 0;
this._columnSizers.length = 0;
// Dispose of the rest of the layout.
_super.prototype.dispose.call(this);
};
Object.defineProperty(GridLayout.prototype, "rowCount", {
/**
* Get the number of rows in the layout.
*/
get: function () {
return this._rowSizers.length;
},
/**
* Set the number of rows in the layout.
*
* #### Notes
* The minimum row count is `1`.
*/
set: function (value) {
// Do nothing if the row count does not change.
if (value === this.rowCount) {
return;
}
// Reallocate the row sizers.
Private.reallocSizers(this._rowSizers, value);
// Schedule a fit of the parent.
if (this.parent) {
this.parent.fit();
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(GridLayout.prototype, "columnCount", {
/**
* Get the number of columns in the layout.
*/
get: function () {
return this._columnSizers.length;
},
/**
* Set the number of columns in the layout.
*
* #### Notes
* The minimum column count is `1`.
*/
set: function (value) {
// Do nothing if the column count does not change.
if (value === this.columnCount) {
return;
}
// Reallocate the column sizers.
Private.reallocSizers(this._columnSizers, value);
// Schedule a fit of the parent.
if (this.parent) {
this.parent.fit();
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(GridLayout.prototype, "rowSpacing", {
/**
* Get the row spacing for the layout.
*/
get: function () {
return this._rowSpacing;
},
/**
* Set the row spacing for the layout.
*/
set: function (value) {
// Clamp the spacing to the allowed range.
value = Private.clampValue(value);
// Bail if the spacing does not change
if (this._rowSpacing === value) {
return;
}
// Update the internal spacing.
this._rowSpacing = value;
// Schedule a fit of the parent.
if (this.parent) {
this.parent.fit();
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(GridLayout.prototype, "columnSpacing", {
/**
* Get the column spacing for the layout.
*/
get: function () {
return this._columnSpacing;
},
/**
* Set the col spacing for the layout.
*/
set: function (value) {
// Clamp the spacing to the allowed range.
value = Private.clampValue(value);
// Bail if the spacing does not change
if (this._columnSpacing === value) {
return;
}
// Update the internal spacing.
this._columnSpacing = value;
// Schedule a fit of the parent.
if (this.parent) {
this.parent.fit();
}
},
enumerable: true,
configurable: true
});
/**
* Get the stretch factor for a specific row.
*
* @param index - The row index of interest.
*
* @returns The stretch factor for the row.
*
* #### Notes
* This returns `-1` if the index is out of range.
*/
GridLayout.prototype.rowStretch = function (index) {
var sizer = this._rowSizers[index];
return sizer ? sizer.stretch : -1;
};
/**
* Set the stretch factor for a specific row.
*
* @param index - The row index of interest.
*
* @param value - The stretch factor for the row.
*
* #### Notes
* This is a no-op if the index is out of range.
*/
GridLayout.prototype.setRowStretch = function (index, value) {
// Look up the row sizer.
var sizer = this._rowSizers[index];
// Bail if the index is out of range.
if (!sizer) {
return;
}
// Clamp the value to the allowed range.
value = Private.clampValue(value);
// Bail if the stretch does not change.
if (sizer.stretch === value) {
return;
}
// Update the sizer stretch.
sizer.stretch = value;
// Schedule an update of the parent.
if (this.parent) {
this.parent.update();
}
};
/**
* Get the stretch factor for a specific column.
*
* @param index - The column index of interest.
*
* @returns The stretch factor for the column.
*
* #### Notes
* This returns `-1` if the index is out of range.
*/
GridLayout.prototype.columnStretch = function (index) {
var sizer = this._columnSizers[index];
return sizer ? sizer.stretch : -1;
};
/**
* Set the stretch factor for a specific column.
*
* @param index - The column index of interest.
*
* @param value - The stretch factor for the column.
*
* #### Notes
* This is a no-op if the index is out of range.
*/
GridLayout.prototype.setColumnStretch = function (index, value) {
// Look up the column sizer.
var sizer = this._columnSizers[index];
// Bail if the index is out of range.
if (!sizer) {
return;
}
// Clamp the value to the allowed range.
value = Private.clampValue(value);
// Bail if the stretch does not change.
if (sizer.stretch === value) {
return;
}
// Update the sizer stretch.
sizer.stretch = value;
// Schedule an update of the parent.
if (this.parent) {
this.parent.update();
}
};
/**
* Create an iterator over the widgets in the layout.
*
* @returns A new iterator over the widgets in the layout.
*/
GridLayout.prototype.iter = function () {
return algorithm_1.map(this._items, function (item) { return item.widget; });
};
/**
* Add a widget to the grid layout.
*
* @param widget - The widget to add to the layout.
*
* #### Notes
* If the widget is already contained in the layout, this is no-op.
*/
GridLayout.prototype.addWidget = function (widget) {
// Look up the index for the widget.
var i = algorithm_1.ArrayExt.findFirstIndex(this._items, function (it) { return it.widget === widget; });
// Bail if the widget is already in the layout.
if (i !== -1) {
return;
}
// Add the widget to the layout.
this._items.push(new layout_1.LayoutItem(widget));
// Attach the widget to the parent.
if (this.parent) {
this.attachWidget(widget);
}
};
/**
* Remove a widget from the grid layout.
*
* @param widget - The widget to remove from the layout.
*
* #### Notes
* A widget is automatically removed from the layout when its `parent`
* is set to `null`. This method should only be invoked directly when
* removing a widget from a layout which has yet to be installed on a
* parent widget.
*
* This method does *not* modify the widget's `parent`.
*/
GridLayout.prototype.removeWidget = function (widget) {
// Look up the index for the widget.
var i = algorithm_1.ArrayExt.findFirstIndex(this._items, function (it) { return it.widget === widget; });
// Bail if the widget is not in the layout.
if (i !== -1) {
return;
}
// Remove the widget from the layout.
var item = algorithm_1.ArrayExt.removeAt(this._items, i);
// Detach the widget from the parent.
if (this.parent) {
this.detachWidget(widget);
}
// Dispose the layout item.
item.dispose();
};
/**
* Perform layout initialization which requires the parent widget.
*/
GridLayout.prototype.init = function () {
var _this = this;
_super.prototype.init.call(this);
algorithm_1.each(this, function (widget) { _this.attachWidget(widget); });
};
/**
* Attach a widget to the parent's DOM node.
*
* @param widget - The widget to attach to the parent.
*/
GridLayout.prototype.attachWidget = function (widget) {
// Send a `'before-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging_1.MessageLoop.sendMessage(widget, widget_1.Widget.Msg.BeforeAttach);
}
// Add the widget's node to the parent.
this.parent.node.appendChild(widget.node);
// Send an `'after-attach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging_1.MessageLoop.sendMessage(widget, widget_1.Widget.Msg.AfterAttach);
}
// Post a fit request for the parent widget.
this.parent.fit();
};
/**
* Detach a widget from the parent's DOM node.
*
* @param widget - The widget to detach from the parent.
*/
GridLayout.prototype.detachWidget = function (widget) {
// Send a `'before-detach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging_1.MessageLoop.sendMessage(widget, widget_1.Widget.Msg.BeforeDetach);
}
// Remove the widget's node from the parent.
this.parent.node.removeChild(widget.node);
// Send an `'after-detach'` message if the parent is attached.
if (this.parent.isAttached) {
messaging_1.MessageLoop.sendMessage(widget, widget_1.Widget.Msg.AfterDetach);
}
// Post a fit request for the parent widget.
this.parent.fit();
};
/**
* A message handler invoked on a `'before-show'` message.
*/
GridLayout.prototype.onBeforeShow = function (msg) {
_super.prototype.onBeforeShow.call(this, msg);
this.parent.update();
};
/**
* A message handler invoked on a `'before-attach'` message.
*/
GridLayout.prototype.onBeforeAttach = function (msg) {
_super.prototype.onBeforeAttach.call(this, msg);
this.parent.fit();
};
/**
* A message handler invoked on a `'child-shown'` message.
*/
GridLayout.prototype.onChildShown = function (msg) {
this.parent.fit();
};
/**
* A message handler invoked on a `'child-hidden'` message.
*/
GridLayout.prototype.onChildHidden = function (msg) {
this.parent.fit();
};
/**
* A message handler invoked on a `'resize'` message.
*/
GridLayout.prototype.onResize = function (msg) {
if (this.parent.isVisible) {
this._update(msg.width, msg.height);
}
};
/**
* A message handler invoked on an `'update-request'` message.
*/
GridLayout.prototype.onUpdateRequest = function (msg) {
if (this.parent.isVisible) {
this._update(-1, -1);
}
};
/**
* A message handler invoked on a `'fit-request'` message.
*/
GridLayout.prototype.onFitRequest = function (msg) {
if (this.parent.isAttached) {
this._fit();
}
};
/**
* Fit the layout to the total size required by the widgets.
*/
GridLayout.prototype._fit = function () {
// Reset the min sizes of the sizers.
for (var i = 0, n = this.rowCount; i < n; ++i) {
this._rowSizers[i].minSize = 0;
}
for (var i = 0, n = this.columnCount; i < n; ++i) {
this._columnSizers[i].minSize = 0;
}
// Filter for the visible layout items.
var items = this._items.filter(function (it) { return !it.isHidden; });
// Fit the layout items.
for (var i = 0, n = items.length; i < n; ++i) {
items[i].fit();
}
// Get the max row and column index.
var maxRow = this.rowCount - 1;
var maxCol = this.columnCount - 1;
// Sort the items by row span.
items.sort(Private.rowSpanCmp);
// Update the min sizes of the row sizers.
for (var i = 0, n = items.length; i < n; ++i) {
// Fetch the item.
var item = items[i];
// Get the row bounds for the item.
var config = GridLayout.getCellConfig(item.widget);
var r1 = Math.min(config.row, maxRow);
var r2 = Math.min(config.row + config.rowSpan - 1, maxRow);
// Distribute the minimum height to the sizers as needed.
Private.distributeMin(this._rowSizers, r1, r2, item.minHeight);
}
// Sort the items by column span.
items.sort(Private.columnSpanCmp);
// Update the min sizes of the column sizers.
for (var i = 0, n = items.length; i < n; ++i) {
// Fetch the item.
var item = items[i];
// Get the column bounds for the item.
var config = GridLayout.getCellConfig(item.widget);
var c1 = Math.min(config.column, maxCol);
var c2 = Math.min(config.column + config.columnSpan - 1, maxCol);
// Distribute the minimum width to the sizers as needed.
Private.distributeMin(this._columnSizers, c1, c2, item.minWidth);
}
// If no size constraint is needed, just update the parent.
if (this.fitPolicy === 'set-no-constraint') {
messaging_1.MessageLoop.sendMessage(this.parent, widget_1.Widget.Msg.UpdateRequest);
return;
}
// Set up the computed min size.
var minH = maxRow * this._rowSpacing;
var minW = maxCol * this._columnSpacing;
// Add the sizer minimums to the computed min size.
for (var i = 0, n = this.rowCount; i < n; ++i) {
minH += this._rowSizers[i].minSize;
}
for (var i = 0, n = this.columnCount; i < n; ++i) {
minW += this._columnSizers[i].minSize;
}
// Update the box sizing and add it to the computed min size.
var box = this._box = domutils_1.ElementExt.boxSizing(this.parent.node);
minW += box.horizontalSum;
minH += box.verticalSum;
// Update the parent's min size constraints.
var style = this.parent.node.style;
style.minWidth = minW + "px";
style.minHeight = minH + "px";
// Set the dirty flag to ensure only a single update occurs.
this._dirty = true;
// Notify the ancestor that it should fit immediately. This may
// cause a resize of the parent, fulfilling the required update.
if (this.parent.parent) {
messaging_1.MessageLoop.sendMessage(this.parent.parent, widget_1.Widget.Msg.FitRequest);
}
// If the dirty flag is still set, the parent was not resized.
// Trigger the required update on the parent widget immediately.
if (this._dirty) {
messaging_1.MessageLoop.sendMessage(this.parent, widget_1.Widget.Msg.UpdateRequest);
}
};
/**
* Update the layout position and size of the widgets.
*
* The parent offset dimensions should be `-1` if unknown.
*/
GridLayout.prototype._update = function (offsetWidth, offsetHeight) {
// Clear the dirty flag to indicate the update occurred.
this._dirty = false;
// Measure the parent if the offset dimensions are unknown.
if (offsetWidth < 0) {
offsetWidth = this.parent.node.offsetWidth;
}
if (offsetHeight < 0) {
offsetHeight = this.parent.node.offsetHeight;
}
// Ensure the parent box sizing data is computed.
if (!this._box) {
this._box = domutils_1.ElementExt.boxSizing(this.parent.node);
}
// Compute the layout area adjusted for border and padding.
var top = this._box.paddingTop;
var left = this._box.paddingLeft;
var width = offsetWidth - this._box.horizontalSum;
var height = offsetHeight - this._box.verticalSum;
// Get the max row and column index.
var maxRow = this.rowCount - 1;
var maxCol = this.columnCount - 1;
// Compute the total fixed row and column space.
var fixedRowSpace = maxRow * this._rowSpacing;
var fixedColSpace = maxCol * this._columnSpacing;
// Distribute the available space to the box sizers.
boxengine_1.BoxEngine.calc(this._rowSizers, Math.max(0, height - fixedRowSpace));
boxengine_1.BoxEngine.calc(this._columnSizers, Math.max(0, width - fixedColSpace));
// Update the row start positions.
for (var i = 0, pos = top, n = this.rowCount; i < n; ++i) {
this._rowStarts[i] = pos;
pos += this._rowSizers[i].size + this._rowSpacing;
}
// Update the column start positions.
for (var i = 0, pos = left, n = this.columnCount; i < n; ++i) {
this._columnStarts[i] = pos;
pos += this._columnSizers[i].size + this._columnSpacing;
}
// Update the geometry of the layout items.
for (var i = 0, n = this._items.length; i < n; ++i) {
// Fetch the item.
var item = this._items[i];
// Ignore hidden items.
if (item.isHidden) {
continue;
}
// Fetch the cell bounds for the widget.
var config = GridLayout.getCellConfig(item.widget);
var r1 = Math.min(config.row, maxRow);
var c1 = Math.min(config.column, maxCol);
var r2 = Math.min(config.row + config.rowSpan - 1, maxRow);
var c2 = Math.min(config.column + config.columnSpan - 1, maxCol);
// Compute the cell geometry.
var x = this._columnStarts[c1];
var y = this._rowStarts[r1];
var w = this._columnStarts[c2] + this._columnSizers[c2].size - x;
var h = this._rowStarts[r2] + this._rowSizers[r2].size - y;
// Update the geometry of the layout item.
item.update(x, y, w, h);
}
};
return GridLayout;
}(layout_1.Layout));
exports.GridLayout = GridLayout;
/**
* The namespace for the `GridLayout` class statics.
*/
(function (GridLayout) {
/**
* Get the cell config for the given widget.
*
* @param widget - The widget of interest.
*
* @returns The cell config for the widget.
*/
function getCellConfig(widget) {
return Private.cellConfigProperty.get(widget);
}
GridLayout.getCellConfig = getCellConfig;
/**
* Set the cell config for the given widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the cell config.
*/
function setCellConfig(widget, value) {
Private.cellConfigProperty.set(widget, Private.normalizeConfig(value));
}
GridLayout.setCellConfig = setCellConfig;
})(GridLayout = exports.GridLayout || (exports.GridLayout = {}));
exports.GridLayout = GridLayout;
/**
* The namespace for the module implementation details.
*/
var Private;
(function (Private) {
/**
* The property descriptor for the widget cell config.
*/
Private.cellConfigProperty = new properties_1.AttachedProperty({
name: 'cellConfig',
create: function () { return ({ row: 0, column: 0, rowSpan: 1, columnSpan: 1 }); },
changed: onChildCellConfigChanged
});
/**
* Normalize a partial cell config object.
*/
function normalizeConfig(config) {
var row = Math.max(0, Math.floor(config.row || 0));
var column = Math.max(0, Math.floor(config.column || 0));
var rowSpan = Math.max(1, Math.floor(config.rowSpan || 0));
var columnSpan = Math.max(1, Math.floor(config.columnSpan || 0));
return { row: row, column: column, rowSpan: rowSpan, columnSpan: columnSpan };
}
Private.normalizeConfig = normalizeConfig;
/**
* Clamp a value to an integer >= 0.
*/
function clampValue(value) {
return Math.max(0, Math.floor(value));
}
Private.clampValue = clampValue;
/**
* A sort comparison function for row spans.
*/
function rowSpanCmp(a, b) {
var c1 = Private.cellConfigProperty.get(a.widget);
var c2 = Private.cellConfigProperty.get(b.widget);
return c1.rowSpan - c2.rowSpan;
}
Private.rowSpanCmp = rowSpanCmp;
/**
* A sort comparison function for column spans.
*/
function columnSpanCmp(a, b) {
var c1 = Private.cellConfigProperty.get(a.widget);
var c2 = Private.cellConfigProperty.get(b.widget);
return c1.columnSpan - c2.columnSpan;
}
Private.columnSpanCmp = columnSpanCmp;
/**
* Reallocate the box sizers for the given grid dimensions.
*/
function reallocSizers(sizers, count) {
// Coerce the count to the valid range.
count = Math.max(1, Math.floor(count));
// Add the missing sizers.
while (sizers.length < count) {
sizers.push(new boxengine_1.BoxSizer());
}
// Remove the extra sizers.
if (sizers.length > count) {
sizers.length = count;
}
}
Private.reallocSizers = reallocSizers;
/**
* Distribute a min size constraint across a range of sizers.
*/
function distributeMin(sizers, i1, i2, minSize) {
// Sanity check the indices.
if (i2 < i1) {
return;
}
// Handle the simple case of no cell span.
if (i1 === i2) {
var sizer = sizers[i1];
sizer.minSize = Math.max(sizer.minSize, minSize);
return;
}
// Compute the total current min size of the span.
var totalMin = 0;
for (var i = i1; i <= i2; ++i) {
totalMin += sizers[i].minSize;
}
// Do nothing if the total is greater than the required.
if (totalMin >= minSize) {
return;
}
// Compute the portion of the space to allocate to each sizer.
var portion = (minSize - totalMin) / (i2 - i1 + 1);
// Add the portion to each sizer.
for (var i = i1; i <= i2; ++i) {
sizers[i].minSize += portion;
}
}
Private.distributeMin = distributeMin;
/**
* The change handler for the child cell config property.
*/
function onChildCellConfigChanged(child) {
if (child.parent && child.parent.layout instanceof GridLayout) {
child.parent.fit();
}
}
})(Private || (Private = {}));