UNPKG

golden-layout

Version:

A multi-screen javascript Layout manager

393 lines 14.2 kB
import { AssertError, UnexpectedNullError } from '../errors/internal-error'; import { EventEmitter } from '../utils/event-emitter'; import { getJQueryOffset } from '../utils/jquery-legacy'; import { getUniqueId, setElementDisplayVisibility } from '../utils/utils'; /** * 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 * @public */ export class ContentItem extends EventEmitter { /** @internal */ constructor(layoutManager, config, /** @internal */ _parent, /** @internal */ _element) { super(); this.layoutManager = layoutManager; this._parent = _parent; this._element = _element; /** @internal */ this._popInParentIds = []; this._type = config.type; this._id = config.id; this._isInitialised = false; this.isGround = false; this.isRow = false; this.isColumn = false; this.isStack = false; this.isComponent = false; this.width = config.width; this.minWidth = config.minWidth; this.height = config.height; this.minHeight = config.minHeight; this._isClosable = config.isClosable; this._pendingEventPropagations = {}; this._throttledEvents = ['stateChanged']; this._contentItems = this.createContentItems(config.content); } get type() { return this._type; } get id() { return this._id; } /** @internal */ get popInParentIds() { return this._popInParentIds; } get parent() { return this._parent; } get contentItems() { return this._contentItems; } get isClosable() { return this._isClosable; } get element() { return this._element; } get isInitialised() { return this._isInitialised; } static isStack(item) { return item.isStack; } static isComponentItem(item) { return item.isComponent; } static isComponentParentableItem(item) { return item.isStack || item.isGround; } /** * Removes a child node (and its children) from the tree */ removeChild(contentItem, keepChild = false) { /* * Get the position of the item that's to be removed within all content items this node contains */ const 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. * All children are destroyed as well */ if (!keepChild) { this._contentItems[index].destroy(); } /** * Remove the content item from this nodes array of children */ this._contentItems.splice(index, 1); /** * If this node still contains other content items, adjust their size */ if (this._contentItems.length > 0) { this.updateSize(); } else { /** * If this was the last content item, remove this node as well */ if (!this.isGround && this._isClosable === true) { if (this._parent === null) { throw new UnexpectedNullError('CIUC00874'); } else { 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 * @param suspendResize - Used by descendent implementations */ // eslint-disable-next-line @typescript-eslint/no-unused-vars addChild(contentItem, index, suspendResize) { index !== null && index !== void 0 ? index : (index = this._contentItems.length); this._contentItems.splice(index, 0, contentItem); contentItem.setParent(this); if (this._isInitialised === true && contentItem._isInitialised === false) { contentItem.init(); } return index; } /** * Replaces oldChild with newChild * @param oldChild - * @param newChild - * @internal */ replaceChild(oldChild, newChild, destroyOldChild = false) { // Do not try to replace ComponentItem - will not work const index = this._contentItems.indexOf(oldChild); const parentNode = oldChild._element.parentNode; if (index === -1) { throw new AssertError('CIRCI23232', 'Can\'t replace child. oldChild is not child of this'); } if (parentNode === null) { throw new UnexpectedNullError('CIRCP23232'); } else { parentNode.replaceChild(newChild._element, oldChild._element); /* * Optionally destroy the old content item */ if (destroyOldChild === true) { oldChild._parent = null; oldChild.destroy(); // will now also destroy all children of oldChild } /* * Wire the new contentItem into the tree */ this._contentItems[index] = newChild; newChild.setParent(this); //TODO This doesn't update the config... refactor to leave item nodes untouched after creation if (newChild._parent === null) { throw new UnexpectedNullError('CIRCNC45699'); } else { if (newChild._parent._isInitialised === true && newChild._isInitialised === false) { newChild.init(); } this.updateSize(); } } } /** * Convenience method. * Shorthand for this.parent.removeChild( this ) */ remove() { if (this._parent === null) { throw new UnexpectedNullError('CIR11110'); } else { this._parent.removeChild(this); } } /** * Removes the component from the layout and creates a new * browser window with the component and its children inside */ popout() { const parentId = getUniqueId(); const browserPopout = this.layoutManager.createPopoutFromContentItem(this, undefined, parentId, undefined); this.emitBaseBubblingEvent('stateChanged'); return browserPopout; } /** @internal */ calculateConfigContent() { const contentItems = this._contentItems; const count = contentItems.length; const result = new Array(count); for (let i = 0; i < count; i++) { const item = contentItems[i]; result[i] = item.toConfig(); } return result; } /** @internal */ highlightDropZone(x, y, area) { const dropTargetIndicator = this.layoutManager.dropTargetIndicator; if (dropTargetIndicator === null) { throw new UnexpectedNullError('ACIHDZ5593'); } else { dropTargetIndicator.highlightArea(area); } } /** @internal */ // eslint-disable-next-line @typescript-eslint/no-unused-vars onDrop(contentItem, area) { this.addChild(contentItem); } /** @internal */ show() { // Not sure why showAllActiveContentItems() was called. GoldenLayout seems to work fine without it. Left commented code // in source in case a reason for it becomes apparent. // this.layoutManager.showAllActiveContentItems(); setElementDisplayVisibility(this._element, true); this.layoutManager.updateSizeFromContainer(); for (let i = 0; i < this._contentItems.length; i++) { this._contentItems[i].show(); } } /** * Destroys this item ands its children * @internal */ destroy() { for (let i = 0; i < this._contentItems.length; i++) { this._contentItems[i].destroy(); } this._contentItems = []; this.emitBaseBubblingEvent('beforeItemDestroyed'); this._element.remove(); this.emitBaseBubblingEvent('itemDestroyed'); } /** * Returns the area the component currently occupies * @internal */ getElementArea(element) { element = element !== null && element !== void 0 ? element : this._element; const offset = getJQueryOffset(element); const width = element.offsetWidth; const height = element.offsetHeight; // const widthAndHeight = getJQueryWidthAndHeight(element); return { x1: offset.left + 1, y1: offset.top + 1, x2: offset.left + width - 1, y2: offset.top + height - 1, surface: width * height, contentItem: this }; } /** * 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 * @internal */ init() { this._isInitialised = true; this.emitBaseBubblingEvent('itemCreated'); this.emitUnknownBubblingEvent(this.type + 'Created'); } /** @internal */ setParent(parent) { this._parent = parent; } /** @internal */ addPopInParentId(id) { if (!this.popInParentIds.includes(id)) { this.popInParentIds.push(id); } } /** @internal */ initContentItems() { for (let i = 0; i < this._contentItems.length; i++) { this._contentItems[i].init(); } } /** @internal */ hide() { // Not sure why hideAllActiveContentItems() was called. GoldenLayout seems to work fine without it. Left commented code // in source in case a reason for it becomes apparent. // this.layoutManager.hideAllActiveContentItems(); setElementDisplayVisibility(this._element, false); this.layoutManager.updateSizeFromContainer(); } /** @internal */ updateContentItemsSize() { for (let i = 0; i < this._contentItems.length; i++) { this._contentItems[i].updateSize(); } } /** * creates all content items for this node at initialisation time * PLEASE NOTE, please see addChild for adding contentItems at runtime * @internal */ createContentItems(content) { const count = content.length; const result = new Array(count); for (let i = 0; i < content.length; i++) { result[i] = this.layoutManager.createContentItem(content[i], this); } return result; } /** * 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 - * @internal */ propagateEvent(name, args) { if (args.length === 1) { const event = args[0]; if (event instanceof EventEmitter.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 a child of GroundItem. If that's the case * propagate the bubbling event from the top level of the substree directly * to the layoutManager */ if (this.isGround === false && this._parent) { this._parent.emitUnknown(name, event); } else { this.scheduleEventPropagationToLayoutManager(name, event); } } } } tryBubbleEvent(name, args) { if (args.length === 1) { const event = args[0]; if (event instanceof EventEmitter.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 a child of GroundItem. If that's the case * propagate the bubbling event from the top level of the substree directly * to the layoutManager */ if (this.isGround === false && this._parent) { this._parent.emitUnknown(name, event); } else { this.scheduleEventPropagationToLayoutManager(name, event); } } } } /** * All raw events bubble up to the Ground 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 * @internal */ scheduleEventPropagationToLayoutManager(name, event) { if (this._throttledEvents.indexOf(name) === -1) { this.layoutManager.emitUnknown(name, event); } else { if (this._pendingEventPropagations[name] !== true) { this._pendingEventPropagations[name] = true; globalThis.requestAnimationFrame(() => this.propagateEventToLayoutManager(name, event)); } } } /** * Callback for events scheduled by _scheduleEventPropagationToLayoutManager * * @param name - The name of the event * @internal */ propagateEventToLayoutManager(name, event) { this._pendingEventPropagations[name] = false; this.layoutManager.emitUnknown(name, event); } } //# sourceMappingURL=content-item.js.map