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';
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');

View File

@ -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<SerializedSplitview> {
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<SerializedPaneview> {
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<SerializedGridviewComponent> {
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<SerializedDockview> {
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<SerializedDockview> {
*/
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);
}
/**

View File

@ -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 };

View File

@ -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<Box> = {}): void {
setBounds(bounds: Partial<AnchoredBox> = {}): 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 {

View File

@ -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<DockviewGroupPanel> {
//
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<DockviewGroupPanel> {
export class DockviewComponent
extends BaseGrid<DockviewGroupPanel>
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

View File

@ -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<AnchoredBox>): 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<AnchoredBox>): void {
this.overlay.setBounds(bounds);
}
}

View File

@ -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<AnchoredBox>
| true;
position: never;
};

View File

@ -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 });

View File

@ -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,
}
});
}
}}
>

View File

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