Merge pull request #628 from mathuo/544-floatinggroups-support-for-css-absolute-box-attributes-bottom-and-right

544 floatinggroups support for css absolute box attributes bottom and right
This commit is contained in:
mathuo 2024-07-03 22:18:34 +01:00 committed by GitHub
commit f20b5285da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1545 additions and 228 deletions

View File

@ -1,7 +1,34 @@
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,14 +50,26 @@ 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,
});
} }
); );
cut.setBounds();
expect(cut.toJSON()).toEqual({ expect(cut.toJSON()).toEqual({
top: 70, top: 70,
left: 60, left: 60,
@ -39,7 +78,57 @@ 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,
});
}
);
cut.setBounds();
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,14 +150,26 @@ 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,
});
} }
); );
cut.setBounds();
expect(cut.toJSON()).toEqual({ expect(cut.toJSON()).toEqual({
top: 70, top: 70,
left: 60, left: 60,
@ -77,7 +178,57 @@ 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,
});
}
);
cut.setBounds();
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 +252,21 @@ 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 +278,56 @@ 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

@ -490,14 +490,11 @@ describe('tabsContainer', () => {
const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault'); const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault');
fireEvent(container, event); fireEvent(container, event);
expect(accessor.addFloatingGroup).toHaveBeenCalledWith( expect(accessor.addFloatingGroup).toHaveBeenCalledWith(groupPanel, {
groupPanel, x: 100,
{ y: 60,
x: 100, inDragMode: true,
y: 60, });
},
{ inDragMode: true }
);
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(1); expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(1);
expect(eventPreventDefaultSpy).toHaveBeenCalledTimes(1); expect(eventPreventDefaultSpy).toHaveBeenCalledTimes(1);

View File

@ -1,4 +1,5 @@
import { import {
FloatingGroupOptions,
IDockviewComponent, IDockviewComponent,
SerializedDockview, SerializedDockview,
} from '../dockview/dockviewComponent'; } from '../dockview/dockviewComponent';
@ -42,7 +43,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,
@ -800,9 +801,9 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
*/ */
addFloatingGroup( addFloatingGroup(
item: IDockviewPanel | DockviewGroupPanel, item: IDockviewPanel | DockviewGroupPanel,
coord?: { x: number; y: number } options?: FloatingGroupOptions
): void { ): void {
return this.component.addFloatingGroup(item, coord); return this.component.addFloatingGroup(item, 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;
@ -40,6 +40,9 @@ export class Overlay extends CompositeDisposable {
private static MINIMUM_HEIGHT = 20; private static MINIMUM_HEIGHT = 20;
private static MINIMUM_WIDTH = 20; private static MINIMUM_WIDTH = 20;
private verticalAlignment: 'top' | 'bottom' | undefined;
private horiziontalAlignment: 'left' | 'right' | undefined;
set minimumInViewportWidth(value: number | undefined) { set minimumInViewportWidth(value: number | undefined) {
this.options.minimumInViewportWidth = value; this.options.minimumInViewportWidth = value;
} }
@ -49,7 +52,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 +81,39 @@ 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';
this.verticalAlignment = 'top';
} }
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';
this.verticalAlignment = 'bottom';
}
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';
this.horiziontalAlignment = 'left';
}
if ('right' in bounds && typeof bounds.right === 'number') {
this._element.style.right = `${bounds.right}px`;
this._element.style.left = 'auto';
this.horiziontalAlignment = 'right';
} }
const containerRect = this.options.container.getBoundingClientRect(); const containerRect = this.options.container.getBoundingClientRect();
@ -106,39 +125,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 (this.verticalAlignment === 'top') {
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 (this.verticalAlignment === 'bottom') {
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 (this.horiziontalAlignment === 'left') {
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 (this.horiziontalAlignment === 'right') {
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.verticalAlignment === 'top') {
width: element.width, result.top = parseFloat(this._element.style.top);
height: element.height, } else if (this.verticalAlignment === 'bottom') {
}; result.bottom = parseFloat(this._element.style.bottom);
} else {
result.top = element.top - container.top;
}
if (this.horiziontalAlignment === 'left') {
result.left = parseFloat(this._element.style.left);
} else if (this.horiziontalAlignment === 'right') {
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 +250,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 +262,53 @@ 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 +434,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 = () => {
@ -363,10 +457,13 @@ export class Overlay extends CompositeDisposable {
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 = () => {
@ -384,6 +481,8 @@ export class Overlay extends CompositeDisposable {
: Overlay.MINIMUM_HEIGHT, : Overlay.MINIMUM_HEIGHT,
Number.MAX_VALUE Number.MAX_VALUE
); );
bottom = containerRect.height - top - height;
}; };
const moveLeft = () => { const moveLeft = () => {
@ -406,6 +505,8 @@ export class Overlay extends CompositeDisposable {
startPosition!.originalX + startPosition!.originalX +
startPosition!.originalWidth - startPosition!.originalWidth -
left; left;
right = containerRect.width - left - width;
}; };
const moveRight = () => { const moveRight = () => {
@ -423,6 +524,8 @@ export class Overlay extends CompositeDisposable {
: Overlay.MINIMUM_WIDTH, : Overlay.MINIMUM_WIDTH,
Number.MAX_VALUE Number.MAX_VALUE
); );
right = containerRect.width - left - width;
}; };
switch (direction) { switch (direction) {
@ -456,7 +559,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 +607,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

@ -270,14 +270,11 @@ export class TabsContainer
const { top: rootTop, left: rootLeft } = const { top: rootTop, left: rootLeft } =
this.accessor.element.getBoundingClientRect(); this.accessor.element.getBoundingClientRect();
this.accessor.addFloatingGroup( this.accessor.addFloatingGroup(this.group, {
this.group, x: left - rootLeft + 20,
{ y: top - rootTop + 20,
x: left - rootLeft + 20, inDragMode: true,
y: top - rootTop + 20, });
},
{ inDragMode: true }
);
} }
} }
), ),
@ -380,14 +377,11 @@ export class TabsContainer
const { top: rootTop, left: rootLeft } = const { top: rootTop, left: rootLeft } =
this.accessor.element.getBoundingClientRect(); this.accessor.element.getBoundingClientRect();
this.accessor.addFloatingGroup( this.accessor.addFloatingGroup(panel as DockviewPanel, {
panel as DockviewPanel, x: left - rootLeft,
{ y: top - rootTop,
x: left - rootLeft, inDragMode: true,
y: top - rootTop, });
},
{ inDragMode: true }
);
return; return;
} }

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 {
@ -165,6 +165,17 @@ type MoveGroupOrPanelOptions = {
}; };
}; };
export interface FloatingGroupOptions {
x?: number;
y?: number;
height?: number;
width?: number;
position?: AnchoredBox;
skipRemoveGroup?: boolean;
inDragMode?: boolean;
skipActiveGroup?: boolean;
}
export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> { export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
readonly activePanel: IDockviewPanel | undefined; readonly activePanel: IDockviewPanel | undefined;
readonly totalPanels: number; readonly totalPanels: number;
@ -208,7 +219,7 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
// //
addFloatingGroup( addFloatingGroup(
item: IDockviewPanel | DockviewGroupPanel, item: IDockviewPanel | DockviewGroupPanel,
coord?: { x: number; y: number } options?: FloatingGroupOptions
): void; ): void;
addPopoutGroup( addPopoutGroup(
item: IDockviewPanel | DockviewGroupPanel, item: IDockviewPanel | DockviewGroupPanel,
@ -751,12 +762,7 @@ export class DockviewComponent
addFloatingGroup( addFloatingGroup(
item: DockviewPanel | DockviewGroupPanel, item: DockviewPanel | DockviewGroupPanel,
coord?: { x?: number; y?: number; height?: number; width?: number }, options?: FloatingGroupOptions
options?: {
skipRemoveGroup?: boolean;
inDragMode: boolean;
skipActiveGroup?: boolean;
}
): void { ): void {
let group: DockviewGroupPanel; let group: DockviewGroupPanel;
@ -817,22 +823,62 @@ 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 options?.x === 'number'
? Math.max(options.x, 0)
: DEFAULT_FLOATING_GROUP_POSITION.left,
top:
typeof options?.y === 'number'
? Math.max(options.y, 0)
: DEFAULT_FLOATING_GROUP_POSITION.top,
width:
typeof options?.width === 'number'
? Math.max(options.width, 0)
: DEFAULT_FLOATING_GROUP_POSITION.width,
height:
typeof options?.height === 'number'
? Math.max(options.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
@ -972,7 +1018,7 @@ export class DockviewComponent
this.options.floatingGroupBounds?.minimumWidthWithinViewport; this.options.floatingGroupBounds?.minimumWidthWithinViewport;
} }
group.overlay.setBounds({}); group.overlay.setBounds();
} }
} }
@ -1195,16 +1241,11 @@ export class DockviewComponent
const group = createGroupFromSerializedState(data); const group = createGroupFromSerializedState(data);
this.addFloatingGroup( this.addFloatingGroup(group, {
group, position: position,
{ skipRemoveGroup: true,
x: position.left, inDragMode: false,
y: position.top, });
height: position.height,
width: position.width,
},
{ skipRemoveGroup: true, inDragMode: false }
);
} }
const serializedPopoutGroups = data.popoutGroups ?? []; const serializedPopoutGroups = data.popoutGroups ?? [];
@ -1387,7 +1428,8 @@ export class DockviewComponent
? options.floating ? options.floating
: {}; : {};
this.addFloatingGroup(group, o, { this.addFloatingGroup(group, {
...o,
inDragMode: false, inDragMode: false,
skipRemoveGroup: true, skipRemoveGroup: true,
skipActiveGroup: true, skipActiveGroup: true,
@ -1440,7 +1482,8 @@ export class DockviewComponent
? options.floating ? options.floating
: {}; : {};
this.addFloatingGroup(group, coordinates, { this.addFloatingGroup(group, {
...coordinates,
inDragMode: false, inDragMode: false,
skipRemoveGroup: true, skipRemoveGroup: true,
skipActiveGroup: true, skipActiveGroup: true,

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,13 @@ export interface Box {
height: number; height: number;
width: number; width: number;
} }
type TopLeft = { top: number; left: number };
type TopRight = { top: number; right: number };
type BottomLeft = { bottom: number; left: number };
type BottomRight = { bottom: number; right: number };
type AnchorPosition = TopLeft | TopRight | BottomLeft | BottomRight;
type Size = { width: number; height: number };
export type AnchoredBox = Size & AnchorPosition;

View File

@ -165,6 +165,6 @@ api.addPanel({
api.addPanel({ api.addPanel({
id: 'panel_2', id: 'panel_2',
component: 'default', component: 'default',
floating: { x: 10, y: 10, width: 300, height: 300 }, floating: { left: 10, top: 10, width: 300, height: 300 },
}); });
``` ```

View File

@ -88,7 +88,14 @@ const GroupAction = (props: {
} }
onClick={() => { onClick={() => {
if (group) { if (group) {
props.api.addFloatingGroup(group); props.api.addFloatingGroup(group, {
position: {
width: 400,
height: 300,
top: 50,
right: 50,
},
});
} }
}} }}
> >

View File

@ -74,7 +74,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, {
position: {
width: 400,
height: 300,
bottom: 50,
right: 50,
},
});
} }
}} }}
> >

View File

@ -84,7 +84,7 @@ function addFloatingPanel2(api: DockviewApi) {
id: (++panelCount).toString(), id: (++panelCount).toString(),
title: `Tab ${panelCount}`, title: `Tab ${panelCount}`,
component: 'default', component: 'default',
floating: { width: 250, height: 150, x: 50, y: 50 }, floating: { width: 250, height: 150, left: 50, top: 50 },
}); });
} }
@ -259,11 +259,9 @@ const RightComponent = (props: IDockviewHeaderActionsProps) => {
); );
React.useEffect(() => { React.useEffect(() => {
const disposable = props.group.api.onDidLocationChange( const disposable = props.group.api.onDidLocationChange((event) => {
(event) => { setFloating(event.location.type === 'floating');
setFloating(event.location.type === 'floating'); });
}
);
return () => { return () => {
disposable.dispose(); disposable.dispose();
@ -275,7 +273,14 @@ const RightComponent = (props: IDockviewHeaderActionsProps) => {
const group = props.containerApi.addGroup(); const group = props.containerApi.addGroup();
props.group.api.moveTo({ group }); props.group.api.moveTo({ group });
} else { } else {
props.containerApi.addFloatingGroup(props.group); props.containerApi.addFloatingGroup(props.group, {
position: {
width: 400,
height: 300,
bottom: 50,
right: 50,
},
});
} }
}; };

File diff suppressed because it is too large Load Diff

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({

View File

@ -32,7 +32,14 @@ export const GroupActions = (props: {
onClick={() => { onClick={() => {
const panel = props.api?.getGroup(x); const panel = props.api?.getGroup(x);
if (panel) { if (panel) {
props.api?.addFloatingGroup(panel); props.api?.addFloatingGroup(panel, {
position: {
width: 400,
height: 300,
bottom: 50,
right: 50,
},
});
} }
}} }}
> >

View File

@ -32,7 +32,14 @@ export const PanelActions = (props: {
onClick={() => { onClick={() => {
const panel = props.api?.getPanel(x); const panel = props.api?.getPanel(x);
if (panel) { if (panel) {
props.api?.addFloatingGroup(panel); props.api?.addFloatingGroup(panel, {
position: {
width: 400,
height: 300,
bottom: 50,
right: 50,
},
});
} }
}} }}
> >

View File

@ -209,9 +209,8 @@ export const DockviewPersistence = (props: { theme?: string }) => {
setDisableFloatingGroups((x) => !x); setDisableFloatingGroups((x) => !x);
}} }}
> >
{`${ {`${disableFloatingGroups ? 'Enable' : 'Disable'
disableFloatingGroups ? 'Enable' : 'Disable' } floating groups`}
} floating groups`}
</button> </button>
</div> </div>
<div <div
@ -265,7 +264,14 @@ const RightComponent = (props: IDockviewHeaderActionsProps) => {
const group = props.containerApi.addGroup(); const group = props.containerApi.addGroup();
props.group.api.moveTo({ group }); props.group.api.moveTo({ group });
} else { } else {
props.containerApi.addFloatingGroup(props.group); props.containerApi.addFloatingGroup(props.group, {
position: {
width: 400,
height: 300,
bottom: 50,
right: 50,
}
});
} }
}; };

View File

@ -93,7 +93,14 @@ const RightAction = defineComponent({
const group = this.params.containerApi.addGroup(); const group = this.params.containerApi.addGroup();
this.group.api.moveTo({ group }); this.group.api.moveTo({ group });
} else { } else {
this.containerApi.addFloatingGroup(this.params.group); this.containerApi.addFloatingGroup(this.params.group, {
position: {
width: 400,
height: 300,
bottom: 50,
right: 50,
},
});
} }
}, },
}, },