UNPKG

golden-layout

Version:
1,223 lines (1,092 loc) 75.1 kB
import { ComponentItemConfig, ItemConfig, LayoutConfig, RowOrColumnItemConfig, StackItemConfig } from './config/config'; import { ResolvedComponentItemConfig, ResolvedItemConfig, ResolvedLayoutConfig, ResolvedPopoutLayoutConfig, ResolvedRootItemConfig, ResolvedRowOrColumnItemConfig, ResolvedStackItemConfig } from "./config/resolved-config"; import { ComponentContainer } from './container/component-container'; 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 { ComponentParentableItem } from './items/component-parentable-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 { DomConstants } from './utils/dom-constants'; import { DragListener } from './utils/drag-listener'; import { EventEmitter } from './utils/event-emitter'; import { EventHub } from './utils/event-hub'; import { I18nStringId, I18nStrings, i18nStrings } from './utils/i18n-strings'; import { ItemType, JsonValue, Rect, ResponsiveMode } from './utils/types'; import { getElementWidthAndHeight, removeFromArray, setElementHeight, setElementWidth } from './utils/utils'; /** @internal */ declare global { interface Window { __glInstance: LayoutManager; } } /** * The main class that will be exposed as GoldenLayout. */ /** @public */ export abstract class LayoutManager extends EventEmitter { /** 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 */ resizeWithContainerAutomatically = false; /** The debounce interval (in milliseconds) used whenever a layout is automatically resized. 0 means next tick */ 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. */ resizeDebounceExtendedWhenPossible = true; /** @internal */ private _containerElement: HTMLElement; /** @internal */ private _isInitialised = false; /** @internal */ private _groundItem: GroundItem | undefined = undefined; /** @internal */ private _openPopouts: BrowserPopout[] = []; /** @internal */ private _dropTargetIndicator: DropTargetIndicator | null = null; /** @internal */ private _transitionIndicator: TransitionIndicator | null = null; /** @internal */ private _resizeTimeoutId: ReturnType<typeof setTimeout> | undefined; /** @internal */ private _itemAreas: ContentItem.Area[] = []; /** @internal */ private _maximisedStack: Stack | undefined; /** @internal */ private _maximisePlaceholder = LayoutManager.createMaximisePlaceElement(document); /** @internal */ private _tabDropPlaceholder = LayoutManager.createTabDropPlaceholderElement(document); /** @internal */ private _dragSources: DragSource[] = []; /** @internal */ private _updatingColumnsResponsive = false; /** @internal */ private _firstLoad = true; /** @internal */ private _eventHub = new EventHub(this); /** @internal */ private _width: number | null = null; /** @internal */ private _height: number | null = null; /** @internal */ private _focusedComponentItem: ComponentItem | undefined; /** @internal */ private _virtualSizedContainers: ComponentContainer[] = []; /** @internal */ private _virtualSizedContainerAddingBeginCount = 0; /** @internal */ private _sizeInvalidationBeginCount = 0; /** @internal */ protected _constructorOrSubWindowLayoutConfig: LayoutConfig | undefined; // protected for backwards compatibility /** @internal */ private _resizeObserver = new ResizeObserver(() => this.handleContainerResize()); /** @internal @deprecated to be removed in version 3 */ private _windowBeforeUnloadListener = () => this.onBeforeUnload(); /** @internal @deprecated to be removed in version 3 */ private _windowBeforeUnloadListening = false; /** @internal */ private _maximisedStackBeforeDestroyedListener = (ev: EventEmitter.BubblingEvent) => this.cleanupBeforeMaximisedStackDestroyed(ev); readonly isSubWindow: boolean; layoutConfig: ResolvedLayoutConfig; beforeVirtualRectingEvent: LayoutManager.BeforeVirtualRectingEvent | undefined; afterVirtualRectingEvent: LayoutManager.AfterVirtualRectingEvent | undefined; get container(): HTMLElement { return this._containerElement; } get isInitialised(): boolean { return this._isInitialised; } /** @internal */ get groundItem(): GroundItem | undefined { return this._groundItem; } /** @internal @deprecated use {@link (LayoutManager:class).groundItem} instead */ get root(): GroundItem | undefined { return this._groundItem; } get openPopouts(): BrowserPopout[] { return this._openPopouts; } /** @internal */ get dropTargetIndicator(): DropTargetIndicator | null { return this._dropTargetIndicator; } /** @internal @deprecated To be removed */ get transitionIndicator(): TransitionIndicator | null { return this._transitionIndicator; } get width(): number | null { return this._width; } get height(): number | null { 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(): EventHub { return this._eventHub; } get rootItem(): ContentItem | undefined { 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(): ComponentItem | undefined { return this._focusedComponentItem; } /** @internal */ get tabDropPlaceholder(): HTMLElement { return this._tabDropPlaceholder; } get maximisedStack(): Stack | undefined { return this._maximisedStack; } /** @deprecated indicates deprecated constructor use */ get deprecatedConstructor(): boolean { return !this.isSubWindow && this._constructorOrSubWindowLayoutConfig !== undefined; } /** * @param container - A Dom HTML element. Defaults to body * @internal */ constructor(parameters: LayoutManager.ConstructorParameters) { super(); this.isSubWindow = parameters.isSubWindow; this._constructorOrSubWindowLayoutConfig = parameters.constructorOrSubWindowLayoutConfig; I18nStrings.checkInitialise(); ConfigMinifier.checkInitialise(); if (parameters.containerElement !== undefined) { this._containerElement = parameters.containerElement; } } /** * 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(): void { 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: ResolvedLayoutConfig): ResolvedLayoutConfig { 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: ResolvedLayoutConfig): ResolvedLayoutConfig { return ResolvedLayoutConfig.unminifyConfig(config); } /** @internal */ abstract bindComponent(container: ComponentContainer, itemConfig: ResolvedComponentItemConfig): ComponentContainer.BindableComponent; /** @internal */ abstract unbindComponent(container: ComponentContainer, virtual: boolean, component: ComponentContainer.Component | undefined): void; /** * Called from GoldenLayout class. Finishes of init * @internal */ init(): void { this.setContainer(); this._dropTargetIndicator = new DropTargetIndicator(/*this.container*/); this._transitionIndicator = new TransitionIndicator(); this.updateSizeFromContainer(); let subWindowRootConfig: ComponentItemConfig | undefined; 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 = { ...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: LayoutConfig): void { 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(): ResolvedLayoutConfig { 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: ResolvedRootItemConfig | undefined; if (groundContent.length !== 1) { rootItemConfig = undefined; } else { rootItemConfig = groundContent[0]; } /* * Retrieve config for subwindows */ this.reconcilePopoutWindows(); const openPopouts: ResolvedPopoutLayoutConfig[] = []; for (let i = 0; i < this._openPopouts.length; i++) { openPopouts.push(this._openPopouts[i].toConfig()); } const config: ResolvedLayoutConfig = { 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(): void { if (this._groundItem === undefined) { throw new UnexpectedUndefinedError('LMCL11129'); } else { this._groundItem.clearRoot(); } } /** * @deprecated Use {@link (LayoutManager:class).saveLayout} */ toConfig(): ResolvedLayoutConfig { 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: JsonValue, componentState?: JsonValue, title?: string): ComponentItem { 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: JsonValue, componentState?: JsonValue, title?: string, locationSelectors?: LayoutManager.LocationSelector[] ): ComponentItem | undefined{ 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: JsonValue, componentState?: JsonValue, title?: string): LayoutManager.Location { 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: JsonValue, componentState?: JsonValue, title?: string, locationSelectors?: readonly LayoutManager.LocationSelector[] ): LayoutManager.Location | undefined { const itemConfig: ComponentItemConfig = { 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: RowOrColumnItemConfig | StackItemConfig | ComponentItemConfig): ContentItem { 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: RowOrColumnItemConfig | StackItemConfig | ComponentItemConfig, locationSelectors?: readonly LayoutManager.LocationSelector[] ): ContentItem | undefined { 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: RowOrColumnItemConfig | StackItemConfig | ComponentItemConfig): LayoutManager.Location { 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: RowOrColumnItemConfig | StackItemConfig | ComponentItemConfig, locationSelectors?: readonly LayoutManager.LocationSelector[] ): LayoutManager.Location | undefined { 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: number; switch (parentItem.type) { case ItemType.ground: { const groundItem = parentItem as GroundItem; 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 as RowOrColumn; addIdx = rowOrColumn.addItem(itemConfig, location.index); break; } case ItemType.stack: { if (!ItemConfig.isComponent(itemConfig)) { throw Error(i18nStrings[I18nStringId.ItemConfigIsNotTypeComponent]); } else { const stack = parentItem as Stack; 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: ComponentItemConfig): void { 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: number, height: number): void { this.setSize(width, height); } /** * Updates the layout managers size * * @param width - Width in pixels * @param height - Height in pixels */ setSize(width: number, height: number): void { 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(): void { this._sizeInvalidationBeginCount++; } /** @internal */ endSizeInvalidation(): void { if (--this._sizeInvalidationBeginCount === 0) { this.updateSizeFromContainer(); } } /** @internal */ updateSizeFromContainer(): void { 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): void { if (this._groundItem === undefined) { throw new UnexpectedUndefinedError('LMURS28881'); } else { this._groundItem.updateSize(force); } } /** @public */ createAndInitContentItem(config: ResolvedItemConfig, parent: ContentItem): ContentItem { 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: ResolvedItemConfig, parent: ContentItem): ContentItem { 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: ResolvedStackItemConfig = { 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: string): ComponentItem | undefined { if (this._groundItem === undefined) { throw new UnexpectedUndefinedError('LMFFCIBI82446'); } else { return this.findFirstContentItemTypeByIdRecursive(ItemType.component, id, this._groundItem) as ComponentItem; } } /** * 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: ContentItem | ResolvedRootItemConfig, positionAndSize: ResolvedPopoutLayoutConfig.Window, parentId: string | null, indexInParent: number | null ): BrowserPopout { if (itemConfigOrContentItem instanceof ContentItem) { return this.createPopoutFromContentItem(itemConfigOrContentItem, positionAndSize, parentId, indexInParent); } else { return this.createPopoutFromItemConfig(itemConfigOrContentItem, positionAndSize, parentId, indexInParent); } } /** @internal */ createPopoutFromContentItem(item: ContentItem, window: ResolvedPopoutLayoutConfig.Window | undefined, parentId: string | null, indexInParent: number | null | undefined, ): BrowserPopout { /** * 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[I18nStringId.PopoutCannotBeCreatedWithGroundItemConfig]}`); } else { return this.createPopoutFromItemConfig(itemConfig, window, parentId, indexInParent); } } } /** @internal */ beginVirtualSizedContainerAdding(): void { if (++this._virtualSizedContainerAddingBeginCount === 0) { this._virtualSizedContainers.length = 0; } } /** @internal */ addVirtualSizedContainer(container: ComponentContainer): void { this._virtualSizedContainers.push(container); } /** @internal */ endVirtualSizedContainerAdding(): void { 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: number): void { if (this.beforeVirtualRectingEvent !== undefined) { this.beforeVirtualRectingEvent(count); } } /** @internal */ fireAfterVirtualRectingEvent(): void { if (this.afterVirtualRectingEvent !== undefined) { this.afterVirtualRectingEvent(); } } /** @internal */ private createPopoutFromItemConfig(rootItemConfig: ResolvedRootItemConfig, window: ResolvedPopoutLayoutConfig.Window, parentId: string | null, indexInParent: number | null ) { const layoutConfig = this.toConfig(); const popoutLayoutConfig: ResolvedPopoutLayoutConfig = { root: rootItemConfig, openPopouts: [], settings: layoutConfig.settings, dimensions: layoutConfig.dimensions, header: layoutConfig.header, window, parentId, indexInParent, resolved: true, } return this.createPopoutFromPopoutLayoutConfig(popoutLayoutConfig); } /** @internal */ createPopoutFromPopoutLayoutConfig(config: ResolvedPopoutLayoutConfig): BrowserPopout { const configWindow = config.window; const initialWindow: Rect = { left: configWindow.left ?? (globalThis.screenX || globalThis.screenLeft + 20), top: configWindow.top ?? (globalThis.screenY || globalThis.screenTop + 20), width: configWindow.width ?? 500, height: configWindow.height ?? 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; } } /** * Attaches DragListener to any given DOM element * and turns it into a way of creating new ComponentItems * by 'dragging' the DOM element into the layout * * @param element - The HTML element which will be listened to for commencement of drag. * @param componentTypeOrItemConfigCallback - Type of component to be created, or a callback which will provide the ItemConfig * to be used to create the component. * @param componentState - Optional initial state of component. This will be ignored if componentTypeOrFtn is a function. * * @returns an opaque object that identifies the DOM element * and the attached itemConfig. This can be used in * removeDragSource() later to get rid of the drag listeners. */ newDragSource(element: HTMLElement, itemConfigCallback: () => (DragSource.ComponentItemConfig | ComponentItemConfig)): DragSource; /** @deprecated will be replaced in version 3 with newDragSource(element: HTMLElement, itemConfig: ComponentItemConfig) */ newDragSource(element: HTMLElement, componentType: JsonValue, componentState?: JsonValue, title?: JsonValue, id?: string): DragSource; newDragSource(element: HTMLElement, componentTypeOrItemConfigCallback: JsonValue | (() => (DragSource.ComponentItemConfig | ComponentItemConfig)), componentState?: JsonValue, title?: string, id?: string, ): DragSource { 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: DragSource): void { removeFromArray(dragSource, this._dragSources ); dragSource.destroy(); } /** @internal */ startComponentDrag(x: number, y: number, dragListener: DragListener, componentItem: ComponentItem, stack: Stack): void { 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: ComponentItem, suppressEvent = false): void { 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): void { 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: ComponentItem | undefined, suppressEvents = false): void { if (item !== this._focusedComponentItem) { let newFocusedParentItem: ComponentParentableItem | undefined; 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 */ private createContentItemFromConfig(config: ResolvedItemConfig, parent: ContentItem): ContentItem { switch (config.type) { case ItemType.ground: throw new AssertError('LMCCIFC68871'); case ItemType.row: return new RowOrColumn(false, this, config as ResolvedRowOrColumnItemConfig, parent); case ItemType.column: return new RowOrColumn(true, this, config as ResolvedRowOrColumnItemConfig, parent); case ItemType.stack: return new Stack(this, config as ResolvedStackItemConfig, parent); case ItemType.component: return new ComponentItem(this, config as ResolvedComponentItemConfig, parent as Stack); 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: Stack | undefined): void { if (stack === undefined) { if (this._maximisedStack !== undefined) { this.processMinimiseMaximisedStack(); } } else { if (stack !== this._maximisedStack) { if (this._maximisedStack !== undefined) { this.processMinimiseMaximisedStack(); } this.processMaximiseStack(stack); } } } checkMinimiseMaximisedStack(): void { 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 */ private cleanupBeforeMaximisedStackDestroyed(event: EventEmitter.BubblingEvent) { 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(): void { globalThis.setTimeout(() => globalThis.close(), 1); } /** @internal */ getArea(x: number, y: number): ContentItem.Area | null { 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(): void { 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];