@deephaven/golden-layout
Version:
A multi-screen javascript Layout manager
491 lines (465 loc) • 20.8 kB
JavaScript
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
import $ from 'jquery';
import AbstractContentItem from "./AbstractContentItem.js";
import { animFrame } from "../utils/index.js";
import { Splitter } from "../controls/index.js";
export default class RowOrColumn extends AbstractContentItem {
constructor(isColumn, layoutManager, config, parent) {
super(layoutManager, config, parent, $('<div class="lm_item lm_' + (isColumn ? 'column' : 'row') + '"></div>'));
_defineProperty(this, "isRow", void 0);
_defineProperty(this, "isColumn", void 0);
_defineProperty(this, "childElementContainer", void 0);
_defineProperty(this, "parent", void 0);
_defineProperty(this, "_splitter", []);
_defineProperty(this, "_splitterSize", void 0);
_defineProperty(this, "_splitterGrabSize", void 0);
_defineProperty(this, "_isColumn", void 0);
_defineProperty(this, "_dimension", void 0);
_defineProperty(this, "_splitterPosition", null);
_defineProperty(this, "_splitterMinPosition", null);
_defineProperty(this, "_splitterMaxPosition", null);
this.parent = parent;
this.isRow = !isColumn;
this.isColumn = isColumn;
this.childElementContainer = this.element;
this._splitterSize = layoutManager.config.dimensions.borderWidth;
this._splitterGrabSize = layoutManager.config.dimensions.borderGrabWidth;
this._isColumn = isColumn;
this._dimension = isColumn ? 'height' : 'width';
this._splitterPosition = null;
this._splitterMinPosition = null;
this._splitterMaxPosition = null;
}
/**
* Add a new contentItem to the Row or Column
*
* @param contentItem
* @param index The position of the new item within the Row or Column.
* If no index is provided the item will be added to the end
* @param _$suspendResize If true the items won't be resized. This will leave the item in
* an inconsistent state and is only intended to be used if multiple
* children need to be added in one go and resize is called afterwards
*/
addChild(contentItem, index, _$suspendResize) {
var newItemSize, itemSize, i, splitterElement;
contentItem = this.layoutManager._$normalizeContentItem(contentItem, this);
if (index === undefined) {
index = this.contentItems.length;
}
if (this.contentItems.length > 0) {
splitterElement = this._createSplitter(Math.max(0, index - 1)).element;
if (index > 0) {
this.contentItems[index - 1].element.after(splitterElement);
splitterElement.after(contentItem.element);
} else {
this.contentItems[0].element.before(splitterElement);
splitterElement.before(contentItem.element);
}
} else {
this.childElementContainer.append(contentItem.element);
}
AbstractContentItem.prototype.addChild.call(this, contentItem, index);
newItemSize = 1 / this.contentItems.length * 100;
if (_$suspendResize === true) {
this.emitBubblingEvent('stateChanged');
return;
}
for (i = 0; i < this.contentItems.length; i++) {
if (this.contentItems[i] === contentItem) {
contentItem.config[this._dimension] = newItemSize;
} else {
var _this$contentItems$i$;
itemSize = ((_this$contentItems$i$ = this.contentItems[i].config[this._dimension]) !== null && _this$contentItems$i$ !== void 0 ? _this$contentItems$i$ : 0) * (100 - newItemSize) / 100;
this.contentItems[i].config[this._dimension] = itemSize;
}
}
this.callDownwards('setSize');
this.emitBubblingEvent('stateChanged');
}
/**
* Removes a child of this element
*
* @param contentItem
* @param keepChild If true the child will be removed, but not destroyed
*/
removeChild(contentItem, keepChild) {
var _contentItem$config$t;
var removedItemSize = (_contentItem$config$t = contentItem.config[this._dimension]) !== null && _contentItem$config$t !== void 0 ? _contentItem$config$t : 0,
index = this.contentItems.indexOf(contentItem),
splitterIndex = Math.max(index - 1, 0),
i,
childItem;
if (index === -1) {
throw new Error("Can't remove child. ContentItem is not child of this Row or Column");
}
/**
* Remove the splitter before the item or after if the item happens
* to be the first in the row/column
*/
if (this._splitter[splitterIndex]) {
this._splitter[splitterIndex]._$destroy();
this._splitter.splice(splitterIndex, 1);
}
/**
* Allocate the space that the removed item occupied to the remaining items
*/
for (i = 0; i < this.contentItems.length; i++) {
if (this.contentItems[i] !== contentItem) {
var _this$contentItems$i$2;
this.contentItems[i].config[this._dimension] = ((_this$contentItems$i$2 = this.contentItems[i].config[this._dimension]) !== null && _this$contentItems$i$2 !== void 0 ? _this$contentItems$i$2 : 0) + removedItemSize / (this.contentItems.length - 1);
}
}
AbstractContentItem.prototype.removeChild.call(this, contentItem, keepChild);
if (this.contentItems.length === 1 && this.config.isClosable === true) {
var _this$parent;
childItem = this.contentItems[0];
this.contentItems = [];
(_this$parent = this.parent) === null || _this$parent === void 0 ? void 0 : _this$parent.replaceChild(this, childItem, true);
} else {
this.callDownwards('setSize');
this.emitBubblingEvent('stateChanged');
}
}
/**
* Replaces a child of this Row or Column with another contentItem
*
* @param oldChild The old child to replace
* @param newChild The new child to take the old child's place
* @param destroyOldChild If the old child should be destroyed or not
*/
replaceChild(oldChild, newChild) {
var destroyOldChild = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
var size = oldChild.config[this._dimension];
newChild.config[this._dimension] = size;
super.replaceChild(oldChild, newChild, destroyOldChild);
this.callDownwards('setSize');
this.emitBubblingEvent('stateChanged');
}
/**
* Called whenever the dimensions of this item or one of its parents change
*/
setSize() {
if (this.contentItems.length > 0) {
this._calculateRelativeSizes();
this._setAbsoluteSizes();
}
this.emitBubblingEvent('stateChanged');
this.emit('resize');
}
/**
* Invoked recursively by the layout manager. AbstractContentItem.init appends
* the contentItem's DOM elements to the container, RowOrColumn init adds splitters
* in between them
*/
_$init() {
if (this.isInitialised === true) return;
var i;
AbstractContentItem.prototype._$init.call(this);
for (i = 0; i < this.contentItems.length - 1; i++) {
this.contentItems[i].element.after(this._createSplitter(i).element);
}
}
/**
* Turns the relative sizes calculated by _calculateRelativeSizes into
* absolute pixel values and applies them to the children's DOM elements
*
* Assigns additional pixels to counteract Math.floor
*/
_setAbsoluteSizes() {
var i,
sizeData = this._calculateAbsoluteSizes();
for (i = 0; i < this.contentItems.length; i++) {
if (sizeData.additionalPixel - i > 0) {
sizeData.itemSizes[i]++;
}
if (this._isColumn) {
this.contentItems[i].element.width(sizeData.totalWidth);
this.contentItems[i].element.height(sizeData.itemSizes[i]);
} else {
this.contentItems[i].element.width(sizeData.itemSizes[i]);
this.contentItems[i].element.height(sizeData.totalHeight);
}
}
}
/**
* Calculates the absolute sizes of all of the children of this Item.
* @returns {object} - Set with absolute sizes and additional pixels.
*/
_calculateAbsoluteSizes() {
var _this$element$width, _this$element$height;
var totalSplitterSize = (this.contentItems.length - 1) * this._splitterSize;
var totalWidth = (_this$element$width = this.element.width()) !== null && _this$element$width !== void 0 ? _this$element$width : 0;
var totalHeight = (_this$element$height = this.element.height()) !== null && _this$element$height !== void 0 ? _this$element$height : 0;
var totalAssigned = 0;
var itemSizes = [];
if (this._isColumn) {
totalHeight -= totalSplitterSize;
} else {
totalWidth -= totalSplitterSize;
}
for (var i = 0; i < this.contentItems.length; i++) {
var _this$contentItems$i$3, _this$contentItems$i$4;
var itemSize = this._isColumn ? Math.floor(totalHeight * (((_this$contentItems$i$3 = this.contentItems[i].config.height) !== null && _this$contentItems$i$3 !== void 0 ? _this$contentItems$i$3 : 0) / 100)) : Math.floor(totalWidth * (((_this$contentItems$i$4 = this.contentItems[i].config.width) !== null && _this$contentItems$i$4 !== void 0 ? _this$contentItems$i$4 : 0) / 100));
totalAssigned += itemSize;
itemSizes.push(itemSize);
}
var additionalPixel = Math.floor((this._isColumn ? totalHeight : totalWidth) - totalAssigned);
return {
itemSizes,
additionalPixel,
totalWidth,
totalHeight
};
}
/**
* Calculates the relative sizes of all children of this Item. The logic
* is as follows:
*
* - Add up the total size of all items that have a configured size
*
* - If the total == 100 (check for floating point errors)
* Excellent, job done
*
* - If the total is > 100,
* set the size of items without set dimensions to 1/3 and add this to the total
* set the size off all items so that the total is hundred relative to their original size
*
* - If the total is < 100
* If there are items without set dimensions, distribute the remainder to 100 evenly between them
* If there are no items without set dimensions, increase all items sizes relative to
* their original size so that they add up to 100
*/
_calculateRelativeSizes() {
var total = 0;
var itemsWithoutSetDimension = [];
var dimension = this._isColumn ? 'height' : 'width';
for (var i = 0; i < this.contentItems.length; i++) {
var size = this.contentItems[i].config[dimension];
if (size != null) {
total += size;
} else {
itemsWithoutSetDimension.push(this.contentItems[i]);
}
}
/**
* Everything adds up to hundred, all good :-)
*/
if (Math.round(total) === 100) {
this._respectMinItemWidth();
return;
}
/**
* Allocate the remaining size to the items without a set dimension
*/
if (Math.round(total) < 100 && itemsWithoutSetDimension.length > 0) {
for (var _i = 0; _i < itemsWithoutSetDimension.length; _i++) {
itemsWithoutSetDimension[_i].config[dimension] = (100 - total) / itemsWithoutSetDimension.length;
}
this._respectMinItemWidth();
return;
}
/**
* If the total is > 100, but there are also items without a set dimension left, assing 50
* as their dimension and add it to the total
*
* This will be reset in the next step
*/
if (Math.round(total) > 100) {
for (var _i2 = 0; _i2 < itemsWithoutSetDimension.length; _i2++) {
itemsWithoutSetDimension[_i2].config[dimension] = 50;
total += 50;
}
}
/**
* Set every items size relative to 100 relative to its size to total
*/
for (var _i3 = 0; _i3 < this.contentItems.length; _i3++) {
var _this$contentItems$_i;
this.contentItems[_i3].config[dimension] = ((_this$contentItems$_i = this.contentItems[_i3].config[dimension]) !== null && _this$contentItems$_i !== void 0 ? _this$contentItems$_i : 0) / total * 100;
}
this._respectMinItemWidth();
}
/**
* Adjusts the column widths to respect the dimensions minItemWidth if set.
*/
_respectMinItemWidth() {
var _this$layoutManager$c;
var minItemWidth = this.layoutManager.config.dimensions ? (_this$layoutManager$c = this.layoutManager.config.dimensions.minItemWidth) !== null && _this$layoutManager$c !== void 0 ? _this$layoutManager$c : 0 : 0;
var entriesOverMin = [];
var totalOverMin = 0;
var totalUnderMin = 0;
var remainingWidth = 0;
var allEntries = [];
var entry;
if (this._isColumn || !minItemWidth || this.contentItems.length <= 1) {
return;
}
var sizeData = this._calculateAbsoluteSizes();
/**
* Figure out how much we are under the min item size total and how much room we have to use.
*/
for (var i = 0; i < this.contentItems.length; i++) {
var contentItem = this.contentItems[i];
var itemSize = sizeData.itemSizes[i];
if (itemSize < minItemWidth) {
totalUnderMin += minItemWidth - itemSize;
entry = {
width: minItemWidth
};
} else {
totalOverMin += itemSize - minItemWidth;
entry = {
width: itemSize
};
entriesOverMin.push(entry);
}
allEntries.push(entry);
}
/**
* If there is nothing under min, or there is not enough over to make up the difference, do nothing.
*/
if (totalUnderMin === 0 || totalUnderMin > totalOverMin) {
return;
}
/**
* Evenly reduce all columns that are over the min item width to make up the difference.
*/
var reducePercent = totalUnderMin / totalOverMin;
remainingWidth = totalUnderMin;
for (var _i4 = 0; _i4 < entriesOverMin.length; _i4++) {
entry = entriesOverMin[_i4];
var reducedWidth = Math.round((entry.width - minItemWidth) * reducePercent);
remainingWidth -= reducedWidth;
entry.width -= reducedWidth;
}
/**
* Take anything remaining from the last item.
*/
if (remainingWidth !== 0) {
allEntries[allEntries.length - 1].width -= remainingWidth;
}
/**
* Set every items size relative to 100 relative to its size to total
*/
for (var _i5 = 0; _i5 < this.contentItems.length; _i5++) {
this.contentItems[_i5].config.width = allEntries[_i5].width / sizeData.totalWidth * 100;
}
}
/**
* Instantiates a new lm.controls.Splitter, binds events to it and adds
* it to the array of splitters at the position specified as the index argument
*
* What it doesn't do though is append the splitter to the DOM
*
* @param index The position of the splitter
* @returns The created splitter
*/
_createSplitter(index) {
var splitter;
splitter = new Splitter(this._isColumn, this._splitterSize, this._splitterGrabSize);
splitter.on('drag', this._onSplitterDrag.bind(this, splitter), this);
splitter.on('dragStop', this._onSplitterDragStop.bind(this, splitter), this);
splitter.on('dragStart', this._onSplitterDragStart.bind(this, splitter), this);
this._splitter.splice(index, 0, splitter);
return splitter;
}
/**
* Locates the instance of lm.controls.Splitter in the array of
* registered splitters and returns a map containing the contentItem
* before and after the splitters, both of which are affected if the
* splitter is moved
*
* @param splitter
*
* @returns A map of contentItems that the splitter affects
*/
_getItemsForSplitter(splitter) {
var index = this._splitter.indexOf(splitter);
if (index < 0) {
throw new Error('Splitter not found in RowOrColumn');
}
return {
before: this.contentItems[index],
after: this.contentItems[index + 1]
};
}
/**
* Gets the minimum dimensions for the given item configuration array
* @param item
* @private
*/
_getMinimumDimensions(arr) {
var minWidth = 0,
minHeight = 0;
for (var i = 0; i < arr.length; ++i) {
var _arr$i$minWidth, _arr$i$minHeight;
minWidth = Math.max((_arr$i$minWidth = arr[i].minWidth) !== null && _arr$i$minWidth !== void 0 ? _arr$i$minWidth : 0, minWidth);
minHeight = Math.max((_arr$i$minHeight = arr[i].minHeight) !== null && _arr$i$minHeight !== void 0 ? _arr$i$minHeight : 0, minHeight);
}
return {
horizontal: minWidth,
vertical: minHeight
};
}
/**
* Invoked when a splitter's dragListener fires dragStart. Calculates the splitters
* movement area once (so that it doesn't need calculating on every mousemove event)
*
* @param splitter
*/
_onSplitterDragStart(splitter) {
var _items$before$config$, _items$after$config$c, _items$before$element, _items$after$element$;
var items = this._getItemsForSplitter(splitter);
var minSize = this.layoutManager.config.dimensions[this._isColumn ? 'minItemHeight' : 'minItemWidth'];
var beforeMinDim = this._getMinimumDimensions((_items$before$config$ = items.before.config.content) !== null && _items$before$config$ !== void 0 ? _items$before$config$ : []);
var beforeMinSize = this._isColumn ? beforeMinDim.vertical : beforeMinDim.horizontal;
var afterMinDim = this._getMinimumDimensions((_items$after$config$c = items.after.config.content) !== null && _items$after$config$c !== void 0 ? _items$after$config$c : []);
var afterMinSize = this._isColumn ? afterMinDim.vertical : afterMinDim.horizontal;
this._splitterPosition = 0;
this._splitterMinPosition = -1 * (((_items$before$element = items.before.element[this._dimension]()) !== null && _items$before$element !== void 0 ? _items$before$element : 0) - (beforeMinSize || minSize));
this._splitterMaxPosition = ((_items$after$element$ = items.after.element[this._dimension]()) !== null && _items$after$element$ !== void 0 ? _items$after$element$ : 0) - (afterMinSize || minSize);
}
/**
* Invoked when a splitter's DragListener fires drag. Updates the splitters DOM position,
* but not the sizes of the elements the splitter controls in order to minimize resize events
*
* @param splitter
* @param offsetX Relative pixel values to the splitters original position. Can be negative
* @param offsetY Relative pixel values to the splitters original position. Can be negative
*/
_onSplitterDrag(splitter, offsetX, offsetY) {
var offset = this._isColumn ? offsetY : offsetX;
if (this._splitterMaxPosition == null || this._splitterMinPosition == null) {
return;
}
if (offset > this._splitterMinPosition && offset < this._splitterMaxPosition) {
this._splitterPosition = offset;
splitter.element.css(this._isColumn ? 'top' : 'left', offset);
}
}
/**
* Invoked when a splitter's DragListener fires dragStop. Resets the splitters DOM position,
* and applies the new sizes to the elements before and after the splitter and their children
* on the next animation frame
*
* @param {lm.controls.Splitter} splitter
*/
_onSplitterDragStop(splitter) {
var _items$before$element2, _items$after$element$2, _this$_splitterPositi, _items$before$config$2, _items$after$config$t;
var items = this._getItemsForSplitter(splitter);
var sizeBefore = (_items$before$element2 = items.before.element[this._dimension]()) !== null && _items$before$element2 !== void 0 ? _items$before$element2 : 0;
var sizeAfter = (_items$after$element$2 = items.after.element[this._dimension]()) !== null && _items$after$element$2 !== void 0 ? _items$after$element$2 : 0;
var splitterPositionInRange = (((_this$_splitterPositi = this._splitterPosition) !== null && _this$_splitterPositi !== void 0 ? _this$_splitterPositi : 0) + sizeBefore) / (sizeBefore + sizeAfter);
var totalRelativeSize = ((_items$before$config$2 = items.before.config[this._dimension]) !== null && _items$before$config$2 !== void 0 ? _items$before$config$2 : 0) + ((_items$after$config$t = items.after.config[this._dimension]) !== null && _items$after$config$t !== void 0 ? _items$after$config$t : 0);
items.before.config[this._dimension] = splitterPositionInRange * totalRelativeSize;
items.after.config[this._dimension] = (1 - splitterPositionInRange) * totalRelativeSize;
splitter.element.css({
top: 0,
left: 0
});
animFrame(this.callDownwards.bind(this, 'setSize', undefined, undefined, undefined));
}
}
//# sourceMappingURL=RowOrColumn.js.map