@deephaven/golden-layout
Version:
A multi-screen javascript Layout manager
565 lines (529 loc) • 18 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 { animFrame, BubblingEvent, EventEmitter } from "../utils/index.js";
import { ConfigurationError } from "../errors/index.js";
import { itemDefaultConfig } from "../config/index.js";
export function isStack(item) {
return item.isStack;
}
export function isComponent(item) {
return item.isComponent;
}
export function isRoot(item) {
return item.isRoot;
}
/**
* This is the baseclass that all content items inherit from.
* Most methods provide a subset of what the sub-classes do.
*
* It also provides a number of functions for tree traversal
*
* @param {lm.LayoutManager} layoutManager
* @param {item node configuration} config
* @param {lm.item} parent
*
* @event stateChanged
* @event beforeItemDestroyed
* @event itemDestroyed
* @event itemCreated
* @event componentCreated
* @event rowCreated
* @event columnCreated
* @event stackCreated
*
* @constructor
*/
export default class AbstractContentItem extends EventEmitter {
constructor(layoutManager, config, parent, element) {
super();
_defineProperty(this, "config", void 0);
_defineProperty(this, "type", void 0);
_defineProperty(this, "contentItems", void 0);
_defineProperty(this, "parent", void 0);
_defineProperty(this, "layoutManager", void 0);
_defineProperty(this, "element", void 0);
_defineProperty(this, "childElementContainer", void 0);
_defineProperty(this, "componentName", void 0);
_defineProperty(this, "isInitialised", false);
_defineProperty(this, "isMaximised", false);
_defineProperty(this, "isRoot", false);
_defineProperty(this, "isRow", false);
_defineProperty(this, "isColumn", false);
_defineProperty(this, "isStack", false);
_defineProperty(this, "isComponent", false);
_defineProperty(this, "tab", void 0);
_defineProperty(this, "_pendingEventPropagations", void 0);
_defineProperty(this, "_throttledEvents", void 0);
this.element = element;
// Some GL things expect this config to not change
this.config = this._extendItemNode(config);
this.type = config.type;
this.contentItems = [];
this.parent = parent;
this.layoutManager = layoutManager;
this._pendingEventPropagations = {};
this._throttledEvents = ['stateChanged'];
this.on(EventEmitter.ALL_EVENT, this._propagateEvent, this);
if (config.content) {
this._createContentItems(config);
}
}
/**
* Set the size of the component and its children, called recursively
*
* @abstract
*/
/**
* Calls a method recursively downwards on the tree
*
* @param functionName the name of the function to be called
* @param functionArguments optional arguments that are passed to every function
* @param bottomUp Call methods from bottom to top, defaults to false
* @param skipSelf Don't invoke the method on the class that calls it, defaults to false
*/
callDownwards(functionName) {
var functionArguments = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
var bottomUp = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
var skipSelf = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
if (bottomUp !== true && skipSelf !== true) {
this[functionName].apply(this, functionArguments);
}
for (var i = 0; i < this.contentItems.length; i++) {
this.contentItems[i].callDownwards(functionName, functionArguments, bottomUp);
}
if (bottomUp === true && skipSelf !== true) {
this[functionName].apply(this, functionArguments);
}
}
/**
* Removes a child node (and its children) from the tree
*
* @param contentItem
*/
removeChild(contentItem) {
var _this$config$content;
var keepChild = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
/*
* Get the position of the item that's to be removed within all content items this node contains
*/
var index = this.contentItems.indexOf(contentItem);
/*
* Make sure the content item to be removed is actually a child of this item
*/
if (index === -1) {
throw new Error("Can't remove child item. Unknown content item");
}
/**
* Call ._$destroy on the content item. This also calls ._$destroy on all its children
*/
if (keepChild !== true) {
this.contentItems[index]._$destroy();
}
/**
* Remove the content item from this nodes array of children
*/
this.contentItems.splice(index, 1);
/**
* Remove the item from the configuration
*/
(_this$config$content = this.config.content) === null || _this$config$content === void 0 ? void 0 : _this$config$content.splice(index, 1);
/**
* If this node still contains other content items, adjust their size
*/
if (this.contentItems.length > 0) {
this.callDownwards('setSize');
/**
* If this was the last content item, remove this node as well
*/
} else if (this.type !== 'root' && this.config.isClosable) {
var _this$parent;
(_this$parent = this.parent) === null || _this$parent === void 0 ? void 0 : _this$parent.removeChild(this);
}
}
/**
* Sets up the tree structure for the newly added child
* The responsibility for the actual DOM manipulations lies
* with the concrete item
*
* @param contentItem
* @param index If omitted item will be appended
*/
addChild(contentItem, index) {
contentItem = this.layoutManager._$normalizeContentItem(contentItem, this);
if (index === undefined) {
index = this.contentItems.length;
}
this.contentItems.splice(index, 0, contentItem);
if (this.config.content === undefined) {
this.config.content = [];
}
this.config.content.splice(index, 0, contentItem.config);
contentItem.parent = this;
if (contentItem.parent.isInitialised === true && contentItem.isInitialised === false) {
contentItem._$init();
}
}
/**
* Replaces oldChild with newChild. This used to use jQuery.replaceWith... which for
* some reason removes all event listeners, so isn't really an option.
*
* @param oldChild
* @param newChild
*/
replaceChild(oldChild, newChild) {
var _$destroyOldChild = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
newChild = this.layoutManager._$normalizeContentItem(newChild);
var index = this.contentItems.indexOf(oldChild);
var parentNode = oldChild.element[0].parentNode;
if (index === -1) {
throw new Error("Can't replace child. oldChild is not child of this");
}
parentNode === null || parentNode === void 0 ? void 0 : parentNode.replaceChild(newChild.element[0], oldChild.element[0]);
/*
* Optionally destroy the old content item
*/
if (_$destroyOldChild === true) {
oldChild.parent = null;
oldChild._$destroy();
}
/*
* Wire the new contentItem into the tree
*/
this.contentItems[index] = newChild;
newChild.parent = this;
/*
* Update tab reference
*/
if (isStack(this)) {
this.header.tabs[index].contentItem = newChild;
}
//TODO This doesn't update the config... refactor to leave item nodes untouched after creation
if (newChild.parent.isInitialised === true && newChild.isInitialised === false) {
newChild._$init();
}
this.callDownwards('setSize');
}
/**
* Convenience method.
* Shorthand for this.parent.removeChild( this )
*/
remove() {
var _this$parent2;
(_this$parent2 = this.parent) === null || _this$parent2 === void 0 ? void 0 : _this$parent2.removeChild(this);
}
/**
* Removes the component from the layout and creates a new
* browser window with the component and its children inside
*/
popout() {
var browserPopout = this.layoutManager.createPopout(this);
this.emitBubblingEvent('stateChanged');
return browserPopout;
}
/**
* Maximises the Item or minimises it if it is already maximised
*/
toggleMaximise(e) {
e && e.preventDefault();
if (this.isMaximised === true) {
this.layoutManager._$minimiseItem(this);
} else {
this.layoutManager._$maximiseItem(this);
}
this.isMaximised = !this.isMaximised;
this.emitBubblingEvent('stateChanged');
}
/**
* Selects the item if it is not already selected
*/
select() {
if (this.layoutManager.selectedItem !== this) {
this.layoutManager.selectItem(this, true);
this.element.addClass('lm_selected');
}
}
/**
* De-selects the item if it is selected
*/
deselect() {
if (this.layoutManager.selectedItem === this) {
this.layoutManager.selectedItem = null;
this.element.removeClass('lm_selected');
}
}
/**
* Set this component's title
* @param title
*/
setTitle(title) {
this.config.title = title;
this.emit('titleChanged', title);
this.emitBubblingEvent('stateChanged');
}
/**
* Checks whether a provided id is present
* @param id
* @returns isPresent
*/
hasId(id) {
if (!this.config.id) {
return false;
} else if (typeof this.config.id === 'string') {
return this.config.id === id;
} else if (this.config.id instanceof Array) {
return this.config.id.indexOf(id) !== -1;
}
}
/**
* Adds an id. Adds it as a string if the component doesn't
* have an id yet or creates/uses an array
* @param id
*/
addId(id) {
if (this.hasId(id)) {
return;
}
if (!this.config.id) {
this.config.id = id;
} else if (typeof this.config.id === 'string') {
this.config.id = [this.config.id, id];
} else if (this.config.id instanceof Array) {
this.config.id.push(id);
}
}
/**
* Removes an existing id. Throws an error
* if the id is not present
* @param id
*/
removeId(id) {
if (!this.hasId(id)) {
throw new Error('Id not found');
}
if (typeof this.config.id === 'string') {
delete this.config.id;
} else if (this.config.id instanceof Array) {
var index = this.config.id.indexOf(id);
this.config.id.splice(index, 1);
}
}
/****************************************
* SELECTOR
****************************************/
getItemsByFilter(filter) {
var result = [];
var next = function next(contentItem) {
for (var i = 0; i < contentItem.contentItems.length; i++) {
if (filter(contentItem.contentItems[i]) === true) {
result.push(contentItem.contentItems[i]);
}
next(contentItem.contentItems[i]);
}
};
next(this);
return result;
}
getItemsById(id) {
return this.getItemsByFilter(function (item) {
if (item.config.id instanceof Array) {
return item.config.id.indexOf(id) !== -1;
} else {
return item.config.id === id;
}
});
}
getItemsByType(type) {
return this._$getItemsByProperty('type', type);
}
getComponentsByName(componentName) {
var components = this._$getItemsByProperty('componentName', componentName);
var instances = [];
for (var i = 0; i < components.length; i++) {
instances.push(components[i].instance);
}
return instances;
}
/****************************************
* PACKAGE PRIVATE
****************************************/
_$getItemsByProperty(key, value) {
return this.getItemsByFilter(function (item) {
return item[key] === value;
});
}
_$setParent(parent) {
this.parent = parent;
}
_$highlightDropZone(x, y, area) {
var _this$layoutManager$d;
(_this$layoutManager$d = this.layoutManager.dropTargetIndicator) === null || _this$layoutManager$d === void 0 ? void 0 : _this$layoutManager$d.highlightArea(area);
}
_$onDrop(contentItem, area) {
this.addChild(contentItem);
}
_$hide() {
this._callOnActiveComponents('hide');
this.element.hide();
this.layoutManager.updateSize();
}
_$show() {
this._callOnActiveComponents('show');
this.element.show();
this.layoutManager.updateSize();
}
_callOnActiveComponents(methodName) {
var stacks = this.getItemsByType('stack');
var activeContentItem = null;
for (var i = 0; i < stacks.length; i++) {
activeContentItem = stacks[i].getActiveContentItem();
if (activeContentItem && isComponent(activeContentItem)) {
activeContentItem.container[methodName]();
}
}
}
/**
* Destroys this item ands its children
*/
_$destroy() {
this.emitBubblingEvent('beforeItemDestroyed');
this.callDownwards('_$destroy', [], true, true);
this.element.remove();
this.emitBubblingEvent('itemDestroyed');
}
/**
* Returns the area the component currently occupies in the format
*
* {
* x1: int
* x2: int
* y1: int
* y2: int
* contentItem: contentItem
* }
*/
_$getArea(element) {
var _element$offset, _element$width, _element$height;
element = element || this.element;
var offset = (_element$offset = element.offset()) !== null && _element$offset !== void 0 ? _element$offset : {
left: 0,
top: 0
};
var width = (_element$width = element.width()) !== null && _element$width !== void 0 ? _element$width : 0;
var height = (_element$height = element.height()) !== null && _element$height !== void 0 ? _element$height : 0;
return {
x1: offset.left,
y1: offset.top,
x2: offset.left + width,
y2: offset.top + height,
surface: width * height,
contentItem: this,
side: ''
};
}
/**
* The tree of content items is created in two steps: First all content items are instantiated,
* then init is called recursively from top to bottem. This is the basic init function,
* it can be used, extended or overwritten by the content items
*
* Its behaviour depends on the content item
*/
_$init() {
this.setSize();
for (var i = 0; i < this.contentItems.length; i++) {
var _this$childElementCon;
(_this$childElementCon = this.childElementContainer) === null || _this$childElementCon === void 0 ? void 0 : _this$childElementCon.append(this.contentItems[i].element);
}
this.isInitialised = true;
this.emitBubblingEvent('itemCreated');
this.emitBubblingEvent(this.type + 'Created');
}
/**
* Emit an event that bubbles up the item tree.
*
* @param name The name of the event
*/
emitBubblingEvent(name) {
var event = new BubblingEvent(name, this);
this.emit(name, event);
}
/**
* Private method, creates all content items for this node at initialisation time
* PLEASE NOTE, please see addChild for adding contentItems add runtime
* @param {configuration item node} config
*/
_createContentItems(config) {
var oContentItem;
if (!(config.content instanceof Array)) {
throw new ConfigurationError('content must be an Array', config);
}
for (var i = 0; i < config.content.length; i++) {
oContentItem = this.layoutManager.createContentItem(config.content[i], this);
this.contentItems.push(oContentItem);
}
}
/**
* Extends an item configuration node with default settings
* @param config
* @returns extended config
*/
_extendItemNode(config) {
for (var [key, value] of Object.entries(itemDefaultConfig)) {
// This just appeases TS
var k = key;
if (config[k] === undefined) {
config[k] = value;
}
}
return config;
}
/**
* Called for every event on the item tree. Decides whether the event is a bubbling
* event and propagates it to its parent
*
* @param name the name of the event
* @param event
*/
_propagateEvent(name, event) {
if (event instanceof BubblingEvent && event.isPropagationStopped === false && this.isInitialised === true) {
/**
* In some cases (e.g. if an element is created from a DragSource) it
* doesn't have a parent and is not below root. If that's the case
* propagate the bubbling event from the top level of the substree directly
* to the layoutManager
*/
if (this.isRoot === false && this.parent) {
this.parent.emit.apply(this.parent, [name, event]);
} else {
this._scheduleEventPropagationToLayoutManager(name, event);
}
}
}
/**
* All raw events bubble up to the root element. Some events that
* are propagated to - and emitted by - the layoutManager however are
* only string-based, batched and sanitized to make them more usable
*
* @param name the name of the event
*/
_scheduleEventPropagationToLayoutManager(name, event) {
if (this._throttledEvents.indexOf(name) === -1) {
this.layoutManager.emit(name, event.origin);
} else {
if (this._pendingEventPropagations[name] !== true) {
this._pendingEventPropagations[name] = true;
animFrame(this._propagateEventToLayoutManager.bind(this, name, event));
}
}
}
/**
* Callback for events scheduled by _scheduleEventPropagationToLayoutManager
*
* @param name the name of the event
*/
_propagateEventToLayoutManager(name, event) {
this._pendingEventPropagations[name] = false;
this.layoutManager.emit(name, event);
}
}
//# sourceMappingURL=AbstractContentItem.js.map