golden-layout
Version:
A multi-screen javascript Layout manager
976 lines (863 loc) • 38.5 kB
text/typescript
import { ComponentItemConfig, ItemConfig } from '../config/config';
import { ResolvedComponentItemConfig, ResolvedHeaderedItemConfig, ResolvedItemConfig, ResolvedStackItemConfig } from '../config/resolved-config';
import { Header } from '../controls/header';
import { AssertError, UnexpectedNullError, UnexpectedUndefinedError } from '../errors/internal-error';
import { LayoutManager } from '../layout-manager';
import { DomConstants } from '../utils/dom-constants';
import { DragListener } from '../utils/drag-listener';
import { EventEmitter } from '../utils/event-emitter';
import { AreaLinkedRect, ItemType, JsonValue, Side, SizeUnitEnum, WidthAndHeight, WidthOrHeightPropertyName } from '../utils/types';
import {
getElementWidthAndHeight,
numberToPixels,
setElementDisplayVisibility
} from '../utils/utils';
import { ComponentItem } from './component-item';
import { ComponentParentableItem } from './component-parentable-item';
import { ContentItem } from './content-item';
/** @public */
export class Stack extends ComponentParentableItem {
/** @internal */
private readonly _headerConfig: ResolvedHeaderedItemConfig.Header | undefined;
/** @internal */
private readonly _header: Header;
/** @internal */
private readonly _childElementContainer: HTMLElement;
/** @internal */
private readonly _maximisedEnabled: boolean;
/** @internal */
private _activeComponentItem: ComponentItem | undefined;
/** @internal */
private _dropSegment: Stack.Segment;
/** @internal */
private _dropIndex: number;
/** @internal */
private _contentAreaDimensions: Stack.ContentAreaDimensions;
/** @internal */
private _headerSideChanged = false;
/** @internal */
private readonly _initialWantMaximise: boolean;
/** @internal */
private _initialActiveItemIndex: number;
/** @internal */
private _resizeListener = () => this.handleResize();
/** @internal */
private _maximisedListener = () => this.handleMaximised();
/** @internal */
private _minimisedListener = () => this.handleMinimised();
get childElementContainer(): HTMLElement { return this._childElementContainer; }
get header(): Header { return this._header; }
get headerShow(): boolean { return this._header.show; }
get headerSide(): Side { return this._header.side; }
get headerLeftRightSided(): boolean { return this._header.leftRightSided; }
/** @internal */
get contentAreaDimensions(): Stack.ContentAreaDimensions | undefined { return this._contentAreaDimensions; }
/** @internal */
get initialWantMaximise(): boolean { return this._initialWantMaximise; }
get isMaximised(): boolean { return this === this.layoutManager.maximisedStack; }
get stackParent(): ContentItem {
if (!this.parent) {
throw new Error('Stack should always have a parent');
}
return this.parent;
}
/** @internal */
constructor(layoutManager: LayoutManager, config: ResolvedStackItemConfig, parent: ContentItem) {
super(layoutManager, config, parent, Stack.createElement(document));
this._headerConfig = config.header;
const layoutHeaderConfig = layoutManager.layoutConfig.header;
const configContent = config.content;
// If stack has only one component, then we can also check this for header settings
let componentHeaderConfig: ResolvedHeaderedItemConfig.Header | undefined;
if (configContent.length !== 1) {
componentHeaderConfig = undefined;
} else {
const firstChildItemConfig = configContent[0];
componentHeaderConfig = (firstChildItemConfig as ResolvedHeaderedItemConfig).header; // will be undefined if not component (and wont be stack)
}
this._initialWantMaximise = config.maximised;
this._initialActiveItemIndex = config.activeItemIndex ?? 0; // make sure defined
// check for defined value for each item in order of Stack (this Item), Component (first child), Manager.
const show = this._headerConfig?.show ?? componentHeaderConfig?.show ?? layoutHeaderConfig.show;
const popout = this._headerConfig?.popout ?? componentHeaderConfig?.popout ?? layoutHeaderConfig.popout;
const maximise = this._headerConfig?.maximise ?? componentHeaderConfig?.maximise ?? layoutHeaderConfig.maximise;
const close = this._headerConfig?.close ?? componentHeaderConfig?.close ?? layoutHeaderConfig.close;
const minimise = this._headerConfig?.minimise ?? componentHeaderConfig?.minimise ?? layoutHeaderConfig.minimise;
const tabDropdown = this._headerConfig?.tabDropdown ?? componentHeaderConfig?.tabDropdown ?? layoutHeaderConfig.tabDropdown;
this._maximisedEnabled = maximise !== false;
const headerSettings: Header.Settings = {
show: show !== false,
side: show === false ? Side.top : show,
popoutEnabled: popout !== false,
popoutLabel: popout === false ? '' : popout,
maximiseEnabled: this._maximisedEnabled,
maximiseLabel: maximise === false ? '' : maximise,
closeEnabled: close !== false,
closeLabel: close === false ? '' : close,
minimiseEnabled: true,
minimiseLabel: minimise,
tabDropdownEnabled: tabDropdown !== false,
tabDropdownLabel: tabDropdown === false ? '' : tabDropdown,
};
this._header = new Header(layoutManager,
this, headerSettings,
config.isClosable && close !== false,
() => this.getActiveComponentItem(),
() => this.remove(),
() => this.handlePopoutEvent(),
() => this.toggleMaximise(),
(ev) => this.handleHeaderClickEvent(ev),
(ev) => this.handleHeaderTouchStartEvent(ev),
(item) => this.handleHeaderComponentRemoveEvent(item),
(item) => this.handleHeaderComponentFocusEvent(item),
(x, y, dragListener, item) => this.handleHeaderComponentStartDragEvent(x, y, dragListener, item),
);
// this._dropZones = {};
this.isStack = true;
this._childElementContainer = document.createElement('section');
this._childElementContainer.classList.add(DomConstants.ClassName.Items);
this.on('resize', this._resizeListener);
if (this._maximisedEnabled) {
this.on('maximised', this._maximisedListener);
this.on('minimised', this._minimisedListener);
}
this.element.appendChild(this._header.element);
this.element.appendChild(this._childElementContainer);
this.setupHeaderPosition();
this._header.updateClosability();
}
/** @internal */
override updateSize(force: boolean): void {
this.layoutManager.beginVirtualSizedContainerAdding();
try {
this.updateNodeSize();
this.updateContentItemsSize(force);
} finally {
this.layoutManager.endVirtualSizedContainerAdding();
}
}
/** @internal */
override init(): void {
if (this.isInitialised === true) return;
this.updateNodeSize();
for (let i = 0; i < this.contentItems.length; i++) {
this._childElementContainer.appendChild(this.contentItems[i].element);
}
super.init();
const contentItems = this.contentItems;
const contentItemCount = contentItems.length;
if (contentItemCount > 0) { // contentItemCount will be 0 on drag drop
if (this._initialActiveItemIndex < 0 || this._initialActiveItemIndex >= contentItemCount) {
throw new Error(`ActiveItemIndex out of range: ${this._initialActiveItemIndex} id: ${this.id}`);
} else {
for (let i = 0; i < contentItemCount; i++) {
const contentItem = contentItems[i];
if (!(contentItem instanceof ComponentItem)) {
throw new Error(`Stack Content Item is not of type ComponentItem: ${i} id: ${this.id}`);
} else {
this._header.createTab(contentItem, i);
contentItem.hide();
contentItem.container.setBaseLogicalZIndex();
}
}
this.setActiveComponentItem(contentItems[this._initialActiveItemIndex] as ComponentItem, false);
this._header.updateTabSizes();
}
}
this._header.updateClosability();
this.initContentItems();
}
/** @deprecated Use {@link (Stack:class).setActiveComponentItem} */
setActiveContentItem(item: ContentItem): void {
if (!ContentItem.isComponentItem(item)) {
throw new Error('Stack.setActiveContentItem: item is not a ComponentItem');
} else {
this.setActiveComponentItem(item, false);
}
}
setActiveComponentItem(componentItem: ComponentItem, focus: boolean, suppressFocusEvent = false): void {
if (this._activeComponentItem !== componentItem) {
if (this.contentItems.indexOf(componentItem) === -1) {
throw new Error('componentItem is not a child of this stack');
} else {
this.layoutManager.beginSizeInvalidation();
try {
if (this._activeComponentItem !== undefined) {
this._activeComponentItem.hide();
}
this._activeComponentItem = componentItem;
this._header.processActiveComponentChanged(componentItem);
componentItem.show();
} finally {
this.layoutManager.endSizeInvalidation();
}
this.emit('activeContentItemChanged', componentItem);
this.layoutManager.emit('activeContentItemChanged', componentItem);
this.emitStateChangedEvent();
}
}
if (this.focused || focus) {
this.layoutManager.setFocusedComponentItem(componentItem, suppressFocusEvent);
}
}
/** @deprecated Use {@link (Stack:class).getActiveComponentItem} */
getActiveContentItem(): ContentItem | null {
return this.getActiveComponentItem() ?? null;
}
getActiveComponentItem(): ComponentItem | undefined {
return this._activeComponentItem;
}
/** @internal */
focusActiveContentItem(): void {
this._activeComponentItem?.focus();
}
/** @internal */
override setFocusedValue(value: boolean): void {
this._header.applyFocusedValue(value);
super.setFocusedValue(value);
}
/** @internal */
setRowColumnClosable(value: boolean): void {
this._header.setRowColumnClosable(value);
}
newComponent(componentType: JsonValue, componentState?: JsonValue, title?: string, index?: number): ComponentItem {
const itemConfig: ComponentItemConfig = {
type: 'component',
componentType,
componentState,
title,
};
return this.newItem(itemConfig, index) as ComponentItem;
}
addComponent(componentType: JsonValue, componentState?: JsonValue, title?: string, index?: number): number {
const itemConfig: ComponentItemConfig = {
type: 'component',
componentType,
componentState,
title,
};
return this.addItem(itemConfig, index);
}
newItem(itemConfig: ComponentItemConfig, index?: number): ContentItem {
index = this.addItem(itemConfig, index);
return this.contentItems[index];
}
addItem(itemConfig: ComponentItemConfig, index?: number): number {
this.layoutManager.checkMinimiseMaximisedStack();
const resolvedItemConfig = ItemConfig.resolve(itemConfig, false);
const contentItem = this.layoutManager.createAndInitContentItem(resolvedItemConfig, this);
return this.addChild(contentItem, index);
}
override addChild(contentItem: ContentItem, index?: number, focus = false): number {
if(index !== undefined && index > this.contentItems.length){
index -= 1;
throw new AssertError('SAC99728'); // undisplayChild() removed so this condition should no longer occur
}
if (!(contentItem instanceof ComponentItem)) {
throw new AssertError('SACC88532'); // Stacks can only have Component children
} else {
index = super.addChild(contentItem, index);
this._childElementContainer.appendChild(contentItem.element);
this._header.createTab(contentItem, index);
this.setActiveComponentItem(contentItem, focus);
this._header.updateTabSizes();
this.updateSize(false);
contentItem.container.setBaseLogicalZIndex();
this._header.updateClosability();
this.emitStateChangedEvent();
return index;
}
}
override removeChild(contentItem: ContentItem, keepChild: boolean): void {
const componentItem = contentItem as ComponentItem;
const index = this.contentItems.indexOf(componentItem);
const stackWillBeDeleted = this.contentItems.length === 1;
if (this._activeComponentItem === componentItem) {
if (componentItem.focused) {
componentItem.blur();
}
if (!stackWillBeDeleted) {
// At this point we're already sure we have at least one content item left *after*
// removing contentItem, so we can safely assume index 1 is a valid one if
// the index of contentItem is 0, otherwise we just use the previous content item.
const newActiveComponentIdx = index === 0 ? 1 : index - 1;
this.setActiveComponentItem(this.contentItems[newActiveComponentIdx] as ComponentItem, false);
}
}
this._header.removeTab(componentItem);
super.removeChild(componentItem, keepChild);
if (!stackWillBeDeleted) {
this._header.updateClosability();
}
this.emitStateChangedEvent();
}
/**
* Maximises the Item or minimises it if it is already maximised
*/
toggleMaximise(): void {
if (this.isMaximised) {
this.minimise();
} else {
this.maximise();
}
}
maximise(): void {
if (!this.isMaximised) {
this.layoutManager.setMaximisedStack(this);
const contentItems = this.contentItems;
const contentItemCount = contentItems.length;
for (let i = 0; i < contentItemCount; i++) {
const contentItem = contentItems[i];
if (contentItem instanceof ComponentItem) {
contentItem.enterStackMaximised();
} else {
throw new AssertError('SMAXI87773');
}
}
this.emitStateChangedEvent();
}
}
minimise(): void {
if (this.isMaximised) {
this.layoutManager.setMaximisedStack(undefined);
const contentItems = this.contentItems;
const contentItemCount = contentItems.length;
for (let i = 0; i < contentItemCount; i++) {
const contentItem = contentItems[i];
if (contentItem instanceof ComponentItem) {
contentItem.exitStackMaximised();
} else {
throw new AssertError('SMINI87773');
}
}
this.emitStateChangedEvent();
}
}
/** @internal */
override destroy(): void {
if (this._activeComponentItem?.focused) {
this._activeComponentItem.blur();
}
super.destroy();
this.off('resize', this._resizeListener);
if (this._maximisedEnabled) {
this.off('maximised', this._maximisedListener);
this.off('minimised', this._minimisedListener);
}
this._header.destroy();
}
toConfig(): ResolvedStackItemConfig {
let activeItemIndex: number | undefined;
if (this._activeComponentItem) {
activeItemIndex = this.contentItems.indexOf(this._activeComponentItem);
if (activeItemIndex < 0) {
throw new Error('active component item not found in stack');
}
}
if (this.contentItems.length > 0 && activeItemIndex === undefined) {
throw new Error('expected non-empty stack to have an active component item');
} else {
const result: ResolvedStackItemConfig = {
type: 'stack',
content: this.calculateConfigContent() as ResolvedComponentItemConfig[],
size: this.size,
sizeUnit: this.sizeUnit,
minSize: this.minSize,
minSizeUnit: this.minSizeUnit,
id: this.id,
isClosable: this.isClosable,
maximised: this.isMaximised,
header: this.createHeaderConfig(),
activeItemIndex,
}
return result;
}
}
/**
* Ok, this one is going to be the tricky one: The user has dropped a {@link (ContentItem:class)} onto this stack.
*
* It was dropped on either the stacks header or the top, right, bottom or left bit of the content area
* (which one of those is stored in this._dropSegment). Now, if the user has dropped on the header the case
* is relatively clear: We add the item to the existing stack... job done (might be good to have
* tab reordering at some point, but lets not sweat it right now)
*
* If the item was dropped on the content part things are a bit more complicated. If it was dropped on either the
* top or bottom region we need to create a new column and place the items accordingly.
* Unless, of course if the stack is already within a column... in which case we want
* to add the newly created item to the existing column...
* either prepend or append it, depending on wether its top or bottom.
*
* Same thing for rows and left / right drop segments... so in total there are 9 things that can potentially happen
* (left, top, right, bottom) * is child of the right parent (row, column) + header drop
*
* @internal
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
override onDrop(contentItem: ContentItem, area: ContentItem.Area): void {
/*
* The item was dropped on the header area. Just add it as a child of this stack and
* get the hell out of this logic
*/
if (this._dropSegment === Stack.Segment.Header) {
this.resetHeaderDropZone();
if (this._dropIndex === undefined) {
throw new UnexpectedUndefinedError('SODDI68990');
} else {
this.addChild(contentItem, this._dropIndex);
return;
}
}
/*
* The stack is empty. Let's just add the element.
*/
if (this._dropSegment === Stack.Segment.Body) {
this.addChild(contentItem, 0, true);
return;
}
/*
* The item was dropped on the top-, left-, bottom- or right- part of the content. Let's
* aggregate some conditions to make the if statements later on more readable
*/
const isVertical = this._dropSegment === Stack.Segment.Top || this._dropSegment === Stack.Segment.Bottom;
const isHorizontal = this._dropSegment === Stack.Segment.Left || this._dropSegment === Stack.Segment.Right;
const insertBefore = this._dropSegment === Stack.Segment.Top || this._dropSegment === Stack.Segment.Left;
const hasCorrectParent = (isVertical && this.stackParent.isColumn) || (isHorizontal && this.stackParent.isRow);
/*
* The content item can be either a component or a stack. If it is a component, wrap it into a stack
*/
if (contentItem.isComponent) {
const itemConfig = ResolvedStackItemConfig.createDefault();
itemConfig.header = this.createHeaderConfig();
const stack = this.layoutManager.createAndInitContentItem(itemConfig, this);
stack.addChild(contentItem);
contentItem = stack;
}
/*
* If the contentItem that's being dropped is not dropped on a Stack (cases which just passed above and
* which would wrap the contentItem in a Stack) we need to check whether contentItem is a RowOrColumn.
* If it is, we need to re-wrap it in a Stack like it was when it was dragged by its Tab (it was dragged!).
*/
if(contentItem.type === ItemType.row || contentItem.type === ItemType.column){
const itemConfig = ResolvedStackItemConfig.createDefault();
itemConfig.header = this.createHeaderConfig();
const stack = this.layoutManager.createContentItem(itemConfig, this);
stack.addChild(contentItem)
contentItem = stack
}
/*
* If the item is dropped on top or bottom of a column or left and right of a row, it's already
* layd out in the correct way. Just add it as a child
*/
if (hasCorrectParent) {
const index = this.stackParent.contentItems.indexOf(this);
this.stackParent.addChild(contentItem, insertBefore ? index : index + 1, true);
this.size *= 0.5;
contentItem.size = this.size;
contentItem.sizeUnit = this.sizeUnit;
this.stackParent.updateSize(false);
/*
* This handles items that are dropped on top or bottom of a row or left / right of a column. We need
* to create the appropriate contentItem for them to live in
*/
} else {
const type = isVertical ? ItemType.column : ItemType.row;
const itemConfig = ResolvedItemConfig.createDefault(type) as ResolvedItemConfig;
const rowOrColumn = this.layoutManager.createContentItem(itemConfig, this);
this.stackParent.replaceChild(this, rowOrColumn);
rowOrColumn.addChild(contentItem, insertBefore ? 0 : undefined, true);
rowOrColumn.addChild(this, insertBefore ? undefined : 0, true);
this.size = 50;
contentItem.size = 50;
contentItem.sizeUnit = SizeUnitEnum.Percent;
rowOrColumn.updateSize(false);
}
}
/**
* If the user hovers above the header part of the stack, indicate drop positions for tabs.
* otherwise indicate which segment of the body the dragged item would be dropped on
*
* @param x - Absolute Screen X
* @param y - Absolute Screen Y
* @internal
*/
override highlightDropZone(x: number, y: number): void {
for (const key in this._contentAreaDimensions) {
const segment = key as Stack.Segment;
const area = this._contentAreaDimensions[segment].hoverArea;
if (area.x1 < x && area.x2 > x && area.y1 < y && area.y2 > y) {
if (segment === Stack.Segment.Header) {
this._dropSegment = Stack.Segment.Header;
this.highlightHeaderDropZone(this._header.leftRightSided ? y : x);
} else {
this.resetHeaderDropZone();
this.highlightBodyDropZone(segment);
}
return;
}
}
}
/** @internal */
getArea(): ContentItem.Area | null {
if (this.element.style.display === 'none') {
return null;
}
const headerArea = super.getElementArea(this._header.element);
const contentArea = super.getElementArea(this._childElementContainer);
if (headerArea === null || contentArea === null) {
throw new UnexpectedNullError('SGAHC13086');
}
const contentWidth = contentArea.x2 - contentArea.x1;
const contentHeight = contentArea.y2 - contentArea.y1;
this._contentAreaDimensions = {
header: {
hoverArea: {
x1: headerArea.x1,
y1: headerArea.y1,
x2: headerArea.x2,
y2: headerArea.y2
},
highlightArea: {
x1: headerArea.x1,
y1: headerArea.y1,
x2: headerArea.x2,
y2: headerArea.y2
}
}
};
/**
* Highlight the entire body if the stack is empty
*/
if (this.contentItems.length === 0) {
this._contentAreaDimensions.body = {
hoverArea: {
x1: contentArea.x1,
y1: contentArea.y1,
x2: contentArea.x2,
y2: contentArea.y2
},
highlightArea: {
x1: contentArea.x1,
y1: contentArea.y1,
x2: contentArea.x2,
y2: contentArea.y2
}
};
return super.getElementArea(this.element);
} else {
this._contentAreaDimensions.left = {
hoverArea: {
x1: contentArea.x1,
y1: contentArea.y1,
x2: contentArea.x1 + contentWidth * 0.25,
y2: contentArea.y2
},
highlightArea: {
x1: contentArea.x1,
y1: contentArea.y1,
x2: contentArea.x1 + contentWidth * 0.5,
y2: contentArea.y2
}
};
this._contentAreaDimensions.top = {
hoverArea: {
x1: contentArea.x1 + contentWidth * 0.25,
y1: contentArea.y1,
x2: contentArea.x1 + contentWidth * 0.75,
y2: contentArea.y1 + contentHeight * 0.5
},
highlightArea: {
x1: contentArea.x1,
y1: contentArea.y1,
x2: contentArea.x2,
y2: contentArea.y1 + contentHeight * 0.5
}
};
this._contentAreaDimensions.right = {
hoverArea: {
x1: contentArea.x1 + contentWidth * 0.75,
y1: contentArea.y1,
x2: contentArea.x2,
y2: contentArea.y2
},
highlightArea: {
x1: contentArea.x1 + contentWidth * 0.5,
y1: contentArea.y1,
x2: contentArea.x2,
y2: contentArea.y2
}
};
this._contentAreaDimensions.bottom = {
hoverArea: {
x1: contentArea.x1 + contentWidth * 0.25,
y1: contentArea.y1 + contentHeight * 0.5,
x2: contentArea.x1 + contentWidth * 0.75,
y2: contentArea.y2
},
highlightArea: {
x1: contentArea.x1,
y1: contentArea.y1 + contentHeight * 0.5,
x2: contentArea.x2,
y2: contentArea.y2
}
};
return super.getElementArea(this.element);
}
}
/**
* Programmatically operate with header position.
*
* @param position -
*
* @returns previous header position
* @internal
*/
positionHeader(position: Side): void {
if (this._header.side !== position) {
this._header.setSide(position);
this._headerSideChanged = true;
this.setupHeaderPosition();
}
}
/** @internal */
private updateNodeSize(): void {
if (this.element.style.display !== 'none') {
const content: WidthAndHeight = getElementWidthAndHeight(this.element);
if (this._header.show) {
const dimension = this._header.leftRightSided ? WidthOrHeightPropertyName.width : WidthOrHeightPropertyName.height;
content[dimension] -= this.layoutManager.layoutConfig.dimensions.headerHeight;
}
this._childElementContainer.style.width = numberToPixels(content.width);
this._childElementContainer.style.height = numberToPixels(content.height);
for (let i = 0; i < this.contentItems.length; i++) {
this.contentItems[i].element.style.width = numberToPixels(content.width);
this.contentItems[i].element.style.height = numberToPixels(content.height);
}
this.emit('resize');
this.emitStateChangedEvent();
}
}
/** @internal */
private highlightHeaderDropZone(x: number): void {
const visibleTabsLength = this._header.lastVisibleTabIndex + 1;
const tabsContainerElement = this._header.tabsContainerElement;
const tabsContainerElementChildNodes = tabsContainerElement.childNodes;
// Create shallow copy of childNodes list, excluding DropPlaceHolder, as we will be modifying the childNodes list
const visibleTabElements = new Array<HTMLElement>(visibleTabsLength);
let tabIndex = 0;
let tabCount = 0;
while (tabCount < visibleTabsLength) {
const visibleTabElement = tabsContainerElementChildNodes[tabIndex++] as HTMLElement;
if (visibleTabElement !== this.layoutManager.tabDropPlaceholder) {
visibleTabElements[tabCount++] = visibleTabElement;
}
}
const dropTargetIndicator = this.layoutManager.dropTargetIndicator;
if (dropTargetIndicator === null) {
throw new UnexpectedNullError('SHHDZDTI97110');
}
let area: AreaLinkedRect;
// Empty stack
if (visibleTabsLength === 0) {
const headerRect = this._header.element.getBoundingClientRect();
const headerTop = headerRect.top + document.body.scrollTop;
const headerLeft = headerRect.left + document.body.scrollLeft;
area = {
x1: headerLeft,
x2: headerLeft + 100,
y1: headerTop + headerRect.height - 20,
y2: headerTop + headerRect.height,
};
this._dropIndex = 0;
} else {
let tabIndex = 0;
// This indicates whether our cursor is exactly over a tab
let isAboveTab = false;
let tabTop: number;
let tabLeft: number;
let tabWidth: number;
let tabElement: HTMLElement;
do {
tabElement = visibleTabElements[tabIndex] as HTMLElement;
const tabRect = tabElement.getBoundingClientRect();
const tabRectTop = tabRect.top + document.body.scrollTop;
const tabRectLeft = tabRect.left + document.body.scrollLeft;
if (this._header.leftRightSided) {
tabLeft = tabRectTop;
tabTop = tabRectLeft;
tabWidth = tabRect.height;
} else {
tabLeft = tabRectLeft;
tabTop = tabRectTop;
tabWidth = tabRect.width;
}
if (x >= tabLeft && x < tabLeft + tabWidth) {
isAboveTab = true;
} else {
tabIndex++;
}
} while (tabIndex < visibleTabsLength && !isAboveTab);
// If we're not above any tabs, or to the right of any tab, we are out of the area, so give up
if (isAboveTab === false && x < tabLeft) {
return;
}
const halfX = tabLeft + tabWidth / 2;
if (x < halfX) {
this._dropIndex = tabIndex;
tabElement.insertAdjacentElement('beforebegin', this.layoutManager.tabDropPlaceholder);
} else {
this._dropIndex = Math.min(tabIndex + 1, visibleTabsLength);
tabElement.insertAdjacentElement('afterend', this.layoutManager.tabDropPlaceholder);
}
const tabDropPlaceholderRect = this.layoutManager.tabDropPlaceholder.getBoundingClientRect();
const tabDropPlaceholderRectTop = tabDropPlaceholderRect.top + document.body.scrollTop;
const tabDropPlaceholderRectLeft = tabDropPlaceholderRect.left + document.body.scrollLeft;
const tabDropPlaceholderRectWidth = tabDropPlaceholderRect.width;
if (this._header.leftRightSided) {
const placeHolderTop = tabDropPlaceholderRectTop;
area = {
x1: tabTop,
x2: tabTop + tabElement.clientHeight,
y1: placeHolderTop,
y2: placeHolderTop + tabDropPlaceholderRectWidth,
};
} else {
const placeHolderLeft = tabDropPlaceholderRectLeft;
area = {
x1: placeHolderLeft,
x2: placeHolderLeft + tabDropPlaceholderRectWidth,
y1: tabTop,
y2: tabTop + tabElement.clientHeight,
};
}
}
dropTargetIndicator.highlightArea(area, 0);
return;
}
/** @internal */
private resetHeaderDropZone() {
this.layoutManager.tabDropPlaceholder.remove();
}
/** @internal */
private setupHeaderPosition() {
setElementDisplayVisibility(this._header.element, this._header.show);
this.element.classList.remove(DomConstants.ClassName.Left, DomConstants.ClassName.Right, DomConstants.ClassName.Bottom);
if (this._header.leftRightSided) {
this.element.classList.add('lm_' + this._header.side);
}
//if ([Side.right, Side.bottom].includes(this._header.side)) {
// // move the header behind the content.
// this.element.appendChild(this._header.element);
//}
this.updateSize(false);
}
/** @internal */
private highlightBodyDropZone(segment: Stack.Segment): void {
if (this._contentAreaDimensions === undefined) {
throw new UnexpectedUndefinedError('SHBDZC82265');
} else {
const highlightArea = this._contentAreaDimensions[segment].highlightArea;
const dropTargetIndicator = this.layoutManager.dropTargetIndicator;
if (dropTargetIndicator === null) {
throw new UnexpectedNullError('SHBDZD96110');
} else {
dropTargetIndicator.highlightArea(highlightArea, 1);
this._dropSegment = segment;
}
}
}
/** @internal */
private handleResize() {
this._header.updateTabSizes()
}
/** @internal */
private handleMaximised() {
this._header.processMaximised();
}
/** @internal */
private handleMinimised() {
this._header.processMinimised();
}
/** @internal */
private handlePopoutEvent() {
this.popout();
}
/** @internal */
private handleHeaderClickEvent(ev: MouseEvent) {
const eventName = EventEmitter.headerClickEventName;
const bubblingEvent = new EventEmitter.ClickBubblingEvent(eventName, this, ev);
this.emit(eventName, bubblingEvent);
}
/** @internal */
private handleHeaderTouchStartEvent(ev: TouchEvent) {
const eventName = EventEmitter.headerTouchStartEventName;
const bubblingEvent = new EventEmitter.TouchStartBubblingEvent(eventName, this, ev);
this.emit(eventName, bubblingEvent);
}
/** @internal */
private handleHeaderComponentRemoveEvent(item: ComponentItem) {
this.removeChild(item, false);
}
/** @internal */
private handleHeaderComponentFocusEvent(item: ComponentItem) {
this.setActiveComponentItem(item, true);
}
/** @internal */
private handleHeaderComponentStartDragEvent(x: number, y: number, dragListener: DragListener, componentItem: ComponentItem) {
if (this.isMaximised === true) {
this.toggleMaximise();
}
this.layoutManager.startComponentDrag(x, y, dragListener, componentItem, this);
}
/** @internal */
private createHeaderConfig() {
if (!this._headerSideChanged) {
return ResolvedHeaderedItemConfig.Header.createCopy(this._headerConfig);
} else {
const show = this._header.show ? this._header.side : false;
let result = ResolvedHeaderedItemConfig.Header.createCopy(this._headerConfig, show);
if (result === undefined) {
result = {
show,
popout: undefined,
maximise: undefined,
close: undefined,
minimise: undefined,
tabDropdown: undefined,
};
}
return result;
}
}
/** @internal */
private emitStateChangedEvent() {
this.emitBaseBubblingEvent('stateChanged');
}
}
/** @public */
export namespace Stack {
/** @internal */
export const enum Segment {
Header = 'header',
Body = 'body',
Left = 'left',
Right = 'right',
Top = 'top',
Bottom = 'bottom',
}
/** @internal */
export interface ContentAreaDimension {
hoverArea: AreaLinkedRect;
highlightArea: AreaLinkedRect;
}
/** @internal */
export type ContentAreaDimensions = {
[segment: string]: ContentAreaDimension;
};
/** @internal */
export function createElement(document: Document): HTMLDivElement {
const element = document.createElement('div');
element.classList.add(DomConstants.ClassName.Item);
element.classList.add(DomConstants.ClassName.Stack);
return element;
}
}