UNPKG

golden-layout

Version:
1,169 lines 67.4 kB
import { ItemConfig, LayoutConfig } from './config/config'; import { ResolvedItemConfig, ResolvedLayoutConfig, ResolvedRootItemConfig } from "./config/resolved-config"; import { BrowserPopout } from './controls/browser-popout'; import { DragProxy } from './controls/drag-proxy'; import { DragSource } from './controls/drag-source'; import { DropTargetIndicator } from './controls/drop-target-indicator'; import { TransitionIndicator } from './controls/transition-indicator'; import { ConfigurationError } from './errors/external-error'; import { AssertError, UnexpectedNullError, UnexpectedUndefinedError, UnreachableCaseError } from './errors/internal-error'; import { ComponentItem } from './items/component-item'; import { ContentItem } from './items/content-item'; import { GroundItem } from './items/ground-item'; import { RowOrColumn } from './items/row-or-column'; import { Stack } from './items/stack'; import { ConfigMinifier } from './utils/config-minifier'; import { EventEmitter } from './utils/event-emitter'; import { EventHub } from './utils/event-hub'; import { I18nStrings, i18nStrings } from './utils/i18n-strings'; import { ItemType, ResponsiveMode } from './utils/types'; import { getElementWidthAndHeight, removeFromArray, setElementHeight, setElementWidth } from './utils/utils'; /** * The main class that will be exposed as GoldenLayout. */ /** @public */ export class LayoutManager extends EventEmitter { /** * @param container - A Dom HTML element. Defaults to body * @internal */ constructor(parameters) { super(); /** Whether the layout will be automatically be resized to container whenever the container's size is changed * Default is true if <body> is the container otherwise false * Default will be changed to true for any container in the future */ this.resizeWithContainerAutomatically = false; /** The debounce interval (in milliseconds) used whenever a layout is automatically resized. 0 means next tick */ this.resizeDebounceInterval = 100; /** Extend the current debounce delay time period if it is triggered during the delay. * If this is true, the layout will only resize when its container has stopped being resized. * If it is false, the layout will resize at intervals while its container is being resized. */ this.resizeDebounceExtendedWhenPossible = true; /** @internal */ this._isInitialised = false; /** @internal */ this._groundItem = undefined; /** @internal */ this._openPopouts = []; /** @internal */ this._dropTargetIndicator = null; /** @internal */ this._transitionIndicator = null; /** @internal */ this._itemAreas = []; /** @internal */ this._maximisePlaceholder = LayoutManager.createMaximisePlaceElement(document); /** @internal */ this._tabDropPlaceholder = LayoutManager.createTabDropPlaceholderElement(document); /** @internal */ this._dragSources = []; /** @internal */ this._updatingColumnsResponsive = false; /** @internal */ this._firstLoad = true; /** @internal */ this._eventHub = new EventHub(this); /** @internal */ this._width = null; /** @internal */ this._height = null; /** @internal */ this._virtualSizedContainers = []; /** @internal */ this._virtualSizedContainerAddingBeginCount = 0; /** @internal */ this._sizeInvalidationBeginCount = 0; /** @internal */ this._resizeObserver = new ResizeObserver(() => this.handleContainerResize()); /** @internal @deprecated to be removed in version 3 */ this._windowBeforeUnloadListener = () => this.onBeforeUnload(); /** @internal @deprecated to be removed in version 3 */ this._windowBeforeUnloadListening = false; /** @internal */ this._maximisedStackBeforeDestroyedListener = (ev) => this.cleanupBeforeMaximisedStackDestroyed(ev); this.isSubWindow = parameters.isSubWindow; this._constructorOrSubWindowLayoutConfig = parameters.constructorOrSubWindowLayoutConfig; I18nStrings.checkInitialise(); ConfigMinifier.checkInitialise(); if (parameters.containerElement !== undefined) { this._containerElement = parameters.containerElement; } } get container() { return this._containerElement; } get isInitialised() { return this._isInitialised; } /** @internal */ get groundItem() { return this._groundItem; } /** @internal @deprecated use {@link (LayoutManager:class).groundItem} instead */ get root() { return this._groundItem; } get openPopouts() { return this._openPopouts; } /** @internal */ get dropTargetIndicator() { return this._dropTargetIndicator; } /** @internal @deprecated To be removed */ get transitionIndicator() { return this._transitionIndicator; } get width() { return this._width; } get height() { return this._height; } /** * Retrieves the {@link (EventHub:class)} instance associated with this layout manager. * This can be used to propagate events between the windows * @public */ get eventHub() { return this._eventHub; } get rootItem() { if (this._groundItem === undefined) { throw new Error('Cannot access rootItem before init'); } else { const groundContentItems = this._groundItem.contentItems; if (groundContentItems.length === 0) { return undefined; } else { return this._groundItem.contentItems[0]; } } } get focusedComponentItem() { return this._focusedComponentItem; } /** @internal */ get tabDropPlaceholder() { return this._tabDropPlaceholder; } get maximisedStack() { return this._maximisedStack; } /** @deprecated indicates deprecated constructor use */ get deprecatedConstructor() { return !this.isSubWindow && this._constructorOrSubWindowLayoutConfig !== undefined; } /** * Destroys the LayoutManager instance itself as well as every ContentItem * within it. After this is called nothing should be left of the LayoutManager. * * This function only needs to be called if an application wishes to destroy the Golden Layout object while * a page remains loaded. When a page is unloaded, all resources claimed by Golden Layout will automatically * be released. */ destroy() { if (this._isInitialised) { if (this._windowBeforeUnloadListening) { globalThis.removeEventListener('beforeunload', this._windowBeforeUnloadListener); this._windowBeforeUnloadListening = false; } if (this.layoutConfig.settings.closePopoutsOnUnload === true) { this.closeAllOpenPopouts(); } this._resizeObserver.disconnect(); this.checkClearResizeTimeout(); if (this._groundItem !== undefined) { this._groundItem.destroy(); } this._tabDropPlaceholder.remove(); if (this._dropTargetIndicator !== null) { this._dropTargetIndicator.destroy(); } if (this._transitionIndicator !== null) { this._transitionIndicator.destroy(); } this._eventHub.destroy(); for (const dragSource of this._dragSources) { dragSource.destroy(); } this._dragSources = []; this._isInitialised = false; } } /** * Takes a GoldenLayout configuration object and * replaces its keys and values recursively with * one letter codes * @deprecated use {@link (ResolvedLayoutConfig:namespace).minifyConfig} instead */ minifyConfig(config) { return ResolvedLayoutConfig.minifyConfig(config); } /** * Takes a configuration Object that was previously minified * using minifyConfig and returns its original version * @deprecated use {@link (ResolvedLayoutConfig:namespace).unminifyConfig} instead */ unminifyConfig(config) { return ResolvedLayoutConfig.unminifyConfig(config); } /** * Called from GoldenLayout class. Finishes of init * @internal */ init() { this.setContainer(); this._dropTargetIndicator = new DropTargetIndicator( /*this.container*/); this._transitionIndicator = new TransitionIndicator(); this.updateSizeFromContainer(); let subWindowRootConfig; if (this.isSubWindow) { if (this._constructorOrSubWindowLayoutConfig === undefined) { // SubWindow LayoutConfig should have been generated by constructor throw new UnexpectedUndefinedError('LMIU07155'); } else { const root = this._constructorOrSubWindowLayoutConfig.root; if (root === undefined) { // SubWindow LayoutConfig must not be empty throw new AssertError('LMIC07156'); } else { if (ItemConfig.isComponent(root)) { subWindowRootConfig = root; } else { // SubWindow LayoutConfig must have Component as Root throw new AssertError('LMIC07157'); } } const resolvedLayoutConfig = LayoutConfig.resolve(this._constructorOrSubWindowLayoutConfig); // remove root from layoutConfig this.layoutConfig = Object.assign(Object.assign({}, resolvedLayoutConfig), { root: undefined }); } } else { if (this._constructorOrSubWindowLayoutConfig === undefined) { this.layoutConfig = ResolvedLayoutConfig.createDefault(); // will overwritten be loaded via loadLayout } else { // backwards compatibility this.layoutConfig = LayoutConfig.resolve(this._constructorOrSubWindowLayoutConfig); } } const layoutConfig = this.layoutConfig; this._groundItem = new GroundItem(this, layoutConfig.root, this._containerElement); this._groundItem.init(); this.checkLoadedLayoutMaximiseItem(); this._resizeObserver.observe(this._containerElement); this._isInitialised = true; this.adjustColumnsResponsive(); this.emit('initialised'); if (subWindowRootConfig !== undefined) { // must be SubWindow this.loadComponentAsRoot(subWindowRootConfig); } } /** * Loads a new layout * @param layoutConfig - New layout to be loaded */ loadLayout(layoutConfig) { if (!this.isInitialised) { // In case application not correctly using legacy constructor throw new Error('GoldenLayout: Need to call init() if LayoutConfig with defined root passed to constructor'); } else { if (this._groundItem === undefined) { throw new UnexpectedUndefinedError('LMLL11119'); } else { this.createSubWindows(); // still needs to be tested this.layoutConfig = LayoutConfig.resolve(layoutConfig); this._groundItem.loadRoot(this.layoutConfig.root); this.checkLoadedLayoutMaximiseItem(); this.adjustColumnsResponsive(); } } } /** * Creates a layout configuration object based on the the current state * * @public * @returns GoldenLayout configuration */ saveLayout() { if (this._isInitialised === false) { throw new Error('Can\'t create config, layout not yet initialised'); } else { // if (root !== undefined && !(root instanceof ContentItem)) { // throw new Error('Root must be a ContentItem'); // } /* * Content */ if (this._groundItem === undefined) { throw new UnexpectedUndefinedError('LMTC18244'); } else { const groundContent = this._groundItem.calculateConfigContent(); let rootItemConfig; if (groundContent.length !== 1) { rootItemConfig = undefined; } else { rootItemConfig = groundContent[0]; } /* * Retrieve config for subwindows */ this.reconcilePopoutWindows(); const openPopouts = []; for (let i = 0; i < this._openPopouts.length; i++) { openPopouts.push(this._openPopouts[i].toConfig()); } const config = { root: rootItemConfig, openPopouts, settings: ResolvedLayoutConfig.Settings.createCopy(this.layoutConfig.settings), dimensions: ResolvedLayoutConfig.Dimensions.createCopy(this.layoutConfig.dimensions), header: ResolvedLayoutConfig.Header.createCopy(this.layoutConfig.header), resolved: true, }; return config; } } } /** * Removes any existing layout. Effectively, an empty layout will be loaded. */ clear() { if (this._groundItem === undefined) { throw new UnexpectedUndefinedError('LMCL11129'); } else { this._groundItem.clearRoot(); } } /** * @deprecated Use {@link (LayoutManager:class).saveLayout} */ toConfig() { return this.saveLayout(); } /** * Adds a new ComponentItem. Will use default location selectors to ensure a location is found and * component is successfully added * @param componentTypeName - Name of component type to be created. * @param state - Optional initial state to be assigned to component * @returns New ComponentItem created. */ newComponent(componentType, componentState, title) { const componentItem = this.newComponentAtLocation(componentType, componentState, title); if (componentItem === undefined) { throw new AssertError('LMNC65588'); } else { return componentItem; } } /** * Adds a ComponentItem at the first valid selector location. * @param componentTypeName - Name of component type to be created. * @param state - Optional initial state to be assigned to component * @param locationSelectors - Array of location selectors used to find location in layout where component * will be added. First location in array which is valid will be used. If locationSelectors is undefined, * {@link (LayoutManager:namespace).defaultLocationSelectors} will be used * @returns New ComponentItem created or undefined if no valid location selector was in array. */ newComponentAtLocation(componentType, componentState, title, locationSelectors) { if (this._groundItem === undefined) { throw new Error('Cannot add component before init'); } else { const location = this.addComponentAtLocation(componentType, componentState, title, locationSelectors); if (location === undefined) { return undefined; } else { const createdItem = location.parentItem.contentItems[location.index]; if (!ContentItem.isComponentItem(createdItem)) { throw new AssertError('LMNC992877533'); } else { return createdItem; } } } } /** * Adds a new ComponentItem. Will use default location selectors to ensure a location is found and * component is successfully added * @param componentType - Type of component to be created. * @param state - Optional initial state to be assigned to component * @returns Location of new ComponentItem created. */ addComponent(componentType, componentState, title) { const location = this.addComponentAtLocation(componentType, componentState, title); if (location === undefined) { throw new AssertError('LMAC99943'); } else { return location; } } /** * Adds a ComponentItem at the first valid selector location. * @param componentType - Type of component to be created. * @param state - Optional initial state to be assigned to component * @param locationSelectors - Array of location selectors used to find determine location in layout where component * will be added. First location in array which is valid will be used. If undefined, * {@link (LayoutManager:namespace).defaultLocationSelectors} will be used. * @returns Location of new ComponentItem created or undefined if no valid location selector was in array. */ addComponentAtLocation(componentType, componentState, title, locationSelectors) { const itemConfig = { type: 'component', componentType, componentState, title, }; return this.addItemAtLocation(itemConfig, locationSelectors); } /** * Adds a new ContentItem. Will use default location selectors to ensure a location is found and * component is successfully added * @param itemConfig - ResolvedItemConfig of child to be added. * @returns New ContentItem created. */ newItem(itemConfig) { const contentItem = this.newItemAtLocation(itemConfig); if (contentItem === undefined) { throw new AssertError('LMNC65588'); } else { return contentItem; } } /** * Adds a new child ContentItem under the root ContentItem. If a root does not exist, then create root ContentItem instead * @param itemConfig - ResolvedItemConfig of child to be added. * @param locationSelectors - Array of location selectors used to find determine location in layout where ContentItem * will be added. First location in array which is valid will be used. If undefined, * {@link (LayoutManager:namespace).defaultLocationSelectors} will be used. * @returns New ContentItem created or undefined if no valid location selector was in array. */ newItemAtLocation(itemConfig, locationSelectors) { if (this._groundItem === undefined) { throw new Error('Cannot add component before init'); } else { const location = this.addItemAtLocation(itemConfig, locationSelectors); if (location === undefined) { return undefined; } else { const createdItem = location.parentItem.contentItems[location.index]; return createdItem; } } } /** * Adds a new ContentItem. Will use default location selectors to ensure a location is found and * component is successfully added. * @param itemConfig - ResolvedItemConfig of child to be added. * @returns Location of new ContentItem created. */ addItem(itemConfig) { const location = this.addItemAtLocation(itemConfig); if (location === undefined) { throw new AssertError('LMAI99943'); } else { return location; } } /** * Adds a ContentItem at the first valid selector location. * @param itemConfig - ResolvedItemConfig of child to be added. * @param locationSelectors - Array of location selectors used to find determine location in layout where ContentItem * will be added. First location in array which is valid will be used. If undefined, * {@link (LayoutManager:namespace).defaultLocationSelectors} will be used. * @returns Location of new ContentItem created or undefined if no valid location selector was in array. */ addItemAtLocation(itemConfig, locationSelectors) { if (this._groundItem === undefined) { throw new Error('Cannot add component before init'); } else { if (locationSelectors === undefined) { // defaultLocationSelectors should always find a location locationSelectors = LayoutManager.defaultLocationSelectors; } const location = this.findFirstLocation(locationSelectors); if (location === undefined) { return undefined; } else { let parentItem = location.parentItem; let addIdx; switch (parentItem.type) { case ItemType.ground: { const groundItem = parentItem; addIdx = groundItem.addItem(itemConfig, location.index); if (addIdx >= 0) { parentItem = this._groundItem.contentItems[0]; // was added to rootItem } else { addIdx = 0; // was added as rootItem (which is the first and only ContentItem in GroundItem) } break; } case ItemType.row: case ItemType.column: { const rowOrColumn = parentItem; addIdx = rowOrColumn.addItem(itemConfig, location.index); break; } case ItemType.stack: { if (!ItemConfig.isComponent(itemConfig)) { throw Error(i18nStrings[6 /* ItemConfigIsNotTypeComponent */]); } else { const stack = parentItem; addIdx = stack.addItem(itemConfig, location.index); break; } } case ItemType.component: { throw new AssertError('LMAIALC87444602'); } default: throw new UnreachableCaseError('LMAIALU98881733', parentItem.type); } if (ItemConfig.isComponent(itemConfig)) { // see if stack was inserted const item = parentItem.contentItems[addIdx]; if (ContentItem.isStack(item)) { parentItem = item; addIdx = 0; } } location.parentItem = parentItem; location.index = addIdx; return location; } } } /** Loads the specified component ResolvedItemConfig as root. * This can be used to display a Component all by itself. The layout cannot be changed other than having another new layout loaded. * Note that, if this layout is saved and reloaded, it will reload with the Component as a child of a Stack. */ loadComponentAsRoot(itemConfig) { if (this._groundItem === undefined) { throw new Error('Cannot add item before init'); } else { this._groundItem.loadComponentAsRoot(itemConfig); } } /** @deprecated Use {@link (LayoutManager:class).setSize} */ updateSize(width, height) { this.setSize(width, height); } /** * Updates the layout managers size * * @param width - Width in pixels * @param height - Height in pixels */ setSize(width, height) { this._width = width; this._height = height; if (this._isInitialised === true) { if (this._groundItem === undefined) { throw new UnexpectedUndefinedError('LMUS18881'); } else { this._groundItem.setSize(this._width, this._height); if (this._maximisedStack) { const { width, height } = getElementWidthAndHeight(this._containerElement); setElementWidth(this._maximisedStack.element, width); setElementHeight(this._maximisedStack.element, height); this._maximisedStack.updateSize(false); } this.adjustColumnsResponsive(); } } } /** @internal */ beginSizeInvalidation() { this._sizeInvalidationBeginCount++; } /** @internal */ endSizeInvalidation() { if (--this._sizeInvalidationBeginCount === 0) { this.updateSizeFromContainer(); } } /** @internal */ updateSizeFromContainer() { const { width, height } = getElementWidthAndHeight(this._containerElement); this.setSize(width, height); } /** * Update the size of the root ContentItem. This will update the size of all contentItems in the tree * @param force - In some cases the size is not updated if it has not changed. In this case, events * (such as ComponentContainer.virtualRectingRequiredEvent) are not fired. Setting force to true, ensures the size is updated regardless, and * the respective events are fired. This is sometimes necessary when a component's size has not changed but it has become visible, and the * relevant events need to be fired. */ updateRootSize(force = false) { if (this._groundItem === undefined) { throw new UnexpectedUndefinedError('LMURS28881'); } else { this._groundItem.updateSize(force); } } /** @public */ createAndInitContentItem(config, parent) { const newItem = this.createContentItem(config, parent); newItem.init(); return newItem; } /** * Recursively creates new item tree structures based on a provided * ItemConfiguration object * * @param config - ResolvedItemConfig * @param parent - The item the newly created item should be a child of * @internal */ createContentItem(config, parent) { if (typeof config.type !== 'string') { throw new ConfigurationError('Missing parameter \'type\'', JSON.stringify(config)); } /** * We add an additional stack around every component that's not within a stack anyways. */ if ( // If this is a component ResolvedItemConfig.isComponentItem(config) && // and it's not already within a stack !(parent instanceof Stack) && // and we have a parent !!parent && // and it's not the topmost item in a new window !(this.isSubWindow === true && parent instanceof GroundItem)) { const stackConfig = { type: ItemType.stack, content: [config], size: config.size, sizeUnit: config.sizeUnit, minSize: config.minSize, minSizeUnit: config.minSizeUnit, id: config.id, maximised: config.maximised, isClosable: config.isClosable, activeItemIndex: 0, header: undefined, }; config = stackConfig; } const contentItem = this.createContentItemFromConfig(config, parent); return contentItem; } findFirstComponentItemById(id) { if (this._groundItem === undefined) { throw new UnexpectedUndefinedError('LMFFCIBI82446'); } else { return this.findFirstContentItemTypeByIdRecursive(ItemType.component, id, this._groundItem); } } /** * Creates a popout window with the specified content at the specified position * * @param itemConfigOrContentItem - The content of the popout window's layout manager derived from either * a {@link (ContentItem:class)} or {@link (ItemConfig:interface)} or ResolvedItemConfig content (array of {@link (ItemConfig:interface)}) * @param positionAndSize - The width, height, left and top of Popout window * @param parentId -The id of the element this item will be appended to when popIn is called * @param indexInParent - The position of this item within its parent element */ createPopout(itemConfigOrContentItem, positionAndSize, parentId, indexInParent) { if (itemConfigOrContentItem instanceof ContentItem) { return this.createPopoutFromContentItem(itemConfigOrContentItem, positionAndSize, parentId, indexInParent); } else { return this.createPopoutFromItemConfig(itemConfigOrContentItem, positionAndSize, parentId, indexInParent); } } /** @internal */ createPopoutFromContentItem(item, window, parentId, indexInParent) { /** * If the item is the only component within a stack or for some * other reason the only child of its parent the parent will be destroyed * when the child is removed. * * In order to support this we move up the tree until we find something * that will remain after the item is being popped out */ let parent = item.parent; let child = item; while (parent !== null && parent.contentItems.length === 1 && !parent.isGround) { child = parent; parent = parent.parent; } if (parent === null) { throw new UnexpectedNullError('LMCPFCI00834'); } else { if (indexInParent === undefined) { indexInParent = parent.contentItems.indexOf(child); } if (parentId !== null) { parent.addPopInParentId(parentId); } if (window === undefined) { const windowLeft = globalThis.screenX || globalThis.screenLeft; const windowTop = globalThis.screenY || globalThis.screenTop; const offsetLeft = item.element.offsetLeft; const offsetTop = item.element.offsetTop; // const { left: offsetLeft, top: offsetTop } = getJQueryLeftAndTop(item.element); const { width, height } = getElementWidthAndHeight(item.element); window = { left: windowLeft + offsetLeft, top: windowTop + offsetTop, width, height, }; } const itemConfig = item.toConfig(); item.remove(); if (!ResolvedRootItemConfig.isRootItemConfig(itemConfig)) { throw new Error(`${i18nStrings[0 /* PopoutCannotBeCreatedWithGroundItemConfig */]}`); } else { return this.createPopoutFromItemConfig(itemConfig, window, parentId, indexInParent); } } } /** @internal */ beginVirtualSizedContainerAdding() { if (++this._virtualSizedContainerAddingBeginCount === 0) { this._virtualSizedContainers.length = 0; } } /** @internal */ addVirtualSizedContainer(container) { this._virtualSizedContainers.push(container); } /** @internal */ endVirtualSizedContainerAdding() { if (--this._virtualSizedContainerAddingBeginCount === 0) { const count = this._virtualSizedContainers.length; if (count > 0) { this.fireBeforeVirtualRectingEvent(count); for (let i = 0; i < count; i++) { const container = this._virtualSizedContainers[i]; container.notifyVirtualRectingRequired(); } this.fireAfterVirtualRectingEvent(); this._virtualSizedContainers.length = 0; } } } /** @internal */ fireBeforeVirtualRectingEvent(count) { if (this.beforeVirtualRectingEvent !== undefined) { this.beforeVirtualRectingEvent(count); } } /** @internal */ fireAfterVirtualRectingEvent() { if (this.afterVirtualRectingEvent !== undefined) { this.afterVirtualRectingEvent(); } } /** @internal */ createPopoutFromItemConfig(rootItemConfig, window, parentId, indexInParent) { const layoutConfig = this.toConfig(); const popoutLayoutConfig = { root: rootItemConfig, openPopouts: [], settings: layoutConfig.settings, dimensions: layoutConfig.dimensions, header: layoutConfig.header, window, parentId, indexInParent, resolved: true, }; return this.createPopoutFromPopoutLayoutConfig(popoutLayoutConfig); } /** @internal */ createPopoutFromPopoutLayoutConfig(config) { var _a, _b, _c, _d; const configWindow = config.window; const initialWindow = { left: (_a = configWindow.left) !== null && _a !== void 0 ? _a : (globalThis.screenX || globalThis.screenLeft + 20), top: (_b = configWindow.top) !== null && _b !== void 0 ? _b : (globalThis.screenY || globalThis.screenTop + 20), width: (_c = configWindow.width) !== null && _c !== void 0 ? _c : 500, height: (_d = configWindow.height) !== null && _d !== void 0 ? _d : 309, }; const browserPopout = new BrowserPopout(config, initialWindow, this); browserPopout.on('initialised', () => this.emit('windowOpened', browserPopout)); browserPopout.on('closed', () => this.reconcilePopoutWindows()); this._openPopouts.push(browserPopout); if (this.layoutConfig.settings.closePopoutsOnUnload && !this._windowBeforeUnloadListening) { globalThis.addEventListener('beforeunload', this._windowBeforeUnloadListener, { passive: true }); this._windowBeforeUnloadListening = true; } return browserPopout; } /** * Closes all Open Popouts * Applications can call this method when a page is unloaded to remove its open popouts */ closeAllOpenPopouts() { for (let i = 0; i < this._openPopouts.length; i++) { this._openPopouts[i].close(); } this._openPopouts.length = 0; if (this._windowBeforeUnloadListening) { globalThis.removeEventListener('beforeunload', this._windowBeforeUnloadListener); this._windowBeforeUnloadListening = false; } } newDragSource(element, componentTypeOrItemConfigCallback, componentState, title, id) { const dragSource = new DragSource(this, element, [], componentTypeOrItemConfigCallback, componentState, title, id); this._dragSources.push(dragSource); return dragSource; } /** * Removes a DragListener added by createDragSource() so the corresponding * DOM element is not a drag source any more. */ removeDragSource(dragSource) { removeFromArray(dragSource, this._dragSources); dragSource.destroy(); } /** @internal */ startComponentDrag(x, y, dragListener, componentItem, stack) { new DragProxy(x, y, dragListener, this, componentItem, stack); } /** * Programmatically focuses an item. This focuses the specified component item * and the item emits a focus event * * @param item - The component item to be focused * @param suppressEvent - Whether to emit focus event */ focusComponent(item, suppressEvent = false) { item.focus(suppressEvent); } /** * Programmatically blurs (defocuses) the currently focused component. * If a component item is focused, then it is blurred and and the item emits a blur event * * @param item - The component item to be blurred * @param suppressEvent - Whether to emit blur event */ clearComponentFocus(suppressEvent = false) { this.setFocusedComponentItem(undefined, suppressEvent); } /** * Programmatically focuses a component item or removes focus (blurs) from an existing focused component item. * * @param item - If defined, specifies the component item to be given focus. If undefined, clear component focus. * @param suppressEvents - Whether to emit focus and blur events * @internal */ setFocusedComponentItem(item, suppressEvents = false) { if (item !== this._focusedComponentItem) { let newFocusedParentItem; if (item === undefined) { newFocusedParentItem === undefined; } else { newFocusedParentItem = item.parentItem; } if (this._focusedComponentItem !== undefined) { const oldFocusedItem = this._focusedComponentItem; this._focusedComponentItem = undefined; oldFocusedItem.setBlurred(suppressEvents); const oldFocusedParentItem = oldFocusedItem.parentItem; if (newFocusedParentItem === oldFocusedParentItem) { newFocusedParentItem = undefined; } else { oldFocusedParentItem.setFocusedValue(false); } } if (item !== undefined) { this._focusedComponentItem = item; item.setFocused(suppressEvents); if (newFocusedParentItem !== undefined) { newFocusedParentItem.setFocusedValue(true); } } } } /** @internal */ createContentItemFromConfig(config, parent) { switch (config.type) { case ItemType.ground: throw new AssertError('LMCCIFC68871'); case ItemType.row: return new RowOrColumn(false, this, config, parent); case ItemType.column: return new RowOrColumn(true, this, config, parent); case ItemType.stack: return new Stack(this, config, parent); case ItemType.component: return new ComponentItem(this, config, parent); default: throw new UnreachableCaseError('CCC913564', config.type, 'Invalid Config Item type specified'); } } /** * This should only be called from stack component. * Stack will look after docking processing associated with maximise/minimise * @internal **/ setMaximisedStack(stack) { if (stack === undefined) { if (this._maximisedStack !== undefined) { this.processMinimiseMaximisedStack(); } } else { if (stack !== this._maximisedStack) { if (this._maximisedStack !== undefined) { this.processMinimiseMaximisedStack(); } this.processMaximiseStack(stack); } } } checkMinimiseMaximisedStack() { if (this._maximisedStack !== undefined) { this._maximisedStack.minimise(); } } // showAllActiveContentItems() was called from ContentItem.show(). Not sure what its purpose was so have commented out // Everything seems to work ok without this. Have left commented code just in case there was a reason for it becomes // apparent // /** @internal */ // showAllActiveContentItems(): void { // const allStacks = this.getAllStacks(); // for (let i = 0; i < allStacks.length; i++) { // const stack = allStacks[i]; // const activeContentItem = stack.getActiveComponentItem(); // if (activeContentItem !== undefined) { // if (!(activeContentItem instanceof ComponentItem)) { // throw new AssertError('LMSAACIS22298'); // } else { // activeContentItem.container.show(); // } // } // } // } // hideAllActiveContentItems() was called from ContentItem.hide(). Not sure what its purpose was so have commented out // Everything seems to work ok without this. Have left commented code just in case there was a reason for it becomes // apparent // /** @internal */ // hideAllActiveContentItems(): void { // const allStacks = this.getAllStacks(); // for (let i = 0; i < allStacks.length; i++) { // const stack = allStacks[i]; // const activeContentItem = stack.getActiveComponentItem(); // if (activeContentItem !== undefined) { // if (!(activeContentItem instanceof ComponentItem)) { // throw new AssertError('LMSAACIH22298'); // } else { // activeContentItem.container.hide(); // } // } // } // } /** @internal */ cleanupBeforeMaximisedStackDestroyed(event) { if (this._maximisedStack !== null && this._maximisedStack === event.target) { this._maximisedStack.off('beforeItemDestroyed', this._maximisedStackBeforeDestroyedListener); this._maximisedStack = undefined; } } /** * This method is used to get around sandboxed iframe restrictions. * If 'allow-top-navigation' is not specified in the iframe's 'sandbox' attribute * (as is the case with codepens) the parent window is forbidden from calling certain * methods on the child, such as window.close() or setting document.location.href. * * This prevented GoldenLayout popouts from popping in in codepens. The fix is to call * _$closeWindow on the child window's gl instance which (after a timeout to disconnect * the invoking method from the close call) closes itself. * * @internal */ closeWindow() { globalThis.setTimeout(() => globalThis.close(), 1); } /** @internal */ getArea(x, y) { let matchingArea = null; let smallestSurface = Infinity; for (let i = 0; i < this._itemAreas.length; i++) { const area = this._itemAreas[i]; if (x >= area.x1 && x < area.x2 && // x2 is not included in area y >= area.y1 && y < area.y2 && // y2 is not included in area smallestSurface > area.surface) { smallestSurface = area.surface; matchingArea = area; } } return matchingArea; } /** @internal */ calculateItemAreas() { const allContentItems = this.getAllContentItems(); /** * If the last item is dragged out, highlight the entire container size to * allow to re-drop it. this.ground.contentiItems.length === 0 at this point * * Don't include ground into the possible drop areas though otherwise since it * will used for every gap in the layout, e.g. splitters */ const groundItem = this._groundItem; if (groundItem === undefined) { throw new UnexpectedUndefinedError('LMCIAR44365'); } else { if (allContentItems.length === 1) { // No root ContentItem (just Ground ContentItem) const groundArea = groundItem.getElementArea(); if (groundArea === null) { throw new UnexpectedNullError('LMCIARA44365'); } else { this._itemAreas = [groundArea]; } return; } else { if (groundItem.contentItems[0].isStack) { // if root is Stack, then split stack and sides of Layout are same, so skip sides this._itemAreas = []; } else { // sides of layout this._itemAreas = groundItem.createSideAreas(); } for (let i = 0; i < allContentItems.length; i++) { const stack = allContentItems[i]; if (ContentItem.isStack(stack)) { const area = stack.getArea(); if (area === null) { continue; } else { this._itemAreas.push(area); const stackContentAreaDimensions = stack.contentAreaDimensions; if (stackContentAreaDimensions === undefined) { throw new UnexpectedUndefinedError('LMCIASC45599'); } else { const highlightArea = stackContentAreaDimensions.header.highlightArea; const surface = (highlightArea.x2 - highlightArea.x1) * (highlightArea.y2 - highlightArea.y1); const header = { x1: highlightArea.x1, x2: highlightArea.x2, y1: highlightArea.y1, y2: highlightArea.y2, contentItem: stack, surface, }; this._itemAreas.push(header); } } } } } } } /** * Called as part of loading a new layout (including initial init()). * Checks to see layout has a maximised item. If so, it maximises that item. * @internal */ checkLoadedLayoutMaximiseItem() { if (this._groundItem === undefined) { throw new UnexpectedUndefinedError('LMCLLMI43432'); } else { const configMaximisedItems = this._groundItem.getConfigMaximisedItems(); if (configMaximisedItems.length > 0) { let item = configMaximisedItems[0]; if (ContentItem.isComponentItem(item)) { const stack = item.parent; if (stack === null) { throw new UnexpectedNullError('LMXLLMI69999'); } else { item = stack; } } if (!ContentItem.isStack(item)) { throw new AssertError('LMCLLMI19993'); } else { item.maximise(); } } } } /** @internal */ processMaximiseStack(stack) { this._maximisedStack = stack; stack.on('beforeItemDestroyed', this._maximisedStackBeforeDestroyedListener); stack.element.classList.add("lm_maximised" /* Maximised */); stack.element.insertAdjacentElement('afterend', this._maximisePlaceholder); if (this._groundItem === undefined) { throw new UnexpectedUndefinedError('LMMXI19993'); } else { this._groundItem.element.prepend(stack.element); const { width, height } = getElementWidthAndHeight(this._containerElement); setElementWidth(stack.element, width); setElementHeight(stack.element, height); stack.updateSize(true); stack.focusActiveContentItem(); this._maximisedStack.emit('maximised'); this.emit('stateChanged'); } } /** @internal */ processMinimiseMaximisedStack() { if (this._maximisedStack === undefined) { throw new AssertError('LMMMS74422'); } else { const stack = this._maximisedStack; if (stack.parent === null) { throw new UnexpectedNullError('LMMI13668'); } else { stack.element.classList.remove("lm_maximised" /* Maximised */); this._maximisePlaceholder.insertAdjacentElement('afterend', stack.element); this._maximisePlaceholder.remove(); this.updateRootSize(true); this._maximisedStack = undefined; stack.off('beforeItemDestroyed', this._maximisedStackBeforeDestroyedListener); stack.emit('minimised'); this.emit('stateChanged'); } } } /** * Iterates through the array of open popout windows and removes the ones * that are effectively closed. This is necessary due to the lack of reliably * listening for window.close / unload events in a cross browser compatible fashion. * @internal */ reconcilePopoutWindows() { const openPopouts = []; for (let i = 0; i < this._openPopouts.length; i++) { if (this._openPopouts[i].getWindow().closed === false) { openPopouts.push(this._openPopouts[i]); } else { this.emit('windowClosed', this._openPopouts[i]); } } if (this._openPopouts.length !== openPopouts.length) { this._openPopouts = openPopouts; this.emit('stateChanged'); } } /** * Returns a flattened array of all content items, * regardles of level or type * @internal */ getAllContentItems() { if (this._groundItem === undefined) { throw new U