diff --git a/packages/dockview-core/src/__tests__/dnd/overlay.spec.ts b/packages/dockview-core/src/__tests__/dnd/overlay.spec.ts index edc661a12..5c3ba8ff9 100644 --- a/packages/dockview-core/src/__tests__/dnd/overlay.spec.ts +++ b/packages/dockview-core/src/__tests__/dnd/overlay.spec.ts @@ -1,7 +1,14 @@ import { Overlay } from '../../dnd/overlay'; +const mockGetBoundingClientRect = ({ left, top, height, width }: { left: number, top: number, height: number, width: number }) => { + const result = { left, top, height, width, right: left + width, bottom: top + height, x: left, y: top }; + return { + ...result, toJSON: () => (result) + } +} + describe('overlay', () => { - test('toJSON', () => { + test('toJSON, top left', () => { const container = document.createElement('div'); const content = document.createElement('div'); @@ -23,11 +30,11 @@ describe('overlay', () => { container.childNodes.item(0) as HTMLElement, 'getBoundingClientRect' ).mockImplementation(() => { - return { left: 80, top: 100, width: 40, height: 50 } as any; + return mockGetBoundingClientRect({ left: 80, top: 100, width: 40, height: 50 }); }); jest.spyOn(container, 'getBoundingClientRect').mockImplementation( () => { - return { left: 20, top: 30, width: 100, height: 100 } as any; + return mockGetBoundingClientRect({ left: 20, top: 30, width: 100, height: 100 }); } ); @@ -39,7 +46,45 @@ describe('overlay', () => { }); }); - test('that out-of-bounds dimensions are fixed', () => { + test('toJSON, bottom right', () => { + const container = document.createElement('div'); + const content = document.createElement('div'); + + document.body.appendChild(container); + container.appendChild(content); + + const cut = new Overlay({ + height: 200, + width: 100, + right: 10, + bottom: 20, + minimumInViewportWidth: 0, + minimumInViewportHeight: 0, + container, + content, + }); + + jest.spyOn( + container.childNodes.item(0) as HTMLElement, + 'getBoundingClientRect' + ).mockImplementation(() => { + return mockGetBoundingClientRect({ left: 80, top: 100, width: 40, height: 50 }); + }); + jest.spyOn(container, 'getBoundingClientRect').mockImplementation( + () => { + return mockGetBoundingClientRect({ left: 20, top: 30, width: 100, height: 100 }); + } + ); + + expect(cut.toJSON()).toEqual({ + bottom: -20, + right: 0, + width: 40, + height: 50, + }); + }); + + test('that out-of-bounds dimensions are fixed, top left', () => { const container = document.createElement('div'); const content = document.createElement('div'); @@ -61,11 +106,11 @@ describe('overlay', () => { container.childNodes.item(0) as HTMLElement, 'getBoundingClientRect' ).mockImplementation(() => { - return { left: 80, top: 100, width: 40, height: 50 } as any; + return mockGetBoundingClientRect({ left: 80, top: 100, width: 40, height: 50 }); }); jest.spyOn(container, 'getBoundingClientRect').mockImplementation( () => { - return { left: 20, top: 30, width: 100, height: 100 } as any; + return mockGetBoundingClientRect({ left: 20, top: 30, width: 100, height: 100 }); } ); @@ -77,7 +122,45 @@ describe('overlay', () => { }); }); - test('setBounds', () => { + test('that out-of-bounds dimensions are fixed, bottom right', () => { + const container = document.createElement('div'); + const content = document.createElement('div'); + + document.body.appendChild(container); + container.appendChild(content); + + const cut = new Overlay({ + height: 200, + width: 100, + bottom: -1000, + right: -1000, + minimumInViewportWidth: 0, + minimumInViewportHeight: 0, + container, + content, + }); + + jest.spyOn( + container.childNodes.item(0) as HTMLElement, + 'getBoundingClientRect' + ).mockImplementation(() => { + return mockGetBoundingClientRect({ left: 80, top: 100, width: 40, height: 50 }); + }); + jest.spyOn(container, 'getBoundingClientRect').mockImplementation( + () => { + return mockGetBoundingClientRect({ left: 20, top: 30, width: 100, height: 100 }); + } + ); + + expect(cut.toJSON()).toEqual({ + bottom: -20, + right: 0, + width: 40, + height: 50, + }); + }); + + test('setBounds, top left', () => { const container = document.createElement('div'); const content = document.createElement('div'); @@ -101,11 +184,11 @@ describe('overlay', () => { expect(element).toBeTruthy(); jest.spyOn(element, 'getBoundingClientRect').mockImplementation(() => { - return { left: 300, top: 400, width: 1000, height: 1000 } as any; + return mockGetBoundingClientRect({ left: 300, top: 400, width: 200, height: 100 }); }); jest.spyOn(container, 'getBoundingClientRect').mockImplementation( () => { - return { left: 0, top: 0, width: 1000, height: 1000 } as any; + return mockGetBoundingClientRect({ left: 0, top: 0, width: 1000, height: 1000 }); } ); @@ -117,6 +200,46 @@ describe('overlay', () => { expect(element.style.top).toBe('400px'); }); + test('setBounds, bottom right', () => { + const container = document.createElement('div'); + const content = document.createElement('div'); + + document.body.appendChild(container); + container.appendChild(content); + + const cut = new Overlay({ + height: 1000, + width: 1000, + right: 0, + bottom: 0, + minimumInViewportWidth: 0, + minimumInViewportHeight: 0, + container, + content, + }); + + const element: HTMLElement = container.querySelector( + '.dv-resize-container' + )!; + expect(element).toBeTruthy(); + + jest.spyOn(element, 'getBoundingClientRect').mockImplementation(() => { + return mockGetBoundingClientRect({ left: 500, top: 500, width: 200, height: 100 }); + }); + jest.spyOn(container, 'getBoundingClientRect').mockImplementation( + () => { + return mockGetBoundingClientRect({ left: 0, top: 0, width: 1000, height: 1000 }); + } + ); + + cut.setBounds({ height: 100, width: 200, right: 300, bottom: 400 }); + + expect(element.style.height).toBe('100px'); + expect(element.style.width).toBe('200px'); + expect(element.style.right).toBe('300px'); + expect(element.style.bottom).toBe('400px'); + }); + test('that the resize handles are added', () => { const container = document.createElement('div'); const content = document.createElement('div'); diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index 1da34f822..24b964a8b 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -42,7 +42,7 @@ import { GroupDragEvent, TabDragEvent, } from '../dockview/components/titlebar/tabsContainer'; -import { Box } from '../types'; +import { AnchoredBox, Box } from '../types'; import { DockviewDidDropEvent, DockviewWillDropEvent, @@ -139,7 +139,7 @@ export class SplitviewApi implements CommonApi { return this.component.onDidRemoveView; } - constructor(private readonly component: ISplitviewComponent) {} + constructor(private readonly component: ISplitviewComponent) { } /** * Update configuratable options. @@ -295,7 +295,7 @@ export class PaneviewApi implements CommonApi { return emitter.event; } - constructor(private readonly component: IPaneviewComponent) {} + constructor(private readonly component: IPaneviewComponent) { } /** * Remove a panel given the panel object. @@ -459,7 +459,7 @@ export class GridviewApi implements CommonApi { this.component.updateOptions({ orientation: value }); } - constructor(private readonly component: IGridviewComponent) {} + constructor(private readonly component: IGridviewComponent) { } /** * Focus the component. Will try to focus an active panel if one exists. @@ -728,7 +728,7 @@ export class DockviewApi implements CommonApi { return this.component.activeGroup; } - constructor(private readonly component: IDockviewComponent) {} + constructor(private readonly component: IDockviewComponent) { } /** * Focus the component. Will try to focus an active panel if one exists. @@ -800,9 +800,15 @@ export class DockviewApi implements CommonApi { */ addFloatingGroup( item: IDockviewPanel | DockviewGroupPanel, - coord?: { x: number; y: number } + coord?: { x: number; y: number }, + options?: { + position?: AnchoredBox; + skipRemoveGroup?: boolean; + inDragMode?: boolean; + skipActiveGroup?: boolean; + } ): void { - return this.component.addFloatingGroup(item, coord); + return this.component.addFloatingGroup(item, coord, options); } /** diff --git a/packages/dockview-core/src/constants.ts b/packages/dockview-core/src/constants.ts index b8b9fd3f6..c813942d3 100644 --- a/packages/dockview-core/src/constants.ts +++ b/packages/dockview-core/src/constants.ts @@ -1,3 +1,3 @@ export const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100; -export const DEFAULT_FLOATING_GROUP_POSITION = { left: 100, top: 100 }; +export const DEFAULT_FLOATING_GROUP_POSITION = { left: 100, top: 100, width: 300, height: 300 }; diff --git a/packages/dockview-core/src/dnd/overlay.ts b/packages/dockview-core/src/dnd/overlay.ts index 7780bb785..8f2bee430 100644 --- a/packages/dockview-core/src/dnd/overlay.ts +++ b/packages/dockview-core/src/dnd/overlay.ts @@ -11,7 +11,7 @@ import { } from '../events'; import { CompositeDisposable, MutableDisposable } from '../lifecycle'; import { clamp } from '../math'; -import { Box } from '../types'; +import { AnchoredBox } from '../types'; const bringElementToFront = (() => { let previous: HTMLElement | null = null; @@ -49,7 +49,7 @@ export class Overlay extends CompositeDisposable { } constructor( - private readonly options: Box & { + private readonly options: AnchoredBox & { container: HTMLElement; content: HTMLElement; minimumInViewportWidth?: number; @@ -78,23 +78,35 @@ export class Overlay extends CompositeDisposable { this.setBounds({ height: this.options.height, width: this.options.width, - top: this.options.top, - left: this.options.left, + ...("top" in this.options && { top: this.options.top }), + ...("bottom" in this.options && { bottom: this.options.bottom }), + ...("left" in this.options && { left: this.options.left }), + ...("right" in this.options && { right: this.options.right }) }); } - setBounds(bounds: Partial = {}): void { + setBounds(bounds: Partial = {}): void { if (typeof bounds.height === 'number') { this._element.style.height = `${bounds.height}px`; } if (typeof bounds.width === 'number') { this._element.style.width = `${bounds.width}px`; } - if (typeof bounds.top === 'number') { + if ("top" in bounds && typeof bounds.top === 'number') { this._element.style.top = `${bounds.top}px`; + this._element.style.bottom = "auto"; } - if (typeof bounds.left === 'number') { + if ("bottom" in bounds && typeof bounds.bottom === 'number') { + this._element.style.bottom = `${bounds.bottom}px`; + this._element.style.top = "auto"; + } + if ("left" in bounds && typeof bounds.left === 'number') { this._element.style.left = `${bounds.left}px`; + this._element.style.right = "auto"; + } + if ("right" in bounds && typeof bounds.right === 'number') { + this._element.style.right = `${bounds.right}px`; + this._element.style.left = "auto"; } const containerRect = this.options.container.getBoundingClientRect(); @@ -106,39 +118,77 @@ export class Overlay extends CompositeDisposable { const xOffset = Math.max(0, this.getMinimumWidth(overlayRect.width)); // a minimum height of minimumViewportHeight must be inside the viewport - const yOffset = - typeof this.options.minimumInViewportHeight === 'number' - ? Math.max(0, this.getMinimumHeight(overlayRect.height)) - : 0; + const yOffset = Math.max(0, this.getMinimumHeight(overlayRect.height)); - const left = clamp( - overlayRect.left - containerRect.left, - -xOffset, - Math.max(0, containerRect.width - overlayRect.width + xOffset) - ); + if ("top" in bounds && typeof bounds.top === 'number') { + const top = clamp( + overlayRect.top - containerRect.top, + -yOffset, + Math.max(0, containerRect.height - overlayRect.height + yOffset) + ); + this._element.style.top = `${top}px`; + this._element.style.bottom = "auto"; + } - const top = clamp( - overlayRect.top - containerRect.top, - -yOffset, - Math.max(0, containerRect.height - overlayRect.height + yOffset) - ); + if ("bottom" in bounds && typeof bounds.bottom === 'number') { + const bottom = clamp( + containerRect.bottom - overlayRect.bottom, + -yOffset, + Math.max(0, containerRect.height - overlayRect.height + yOffset) + ); + this._element.style.bottom = `${bottom}px`; + this._element.style.top = "auto"; + } - this._element.style.left = `${left}px`; - this._element.style.top = `${top}px`; + if ("left" in bounds && typeof bounds.left === 'number') { + const left = clamp( + overlayRect.left - containerRect.left, + -xOffset, + Math.max(0, containerRect.width - overlayRect.width + xOffset) + ); + this._element.style.left = `${left}px`; + this._element.style.right = "auto"; + } + + if ("right" in bounds && typeof bounds.right === 'number') { + const right = clamp( + containerRect.right - overlayRect.right, + -xOffset, + Math.max(0, containerRect.width - overlayRect.width + xOffset) + ); + this._element.style.right = `${right}px`; + this._element.style.left = "auto"; + } this._onDidChange.fire(); } - toJSON(): Box { + toJSON(): AnchoredBox { const container = this.options.container.getBoundingClientRect(); const element = this._element.getBoundingClientRect(); - return { - top: element.top - container.top, - left: element.left - container.left, - width: element.width, - height: element.height, - }; + const result: any = {}; + + if (this._element.style.top !== "auto") { + result.top = parseFloat(this._element.style.top); + } else if (this._element.style.bottom !== "auto") { + result.bottom = parseFloat(this._element.style.bottom); + } else { + result.top = element.top - container.top; + } + + if (this._element.style.left !== "auto") { + result.left = parseFloat(this._element.style.left); + } else if (this._element.style.right !== "auto") { + result.right = parseFloat(this._element.style.right); + } else { + result.left = element.left - container.left; + } + + result.width = element.width; + result.height = element.height; + + return result; } setupDrag( @@ -193,18 +243,7 @@ export class Overlay extends CompositeDisposable { ); const yOffset = Math.max( 0, - this.options.minimumInViewportHeight - ? this.getMinimumHeight(overlayRect.height) - : 0 - ); - - const left = clamp( - x - offset.x, - -xOffset, - Math.max( - 0, - containerRect.width - overlayRect.width + xOffset - ) + this.getMinimumHeight(overlayRect.height) ); const top = clamp( @@ -216,7 +255,50 @@ export class Overlay extends CompositeDisposable { ) ); - this.setBounds({ top, left }); + const bottom = clamp( + offset.y - y + containerRect.height - overlayRect.height, + -yOffset, + Math.max( + 0, + containerRect.height - overlayRect.height + yOffset + ) + ); + + const left = clamp( + x - offset.x, + -xOffset, + Math.max( + 0, + containerRect.width - overlayRect.width + xOffset + ) + ); + + const right = clamp( + offset.x - x + containerRect.width - overlayRect.width, + -xOffset, + Math.max( + 0, + containerRect.width - overlayRect.width + xOffset + ) + ); + + const bounds: any = {}; + + // Anchor to top or to bottom depending on which one is closer + if (top <= bottom) { + bounds.top = top; + } else { + bounds.bottom = bottom; + } + + // Anchor to left or to right depending on which one is closer + if (left <= right) { + bounds.left = left; + } else { + bounds.right = right; + } + + this.setBounds(bounds); }), addDisposableWindowListener(window, 'mouseup', () => { toggleClass( @@ -342,8 +424,10 @@ export class Overlay extends CompositeDisposable { } let top: number | undefined = undefined; + let bottom: number | undefined = undefined; let height: number | undefined = undefined; let left: number | undefined = undefined; + let right: number | undefined = undefined; let width: number | undefined = undefined; const moveTop = () => { @@ -353,20 +437,21 @@ export class Overlay extends CompositeDisposable { startPosition!.originalY + startPosition!.originalHeight > containerRect.height - ? this.getMinimumHeight( - containerRect.height - ) + ? this.getMinimumHeight(containerRect.height) : Math.max( - 0, - startPosition!.originalY + - startPosition!.originalHeight - - Overlay.MINIMUM_HEIGHT - ) + 0, + startPosition!.originalY + + startPosition!.originalHeight - + Overlay.MINIMUM_HEIGHT + ) ); + height = startPosition!.originalY + startPosition!.originalHeight - top; + + bottom = containerRect.height - top - height; }; const moveBottom = () => { @@ -380,10 +465,12 @@ export class Overlay extends CompositeDisposable { typeof this.options .minimumInViewportHeight === 'number' ? -top + - this.options.minimumInViewportHeight + this.options.minimumInViewportHeight : Overlay.MINIMUM_HEIGHT, Number.MAX_VALUE ); + + bottom = containerRect.height - top - height; }; const moveLeft = () => { @@ -395,17 +482,19 @@ export class Overlay extends CompositeDisposable { containerRect.width ? this.getMinimumWidth(containerRect.width) : Math.max( - 0, - startPosition!.originalX + - startPosition!.originalWidth - - Overlay.MINIMUM_WIDTH - ) + 0, + startPosition!.originalX + + startPosition!.originalWidth - + Overlay.MINIMUM_WIDTH + ) ); width = startPosition!.originalX + startPosition!.originalWidth - left; + + right = containerRect.width - left - width; }; const moveRight = () => { @@ -419,10 +508,12 @@ export class Overlay extends CompositeDisposable { typeof this.options .minimumInViewportWidth === 'number' ? -left + - this.options.minimumInViewportWidth + this.options.minimumInViewportWidth : Overlay.MINIMUM_WIDTH, Number.MAX_VALUE ); + + right = containerRect.width - left - width; }; switch (direction) { @@ -456,7 +547,26 @@ export class Overlay extends CompositeDisposable { break; } - this.setBounds({ height, width, top, left }); + const bounds: any = {}; + + // Anchor to top or to bottom depending on which one is closer + if (top! <= bottom!) { + bounds.top = top; + } else { + bounds.bottom = bottom; + } + + // Anchor to left or to right depending on which one is closer + if (left! <= right!) { + bounds.left = left; + } else { + bounds.right = right; + } + + bounds.height = height; + bounds.width = width; + + this.setBounds(bounds); }), { dispose: () => { @@ -485,7 +595,7 @@ export class Overlay extends CompositeDisposable { if (typeof this.options.minimumInViewportHeight === 'number') { return height - this.options.minimumInViewportHeight; } - return height; + return 0; } override dispose(): void { diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 8342faa9b..3815c9ce0 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -57,7 +57,7 @@ import { GroupDragEvent, TabDragEvent, } from './components/titlebar/tabsContainer'; -import { Box } from '../types'; +import { AnchoredBox, Box } from '../types'; import { DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, DEFAULT_FLOATING_GROUP_POSITION, @@ -126,7 +126,7 @@ export interface PanelReference { export interface SerializedFloatingGroup { data: GroupPanelViewState; - position: Box; + position: AnchoredBox; } export interface SerializedPopoutGroup { @@ -208,7 +208,10 @@ export interface IDockviewComponent extends IBaseGrid { // addFloatingGroup( item: IDockviewPanel | DockviewGroupPanel, - coord?: { x: number; y: number } + coord?: { x: number; y: number }, + options?: { + position?: AnchoredBox + } ): void; addPopoutGroup( item: IDockviewPanel | DockviewGroupPanel, @@ -223,8 +226,7 @@ export interface IDockviewComponent extends IBaseGrid { export class DockviewComponent extends BaseGrid - implements IDockviewComponent -{ + implements IDockviewComponent { private readonly nextGroupId = sequentialNumberGenerator(); private readonly _deserializer = new DefaultDockviewDeserialzier(this); private readonly _api: DockviewApi; @@ -750,6 +752,7 @@ export class DockviewComponent item: DockviewPanel | DockviewGroupPanel, coord?: { x?: number; y?: number; height?: number; width?: number }, options?: { + position?: AnchoredBox; skipRemoveGroup?: boolean; inDragMode: boolean; skipActiveGroup?: boolean; @@ -814,34 +817,70 @@ export class DockviewComponent group.model.location = { type: 'floating' }; - const overlayLeft = - typeof coord?.x === 'number' - ? Math.max(coord.x, 0) - : DEFAULT_FLOATING_GROUP_POSITION.left; - const overlayTop = - typeof coord?.y === 'number' - ? Math.max(coord.y, 0) - : DEFAULT_FLOATING_GROUP_POSITION.top; + function getAnchoredBox(): AnchoredBox { + if (options?.position) { + const result: any = {}; + if ("left" in options.position) { + result.left = Math.max(options.position.left, 0) + } else if ("right" in options.position) { + result.right = Math.max(options.position.right, 0) + } else { + result.left = DEFAULT_FLOATING_GROUP_POSITION.left; + } + if ("top" in options.position) { + result.top = Math.max(options.position.top, 0) + } else if ("bottom" in options.position) { + result.bottom = Math.max(options.position.bottom, 0) + } else { + result.top = DEFAULT_FLOATING_GROUP_POSITION.top; + } + if ("width" in options.position) { + result.width = Math.max(options.position.width, 0) + } else { + result.width = DEFAULT_FLOATING_GROUP_POSITION.width; + } + if ("height" in options.position) { + result.height = Math.max(options.position.height, 0) + } else { + result.height = DEFAULT_FLOATING_GROUP_POSITION.height; + } + return result as AnchoredBox; + } + + return { + left: typeof coord?.x === 'number' + ? Math.max(coord.x, 0) + : DEFAULT_FLOATING_GROUP_POSITION.left, + top: typeof coord?.y === 'number' + ? Math.max(coord.y, 0) + : DEFAULT_FLOATING_GROUP_POSITION.top, + width: typeof coord?.width === 'number' + ? Math.max(coord.width, 0) + : DEFAULT_FLOATING_GROUP_POSITION.width, + height: typeof coord?.height === 'number' + ? Math.max(coord.height, 0) + : DEFAULT_FLOATING_GROUP_POSITION.height, + } + } + + const anchoredBox = getAnchoredBox(); const overlay = new Overlay({ container: this.gridview.element, content: group.element, - height: coord?.height ?? 300, - width: coord?.width ?? 300, - left: overlayLeft, - top: overlayTop, + ...anchoredBox, minimumInViewportWidth: this.options.floatingGroupBounds === 'boundedWithinViewport' ? undefined : this.options.floatingGroupBounds - ?.minimumWidthWithinViewport ?? - DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, + ?.minimumWidthWithinViewport ?? + DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, minimumInViewportHeight: this.options.floatingGroupBounds === 'boundedWithinViewport' ? undefined : this.options.floatingGroupBounds - ?.minimumHeightWithinViewport ?? - DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, + ?.minimumHeightWithinViewport ?? + DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, }); const el = group.element.querySelector('.void-container'); @@ -1194,13 +1233,8 @@ export class DockviewComponent this.addFloatingGroup( group, - { - x: position.left, - y: position.top, - height: position.height, - width: position.width, - }, - { skipRemoveGroup: true, inDragMode: false } + undefined, + { position: position, skipRemoveGroup: true, inDragMode: false } ); } @@ -1338,7 +1372,7 @@ export class DockviewComponent referenceGroup = typeof options.position.referenceGroup === 'string' ? this._groups.get(options.position.referenceGroup) - ?.value + ?.value : options.position.referenceGroup; if (!referenceGroup) { @@ -1380,7 +1414,7 @@ export class DockviewComponent const o = typeof options.floating === 'object' && - options.floating !== null + options.floating !== null ? options.floating : {}; @@ -1433,7 +1467,7 @@ export class DockviewComponent const coordinates = typeof options.floating === 'object' && - options.floating !== null + options.floating !== null ? options.floating : {}; @@ -1471,9 +1505,9 @@ export class DockviewComponent skipDispose: boolean; skipSetActiveGroup?: boolean; } = { - removeEmptyGroup: true, - skipDispose: false, - } + removeEmptyGroup: true, + skipDispose: false, + } ): void { const group = panel.group; @@ -1538,8 +1572,8 @@ export class DockviewComponent const referencePanel = typeof options.referencePanel === 'string' ? this.panels.find( - (panel) => panel.id === options.referencePanel - ) + (panel) => panel.id === options.referencePanel + ) : options.referencePanel; if (!referencePanel) { @@ -1604,11 +1638,11 @@ export class DockviewComponent group: DockviewGroupPanel, options?: | { - skipActive?: boolean; - skipDispose?: boolean; - skipPopoutAssociated?: boolean; - skipPopoutReturn?: boolean; - } + skipActive?: boolean; + skipDispose?: boolean; + skipPopoutAssociated?: boolean; + skipPopoutReturn?: boolean; + } | undefined ): void { this.doRemoveGroup(group, options); @@ -1618,11 +1652,11 @@ export class DockviewComponent group: DockviewGroupPanel, options?: | { - skipActive?: boolean; - skipDispose?: boolean; - skipPopoutAssociated?: boolean; - skipPopoutReturn?: boolean; - } + skipActive?: boolean; + skipDispose?: boolean; + skipPopoutAssociated?: boolean; + skipPopoutReturn?: boolean; + } | undefined ): DockviewGroupPanel { const panels = [...group.panels]; // reassign since group panels will mutate diff --git a/packages/dockview-core/src/dockview/dockviewFloatingGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewFloatingGroupPanel.ts index 65d768c94..9f4dacaa9 100644 --- a/packages/dockview-core/src/dockview/dockviewFloatingGroupPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewFloatingGroupPanel.ts @@ -1,37 +1,22 @@ import { Overlay } from '../dnd/overlay'; import { CompositeDisposable } from '../lifecycle'; +import { AnchoredBox } from '../types'; import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel'; export interface IDockviewFloatingGroupPanel { readonly group: IDockviewGroupPanel; - position( - bounds: Partial<{ - top: number; - left: number; - height: number; - width: number; - }> - ): void; + position(bounds: Partial): void; } export class DockviewFloatingGroupPanel extends CompositeDisposable - implements IDockviewFloatingGroupPanel -{ + implements IDockviewFloatingGroupPanel { constructor(readonly group: DockviewGroupPanel, readonly overlay: Overlay) { super(); - this.addDisposables(overlay); } - position( - bounds: Partial<{ - top: number; - left: number; - height: number; - width: number; - }> - ): void { + position(bounds: Partial): void { this.overlay.setBounds(bounds); } } diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 5791730af..e39bffe4d 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -14,6 +14,7 @@ import { import { IDockviewPanel } from './dockviewPanel'; import { DockviewPanelRenderer } from '../overlayRenderContainer'; import { IGroupHeaderProps } from './framework'; +import { AnchoredBox } from '../types'; export interface IHeaderActionsRenderer extends IDisposable { readonly element: HTMLElement; @@ -37,11 +38,11 @@ export interface DockviewOptions { singleTabMode?: 'fullwidth' | 'default'; disableFloatingGroups?: boolean; floatingGroupBounds?: - | 'boundedWithinViewport' - | { - minimumHeightWithinViewport?: number; - minimumWidthWithinViewport?: number; - }; + | 'boundedWithinViewport' + | { + minimumHeightWithinViewport?: number; + minimumWidthWithinViewport?: number; + }; popoutUrl?: string; defaultRenderer?: DockviewPanelRenderer; debug?: boolean; @@ -74,7 +75,7 @@ export class DockviewUnhandledDragOverEvent implements DockviewDndOverlayEvent { readonly position: Position, readonly getData: () => PanelTransfer | undefined, readonly group?: DockviewGroupPanel - ) {} + ) { } accept(): void { this._isAccepted = true; @@ -176,13 +177,8 @@ export function isPanelOptionsWithGroup( type AddPanelFloatingGroupUnion = { floating: - | { - height?: number; - width?: number; - x?: number; - y?: number; - } - | true; + | Partial + | true; position: never; }; diff --git a/packages/dockview-core/src/types.ts b/packages/dockview-core/src/types.ts index ed669f694..acb170c8b 100644 --- a/packages/dockview-core/src/types.ts +++ b/packages/dockview-core/src/types.ts @@ -8,3 +8,7 @@ export interface Box { height: number; width: number; } + +export type AnchoredBox = + ({ top: number, height: number } | { bottom: number, height: number }) & + ({ left: number, width: number } | { right: number, width: number }); \ No newline at end of file diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/panelActions.tsx b/packages/docs/sandboxes/react/dockview/demo-dockview/src/panelActions.tsx index f0e9b3f0d..02638a0ce 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/panelActions.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/panelActions.tsx @@ -29,7 +29,14 @@ const PanelAction = (props: { onClick={() => { const panel = props.api.getPanel(props.panelId); if (panel) { - props.api.addFloatingGroup(panel); + props.api.addFloatingGroup(panel, undefined, { + position: { + width: 400, + height: 300, + bottom: 20, + right: 20, + } + }); } }} > diff --git a/packages/docs/templates/dockview/demo-dockview/react/src/defaultLayout.ts b/packages/docs/templates/dockview/demo-dockview/react/src/defaultLayout.ts index 9e5be7eb1..568d8093b 100644 --- a/packages/docs/templates/dockview/demo-dockview/react/src/defaultLayout.ts +++ b/packages/docs/templates/dockview/demo-dockview/react/src/defaultLayout.ts @@ -14,7 +14,7 @@ export function defaultConfig(api: DockviewApi) { id: 'panel_1', component: 'default', renderer: 'always', - title: 'Panel 1', + title: 'Panel 1' }); api.addPanel({