Support bottom and right anchor for Overlay

This commit is contained in:
Vincent Lecrubier 2024-06-03 11:25:38 +01:00
parent ce381f8ce9
commit bbdb99dbb5
10 changed files with 422 additions and 157 deletions

View File

@ -1,7 +1,14 @@
import { Overlay } from '../../dnd/overlay'; 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', () => { describe('overlay', () => {
test('toJSON', () => { test('toJSON, top left', () => {
const container = document.createElement('div'); const container = document.createElement('div');
const content = document.createElement('div'); const content = document.createElement('div');
@ -23,11 +30,11 @@ describe('overlay', () => {
container.childNodes.item(0) as HTMLElement, container.childNodes.item(0) as HTMLElement,
'getBoundingClientRect' 'getBoundingClientRect'
).mockImplementation(() => { ).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( 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 container = document.createElement('div');
const content = document.createElement('div'); const content = document.createElement('div');
@ -61,11 +106,11 @@ describe('overlay', () => {
container.childNodes.item(0) as HTMLElement, container.childNodes.item(0) as HTMLElement,
'getBoundingClientRect' 'getBoundingClientRect'
).mockImplementation(() => { ).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( 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 container = document.createElement('div');
const content = document.createElement('div'); const content = document.createElement('div');
@ -101,11 +184,11 @@ describe('overlay', () => {
expect(element).toBeTruthy(); expect(element).toBeTruthy();
jest.spyOn(element, 'getBoundingClientRect').mockImplementation(() => { 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( 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'); 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', () => { test('that the resize handles are added', () => {
const container = document.createElement('div'); const container = document.createElement('div');
const content = document.createElement('div'); const content = document.createElement('div');

View File

@ -42,7 +42,7 @@ import {
GroupDragEvent, GroupDragEvent,
TabDragEvent, TabDragEvent,
} from '../dockview/components/titlebar/tabsContainer'; } from '../dockview/components/titlebar/tabsContainer';
import { Box } from '../types'; import { AnchoredBox, Box } from '../types';
import { import {
DockviewDidDropEvent, DockviewDidDropEvent,
DockviewWillDropEvent, DockviewWillDropEvent,
@ -139,7 +139,7 @@ export class SplitviewApi implements CommonApi<SerializedSplitview> {
return this.component.onDidRemoveView; return this.component.onDidRemoveView;
} }
constructor(private readonly component: ISplitviewComponent) {} constructor(private readonly component: ISplitviewComponent) { }
/** /**
* Update configuratable options. * Update configuratable options.
@ -295,7 +295,7 @@ export class PaneviewApi implements CommonApi<SerializedPaneview> {
return emitter.event; return emitter.event;
} }
constructor(private readonly component: IPaneviewComponent) {} constructor(private readonly component: IPaneviewComponent) { }
/** /**
* Remove a panel given the panel object. * Remove a panel given the panel object.
@ -459,7 +459,7 @@ export class GridviewApi implements CommonApi<SerializedGridviewComponent> {
this.component.updateOptions({ orientation: value }); 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. * Focus the component. Will try to focus an active panel if one exists.
@ -728,7 +728,7 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.activeGroup; 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. * Focus the component. Will try to focus an active panel if one exists.
@ -800,9 +800,15 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
*/ */
addFloatingGroup( addFloatingGroup(
item: IDockviewPanel | DockviewGroupPanel, item: IDockviewPanel | DockviewGroupPanel,
coord?: { x: number; y: number } coord?: { x: number; y: number },
options?: {
position?: AnchoredBox;
skipRemoveGroup?: boolean;
inDragMode?: boolean;
skipActiveGroup?: boolean;
}
): void { ): void {
return this.component.addFloatingGroup(item, coord); return this.component.addFloatingGroup(item, coord, options);
} }
/** /**

View File

@ -1,3 +1,3 @@
export const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100; 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 };

View File

@ -11,7 +11,7 @@ import {
} from '../events'; } from '../events';
import { CompositeDisposable, MutableDisposable } from '../lifecycle'; import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { clamp } from '../math'; import { clamp } from '../math';
import { Box } from '../types'; import { AnchoredBox } from '../types';
const bringElementToFront = (() => { const bringElementToFront = (() => {
let previous: HTMLElement | null = null; let previous: HTMLElement | null = null;
@ -49,7 +49,7 @@ export class Overlay extends CompositeDisposable {
} }
constructor( constructor(
private readonly options: Box & { private readonly options: AnchoredBox & {
container: HTMLElement; container: HTMLElement;
content: HTMLElement; content: HTMLElement;
minimumInViewportWidth?: number; minimumInViewportWidth?: number;
@ -78,23 +78,35 @@ export class Overlay extends CompositeDisposable {
this.setBounds({ this.setBounds({
height: this.options.height, height: this.options.height,
width: this.options.width, width: this.options.width,
top: this.options.top, ...("top" in this.options && { top: this.options.top }),
left: this.options.left, ...("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<Box> = {}): void { setBounds(bounds: Partial<AnchoredBox> = {}): void {
if (typeof bounds.height === 'number') { if (typeof bounds.height === 'number') {
this._element.style.height = `${bounds.height}px`; this._element.style.height = `${bounds.height}px`;
} }
if (typeof bounds.width === 'number') { if (typeof bounds.width === 'number') {
this._element.style.width = `${bounds.width}px`; 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.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.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(); const containerRect = this.options.container.getBoundingClientRect();
@ -106,39 +118,77 @@ export class Overlay extends CompositeDisposable {
const xOffset = Math.max(0, this.getMinimumWidth(overlayRect.width)); const xOffset = Math.max(0, this.getMinimumWidth(overlayRect.width));
// a minimum height of minimumViewportHeight must be inside the viewport // a minimum height of minimumViewportHeight must be inside the viewport
const yOffset = const yOffset = Math.max(0, this.getMinimumHeight(overlayRect.height));
typeof this.options.minimumInViewportHeight === 'number'
? Math.max(0, this.getMinimumHeight(overlayRect.height))
: 0;
const left = clamp( if ("top" in bounds && typeof bounds.top === 'number') {
overlayRect.left - containerRect.left, const top = clamp(
-xOffset, overlayRect.top - containerRect.top,
Math.max(0, containerRect.width - overlayRect.width + xOffset) -yOffset,
); Math.max(0, containerRect.height - overlayRect.height + yOffset)
);
this._element.style.top = `${top}px`;
this._element.style.bottom = "auto";
}
const top = clamp( if ("bottom" in bounds && typeof bounds.bottom === 'number') {
overlayRect.top - containerRect.top, const bottom = clamp(
-yOffset, containerRect.bottom - overlayRect.bottom,
Math.max(0, containerRect.height - overlayRect.height + yOffset) -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`; if ("left" in bounds && typeof bounds.left === 'number') {
this._element.style.top = `${top}px`; 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(); this._onDidChange.fire();
} }
toJSON(): Box { toJSON(): AnchoredBox {
const container = this.options.container.getBoundingClientRect(); const container = this.options.container.getBoundingClientRect();
const element = this._element.getBoundingClientRect(); const element = this._element.getBoundingClientRect();
return { const result: any = {};
top: element.top - container.top,
left: element.left - container.left, if (this._element.style.top !== "auto") {
width: element.width, result.top = parseFloat(this._element.style.top);
height: element.height, } 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( setupDrag(
@ -193,18 +243,7 @@ export class Overlay extends CompositeDisposable {
); );
const yOffset = Math.max( const yOffset = Math.max(
0, 0,
this.options.minimumInViewportHeight this.getMinimumHeight(overlayRect.height)
? this.getMinimumHeight(overlayRect.height)
: 0
);
const left = clamp(
x - offset.x,
-xOffset,
Math.max(
0,
containerRect.width - overlayRect.width + xOffset
)
); );
const top = clamp( 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', () => { addDisposableWindowListener(window, 'mouseup', () => {
toggleClass( toggleClass(
@ -342,8 +424,10 @@ export class Overlay extends CompositeDisposable {
} }
let top: number | undefined = undefined; let top: number | undefined = undefined;
let bottom: number | undefined = undefined;
let height: number | undefined = undefined; let height: number | undefined = undefined;
let left: number | undefined = undefined; let left: number | undefined = undefined;
let right: number | undefined = undefined;
let width: number | undefined = undefined; let width: number | undefined = undefined;
const moveTop = () => { const moveTop = () => {
@ -353,20 +437,21 @@ export class Overlay extends CompositeDisposable {
startPosition!.originalY + startPosition!.originalY +
startPosition!.originalHeight > startPosition!.originalHeight >
containerRect.height containerRect.height
? this.getMinimumHeight( ? this.getMinimumHeight(containerRect.height)
containerRect.height
)
: Math.max( : Math.max(
0, 0,
startPosition!.originalY + startPosition!.originalY +
startPosition!.originalHeight - startPosition!.originalHeight -
Overlay.MINIMUM_HEIGHT Overlay.MINIMUM_HEIGHT
) )
); );
height = height =
startPosition!.originalY + startPosition!.originalY +
startPosition!.originalHeight - startPosition!.originalHeight -
top; top;
bottom = containerRect.height - top - height;
}; };
const moveBottom = () => { const moveBottom = () => {
@ -380,10 +465,12 @@ export class Overlay extends CompositeDisposable {
typeof this.options typeof this.options
.minimumInViewportHeight === 'number' .minimumInViewportHeight === 'number'
? -top + ? -top +
this.options.minimumInViewportHeight this.options.minimumInViewportHeight
: Overlay.MINIMUM_HEIGHT, : Overlay.MINIMUM_HEIGHT,
Number.MAX_VALUE Number.MAX_VALUE
); );
bottom = containerRect.height - top - height;
}; };
const moveLeft = () => { const moveLeft = () => {
@ -395,17 +482,19 @@ export class Overlay extends CompositeDisposable {
containerRect.width containerRect.width
? this.getMinimumWidth(containerRect.width) ? this.getMinimumWidth(containerRect.width)
: Math.max( : Math.max(
0, 0,
startPosition!.originalX + startPosition!.originalX +
startPosition!.originalWidth - startPosition!.originalWidth -
Overlay.MINIMUM_WIDTH Overlay.MINIMUM_WIDTH
) )
); );
width = width =
startPosition!.originalX + startPosition!.originalX +
startPosition!.originalWidth - startPosition!.originalWidth -
left; left;
right = containerRect.width - left - width;
}; };
const moveRight = () => { const moveRight = () => {
@ -419,10 +508,12 @@ export class Overlay extends CompositeDisposable {
typeof this.options typeof this.options
.minimumInViewportWidth === 'number' .minimumInViewportWidth === 'number'
? -left + ? -left +
this.options.minimumInViewportWidth this.options.minimumInViewportWidth
: Overlay.MINIMUM_WIDTH, : Overlay.MINIMUM_WIDTH,
Number.MAX_VALUE Number.MAX_VALUE
); );
right = containerRect.width - left - width;
}; };
switch (direction) { switch (direction) {
@ -456,7 +547,26 @@ export class Overlay extends CompositeDisposable {
break; 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: () => { dispose: () => {
@ -485,7 +595,7 @@ export class Overlay extends CompositeDisposable {
if (typeof this.options.minimumInViewportHeight === 'number') { if (typeof this.options.minimumInViewportHeight === 'number') {
return height - this.options.minimumInViewportHeight; return height - this.options.minimumInViewportHeight;
} }
return height; return 0;
} }
override dispose(): void { override dispose(): void {

View File

@ -57,7 +57,7 @@ import {
GroupDragEvent, GroupDragEvent,
TabDragEvent, TabDragEvent,
} from './components/titlebar/tabsContainer'; } from './components/titlebar/tabsContainer';
import { Box } from '../types'; import { AnchoredBox, Box } from '../types';
import { import {
DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE,
DEFAULT_FLOATING_GROUP_POSITION, DEFAULT_FLOATING_GROUP_POSITION,
@ -126,7 +126,7 @@ export interface PanelReference {
export interface SerializedFloatingGroup { export interface SerializedFloatingGroup {
data: GroupPanelViewState; data: GroupPanelViewState;
position: Box; position: AnchoredBox;
} }
export interface SerializedPopoutGroup { export interface SerializedPopoutGroup {
@ -208,7 +208,10 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
// //
addFloatingGroup( addFloatingGroup(
item: IDockviewPanel | DockviewGroupPanel, item: IDockviewPanel | DockviewGroupPanel,
coord?: { x: number; y: number } coord?: { x: number; y: number },
options?: {
position?: AnchoredBox
}
): void; ): void;
addPopoutGroup( addPopoutGroup(
item: IDockviewPanel | DockviewGroupPanel, item: IDockviewPanel | DockviewGroupPanel,
@ -223,8 +226,7 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
export class DockviewComponent export class DockviewComponent
extends BaseGrid<DockviewGroupPanel> extends BaseGrid<DockviewGroupPanel>
implements IDockviewComponent implements IDockviewComponent {
{
private readonly nextGroupId = sequentialNumberGenerator(); private readonly nextGroupId = sequentialNumberGenerator();
private readonly _deserializer = new DefaultDockviewDeserialzier(this); private readonly _deserializer = new DefaultDockviewDeserialzier(this);
private readonly _api: DockviewApi; private readonly _api: DockviewApi;
@ -750,6 +752,7 @@ export class DockviewComponent
item: DockviewPanel | DockviewGroupPanel, item: DockviewPanel | DockviewGroupPanel,
coord?: { x?: number; y?: number; height?: number; width?: number }, coord?: { x?: number; y?: number; height?: number; width?: number },
options?: { options?: {
position?: AnchoredBox;
skipRemoveGroup?: boolean; skipRemoveGroup?: boolean;
inDragMode: boolean; inDragMode: boolean;
skipActiveGroup?: boolean; skipActiveGroup?: boolean;
@ -814,34 +817,70 @@ export class DockviewComponent
group.model.location = { type: 'floating' }; group.model.location = { type: 'floating' };
const overlayLeft = function getAnchoredBox(): AnchoredBox {
typeof coord?.x === 'number' if (options?.position) {
? Math.max(coord.x, 0) const result: any = {};
: DEFAULT_FLOATING_GROUP_POSITION.left; if ("left" in options.position) {
const overlayTop = result.left = Math.max(options.position.left, 0)
typeof coord?.y === 'number' } else if ("right" in options.position) {
? Math.max(coord.y, 0) result.right = Math.max(options.position.right, 0)
: DEFAULT_FLOATING_GROUP_POSITION.top; } 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({ const overlay = new Overlay({
container: this.gridview.element, container: this.gridview.element,
content: group.element, content: group.element,
height: coord?.height ?? 300, ...anchoredBox,
width: coord?.width ?? 300,
left: overlayLeft,
top: overlayTop,
minimumInViewportWidth: minimumInViewportWidth:
this.options.floatingGroupBounds === 'boundedWithinViewport' this.options.floatingGroupBounds === 'boundedWithinViewport'
? undefined ? undefined
: this.options.floatingGroupBounds : this.options.floatingGroupBounds
?.minimumWidthWithinViewport ?? ?.minimumWidthWithinViewport ??
DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE,
minimumInViewportHeight: minimumInViewportHeight:
this.options.floatingGroupBounds === 'boundedWithinViewport' this.options.floatingGroupBounds === 'boundedWithinViewport'
? undefined ? undefined
: this.options.floatingGroupBounds : this.options.floatingGroupBounds
?.minimumHeightWithinViewport ?? ?.minimumHeightWithinViewport ??
DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE,
}); });
const el = group.element.querySelector('.void-container'); const el = group.element.querySelector('.void-container');
@ -1194,13 +1233,8 @@ export class DockviewComponent
this.addFloatingGroup( this.addFloatingGroup(
group, group,
{ undefined,
x: position.left, { position: position, skipRemoveGroup: true, inDragMode: false }
y: position.top,
height: position.height,
width: position.width,
},
{ skipRemoveGroup: true, inDragMode: false }
); );
} }
@ -1338,7 +1372,7 @@ export class DockviewComponent
referenceGroup = referenceGroup =
typeof options.position.referenceGroup === 'string' typeof options.position.referenceGroup === 'string'
? this._groups.get(options.position.referenceGroup) ? this._groups.get(options.position.referenceGroup)
?.value ?.value
: options.position.referenceGroup; : options.position.referenceGroup;
if (!referenceGroup) { if (!referenceGroup) {
@ -1380,7 +1414,7 @@ export class DockviewComponent
const o = const o =
typeof options.floating === 'object' && typeof options.floating === 'object' &&
options.floating !== null options.floating !== null
? options.floating ? options.floating
: {}; : {};
@ -1433,7 +1467,7 @@ export class DockviewComponent
const coordinates = const coordinates =
typeof options.floating === 'object' && typeof options.floating === 'object' &&
options.floating !== null options.floating !== null
? options.floating ? options.floating
: {}; : {};
@ -1471,9 +1505,9 @@ export class DockviewComponent
skipDispose: boolean; skipDispose: boolean;
skipSetActiveGroup?: boolean; skipSetActiveGroup?: boolean;
} = { } = {
removeEmptyGroup: true, removeEmptyGroup: true,
skipDispose: false, skipDispose: false,
} }
): void { ): void {
const group = panel.group; const group = panel.group;
@ -1538,8 +1572,8 @@ export class DockviewComponent
const referencePanel = const referencePanel =
typeof options.referencePanel === 'string' typeof options.referencePanel === 'string'
? this.panels.find( ? this.panels.find(
(panel) => panel.id === options.referencePanel (panel) => panel.id === options.referencePanel
) )
: options.referencePanel; : options.referencePanel;
if (!referencePanel) { if (!referencePanel) {
@ -1604,11 +1638,11 @@ export class DockviewComponent
group: DockviewGroupPanel, group: DockviewGroupPanel,
options?: options?:
| { | {
skipActive?: boolean; skipActive?: boolean;
skipDispose?: boolean; skipDispose?: boolean;
skipPopoutAssociated?: boolean; skipPopoutAssociated?: boolean;
skipPopoutReturn?: boolean; skipPopoutReturn?: boolean;
} }
| undefined | undefined
): void { ): void {
this.doRemoveGroup(group, options); this.doRemoveGroup(group, options);
@ -1618,11 +1652,11 @@ export class DockviewComponent
group: DockviewGroupPanel, group: DockviewGroupPanel,
options?: options?:
| { | {
skipActive?: boolean; skipActive?: boolean;
skipDispose?: boolean; skipDispose?: boolean;
skipPopoutAssociated?: boolean; skipPopoutAssociated?: boolean;
skipPopoutReturn?: boolean; skipPopoutReturn?: boolean;
} }
| undefined | undefined
): DockviewGroupPanel { ): DockviewGroupPanel {
const panels = [...group.panels]; // reassign since group panels will mutate const panels = [...group.panels]; // reassign since group panels will mutate

View File

@ -1,37 +1,22 @@
import { Overlay } from '../dnd/overlay'; import { Overlay } from '../dnd/overlay';
import { CompositeDisposable } from '../lifecycle'; import { CompositeDisposable } from '../lifecycle';
import { AnchoredBox } from '../types';
import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel'; import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel';
export interface IDockviewFloatingGroupPanel { export interface IDockviewFloatingGroupPanel {
readonly group: IDockviewGroupPanel; readonly group: IDockviewGroupPanel;
position( position(bounds: Partial<AnchoredBox>): void;
bounds: Partial<{
top: number;
left: number;
height: number;
width: number;
}>
): void;
} }
export class DockviewFloatingGroupPanel export class DockviewFloatingGroupPanel
extends CompositeDisposable extends CompositeDisposable
implements IDockviewFloatingGroupPanel implements IDockviewFloatingGroupPanel {
{
constructor(readonly group: DockviewGroupPanel, readonly overlay: Overlay) { constructor(readonly group: DockviewGroupPanel, readonly overlay: Overlay) {
super(); super();
this.addDisposables(overlay); this.addDisposables(overlay);
} }
position( position(bounds: Partial<AnchoredBox>): void {
bounds: Partial<{
top: number;
left: number;
height: number;
width: number;
}>
): void {
this.overlay.setBounds(bounds); this.overlay.setBounds(bounds);
} }
} }

View File

@ -14,6 +14,7 @@ import {
import { IDockviewPanel } from './dockviewPanel'; import { IDockviewPanel } from './dockviewPanel';
import { DockviewPanelRenderer } from '../overlayRenderContainer'; import { DockviewPanelRenderer } from '../overlayRenderContainer';
import { IGroupHeaderProps } from './framework'; import { IGroupHeaderProps } from './framework';
import { AnchoredBox } from '../types';
export interface IHeaderActionsRenderer extends IDisposable { export interface IHeaderActionsRenderer extends IDisposable {
readonly element: HTMLElement; readonly element: HTMLElement;
@ -37,11 +38,11 @@ export interface DockviewOptions {
singleTabMode?: 'fullwidth' | 'default'; singleTabMode?: 'fullwidth' | 'default';
disableFloatingGroups?: boolean; disableFloatingGroups?: boolean;
floatingGroupBounds?: floatingGroupBounds?:
| 'boundedWithinViewport' | 'boundedWithinViewport'
| { | {
minimumHeightWithinViewport?: number; minimumHeightWithinViewport?: number;
minimumWidthWithinViewport?: number; minimumWidthWithinViewport?: number;
}; };
popoutUrl?: string; popoutUrl?: string;
defaultRenderer?: DockviewPanelRenderer; defaultRenderer?: DockviewPanelRenderer;
debug?: boolean; debug?: boolean;
@ -74,7 +75,7 @@ export class DockviewUnhandledDragOverEvent implements DockviewDndOverlayEvent {
readonly position: Position, readonly position: Position,
readonly getData: () => PanelTransfer | undefined, readonly getData: () => PanelTransfer | undefined,
readonly group?: DockviewGroupPanel readonly group?: DockviewGroupPanel
) {} ) { }
accept(): void { accept(): void {
this._isAccepted = true; this._isAccepted = true;
@ -176,13 +177,8 @@ export function isPanelOptionsWithGroup(
type AddPanelFloatingGroupUnion = { type AddPanelFloatingGroupUnion = {
floating: floating:
| { | Partial<AnchoredBox>
height?: number; | true;
width?: number;
x?: number;
y?: number;
}
| true;
position: never; position: never;
}; };

View File

@ -8,3 +8,7 @@ export interface Box {
height: number; height: number;
width: number; width: number;
} }
export type AnchoredBox =
({ top: number, height: number } | { bottom: number, height: number }) &
({ left: number, width: number } | { right: number, width: number });

View File

@ -29,7 +29,14 @@ const PanelAction = (props: {
onClick={() => { onClick={() => {
const panel = props.api.getPanel(props.panelId); const panel = props.api.getPanel(props.panelId);
if (panel) { if (panel) {
props.api.addFloatingGroup(panel); props.api.addFloatingGroup(panel, undefined, {
position: {
width: 400,
height: 300,
bottom: 20,
right: 20,
}
});
} }
}} }}
> >

View File

@ -14,7 +14,7 @@ export function defaultConfig(api: DockviewApi) {
id: 'panel_1', id: 'panel_1',
component: 'default', component: 'default',
renderer: 'always', renderer: 'always',
title: 'Panel 1', title: 'Panel 1'
}); });
api.addPanel({ api.addPanel({