golden-layout
Version:
A multi-screen javascript Layout manager
1,162 lines (1,161 loc) • 68.7 kB
JavaScript
import { ItemConfig, LayoutConfig } from './config/config';
import { ResolvedComponentItemConfig, 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 { ApiError, 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 { deepExtendValue, 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();
/** @internal */
this._isFullPage = false;
/** @internal */
this._isInitialised = false;
/** @internal */
this._groundItem = undefined;
/** @internal */
this._openPopouts = [];
/** @internal */
this._dropTargetIndicator = null;
/** @internal */
this._transitionIndicator = null;
/** @internal */
this._componentTypes = {};
/** @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._windowResizeListener = () => this.processResizeWithDebounce();
/** @internal */
this._windowUnloadListener = () => this.onUnload();
/** @internal */
this._maximisedStackBeforeDestroyedListener = (ev) => this.cleanupBeforeMaximisedStackDestroyed(ev);
let layoutConfig = parameters.layoutConfig;
if (layoutConfig === undefined) {
layoutConfig = ResolvedLayoutConfig.createDefault();
}
this.layoutConfig = layoutConfig;
this.isSubWindow = parameters.isSubWindow;
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 */
get transitionIndicator() { return this._transitionIndicator; }
get width() { return this._width; }
get height() { return this._height; }
/** @internal */
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; }
/**
* Destroys the LayoutManager instance itself as well as every ContentItem
* within it. After this is called nothing should be left of the LayoutManager.
*/
destroy() {
if (this._isInitialised) {
if (this.layoutConfig.settings.closePopoutsOnUnload === true) {
for (let i = 0; i < this._openPopouts.length; i++) {
this._openPopouts[i].close();
}
}
if (this._isFullPage) {
globalThis.removeEventListener('resize', this._windowResizeListener);
}
globalThis.removeEventListener('unload', this._windowUnloadListener);
globalThis.removeEventListener('beforeunload', this._windowUnloadListener);
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.getComponentEvent = undefined;
this.releaseComponentEvent = undefined;
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);
}
/**
* Register a new component type with the layout manager.
*
* @deprecated See {@link https://stackoverflow.com/questions/40922531/how-to-check-if-a-javascript-function-is-a-constructor}
* instead use {@link (LayoutManager:class).registerComponentConstructor}
* or {@link (LayoutManager:class).registerComponentFactoryFunction}
*/
registerComponent(name, componentConstructorOrFactoryFtn) {
if (typeof componentConstructorOrFactoryFtn !== 'function') {
throw new ApiError('registerComponent() componentConstructorOrFactoryFtn parameter is not a function');
}
else {
if (componentConstructorOrFactoryFtn.hasOwnProperty('prototype')) {
const componentConstructor = componentConstructorOrFactoryFtn;
this.registerComponentConstructor(name, componentConstructor);
}
else {
const componentFactoryFtn = componentConstructorOrFactoryFtn;
this.registerComponentFactoryFunction(name, componentFactoryFtn);
}
}
}
/**
* Register a new component type with the layout manager.
*/
registerComponentConstructor(typeName, componentConstructor) {
if (typeof componentConstructor !== 'function') {
throw new Error(i18nStrings[1 /* PleaseRegisterAConstructorFunction */]);
}
if (this._componentTypes[typeName] !== undefined) {
throw new Error(`${i18nStrings[2 /* ComponentIsAlreadyRegistered */]}: ${typeName}`);
}
this._componentTypes[typeName] = {
constructor: componentConstructor,
factoryFunction: undefined,
};
}
/**
* Register a new component with the layout manager.
*/
registerComponentFactoryFunction(typeName, componentFactoryFunction) {
if (typeof componentFactoryFunction !== 'function') {
throw new Error('Please register a constructor function');
}
if (this._componentTypes[typeName] !== undefined) {
throw new Error('Component ' + typeName + ' is already registered');
}
this._componentTypes[typeName] = {
constructor: undefined,
factoryFunction: componentFactoryFunction,
};
}
/**
* Register a component function with the layout manager. This function should
* return a constructor for a component based on a config.
* This function will be called if a component type with the required name is not already registered.
* It is recommended that applications use the {@link (LayoutManager:class).getComponentEvent} and
* {@link (LayoutManager:class).releaseComponentEvent} instead of registering a constructor callback
* @deprecated use {@link (LayoutManager:class).registerGetComponentConstructorCallback}
*/
registerComponentFunction(callback) {
this.registerGetComponentConstructorCallback(callback);
}
/**
* Register a callback closure with the layout manager which supplies a Component Constructor.
* This callback should return a constructor for a component based on a config.
* This function will be called if a component type with the required name is not already registered.
* It is recommended that applications use the {@link (LayoutManager:class).getComponentEvent} and
* {@link (LayoutManager:class).releaseComponentEvent} instead of registering a constructor callback
*/
registerGetComponentConstructorCallback(callback) {
if (typeof callback !== 'function') {
throw new Error('Please register a callback function');
}
if (this._getComponentConstructorFtn !== undefined) {
console.warn('Multiple component functions are being registered. Only the final registered function will be used.');
}
this._getComponentConstructorFtn = callback;
}
getRegisteredComponentTypeNames() {
return Object.keys(this._componentTypes);
}
/**
* Returns a previously registered component instantiator. Attempts to utilize registered
* component type by first, then falls back to the component constructor callback function (if registered).
* If neither gets an instantiator, then returns `undefined`.
* Note that `undefined` will return if config.componentType is not a string
*
* @param config - The item config
* @public
*/
getComponentInstantiator(config) {
let instantiator;
const typeName = ResolvedComponentItemConfig.resolveComponentTypeName(config);
if (typeName !== undefined) {
instantiator = this._componentTypes[typeName];
}
if (instantiator === undefined) {
if (this._getComponentConstructorFtn !== undefined) {
instantiator = {
constructor: this._getComponentConstructorFtn(config),
factoryFunction: undefined,
};
}
}
return instantiator;
}
/** @internal */
getComponent(container, itemConfig) {
let instantiator;
const typeName = ResolvedComponentItemConfig.resolveComponentTypeName(itemConfig);
if (typeName !== undefined) {
instantiator = this._componentTypes[typeName];
}
if (instantiator === undefined) {
if (this._getComponentConstructorFtn !== undefined) {
instantiator = {
constructor: this._getComponentConstructorFtn(itemConfig),
factoryFunction: undefined,
};
}
}
let component;
if (instantiator !== undefined) {
// handle case where component is obtained by name or component constructor callback
let componentState;
if (itemConfig.componentState === undefined) {
componentState = undefined;
}
else {
// make copy
componentState = deepExtendValue({}, itemConfig.componentState);
}
// This next (commented out) if statement is a bad hack. Looks like someone wanted the component name passed
// to the component's constructor. The application really should have put this into the state itself.
// If an application needs this information in the constructor, it should now use the getComponentEvent.
// if (typeof componentState === 'object' && componentState !== null) {
// (componentState as Record<string, unknown>).componentName = itemConfig.componentName;
// }
const componentConstructor = instantiator.constructor;
if (componentConstructor !== undefined) {
component = new componentConstructor(container, componentState);
}
else {
const factoryFunction = instantiator.factoryFunction;
if (factoryFunction !== undefined) {
component = factoryFunction(container, componentState);
}
else {
throw new AssertError('LMGC10008');
}
}
}
else {
if (this.getComponentEvent !== undefined) {
component = this.getComponentEvent(container, itemConfig);
}
else {
throw new Error();
}
}
return component;
}
/** @internal */
releaseComponent(container, component) {
if (this.releaseComponentEvent !== undefined) {
this.releaseComponentEvent(container, component);
}
}
/**
* Called from GoldenLayout class. Finishes of init
* @internal
*/
init() {
this.setContainer();
this._dropTargetIndicator = new DropTargetIndicator( /*this.container*/);
this._transitionIndicator = new TransitionIndicator();
this.updateSizeFromContainer();
const layoutConfig = this.layoutConfig;
this._groundItem = new GroundItem(this, layoutConfig.root, this._containerElement);
this._groundItem.init();
this.checkLoadedLayoutMaximiseItem();
this.bindEvents();
this._isInitialised = true;
this.adjustColumnsResponsive();
this.emit('initialised');
}
/**
* 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.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;
}
}
}
/**
* @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[3 /* 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();
}
this.adjustColumnsResponsive();
}
}
}
/** @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
*/
updateRootSize() {
if (this._groundItem === undefined) {
throw new UnexpectedUndefinedError('LMURS28881');
}
else {
this._groundItem.updateSize();
}
}
/** @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],
width: config.width,
minWidth: config.minWidth,
height: config.height,
minHeight: config.minHeight,
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 */
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);
return browserPopout;
}
/**
* 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 -
* @param componentTypeOrFtn - Type of component to be created, or a function which will provide both component type and state
* @param componentState - Optional initial state of component. This will be ignored if componentTypeOrFtn is a function
*
* @returns 1) 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.
* 2) undefined if constrainDragToContainer is specified
*/
newDragSource(element, componentTypeOrFtn, componentState, title) {
const dragSource = new DragSource(this, element, [], componentTypeOrFtn, componentState, title);
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 mathingArea = 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 &&
y > area.y1 &&
y < area.y2 &&
smallestSurface > area.surface) {
smallestSurface = area.surface;
mathingArea = area;
}
}
return mathingArea;
}
/** @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();
stack.focusActiveContentItem();
this._maximisedStack.emit('maximised');
this.emit('stateChanged');
}