UNPKG

@phosphor/widgets

Version:
723 lines (722 loc) 26.2 kB
"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 = {}));