Merge branch 'master' of https://github.com/mathuo/dockview into 361-full-screen-mode-for-a-single-tab

This commit is contained in:
mathuo 2024-01-01 22:30:21 +00:00
commit c195fa19bf
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
29 changed files with 1119 additions and 212 deletions

View File

@ -11,7 +11,7 @@ describe('groupDragHandler', () => {
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = {
id: 'test_group_id',
api: { isFloating: false } as any,
api: { location: 'grid' } as any,
};
return partial as DockviewGroupPanel;
});
@ -48,12 +48,12 @@ describe('groupDragHandler', () => {
cut.dispose();
});
test('that the event is cancelled when isFloating and shiftKey=true', () => {
test('that the event is cancelled when floating and shiftKey=true', () => {
const element = document.createElement('div');
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = {
api: { isFloating: true } as any,
api: { location: 'floating' } as any,
};
return partial as DockviewGroupPanel;
});
@ -85,7 +85,7 @@ describe('groupDragHandler', () => {
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = {
api: { isFloating: false } as any,
api: { location: 'grid' } as any,
};
return partial as DockviewGroupPanel;
});

View File

@ -478,7 +478,7 @@ describe('tabsContainer', () => {
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { isFloating: false } as any,
api: { location: 'grid' } as any,
}) as DockviewGroupPanel;
});
@ -538,7 +538,7 @@ describe('tabsContainer', () => {
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { isFloating: true } as any,
api: { location: 'floating' } as any,
}) as DockviewGroupPanel;
});
@ -591,7 +591,7 @@ describe('tabsContainer', () => {
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { isFloating: true } as any,
api: { location: 'floating' } as any,
model: {} as any,
}) as DockviewGroupPanel;
});
@ -653,7 +653,7 @@ describe('tabsContainer', () => {
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { isFloating: true } as any,
api: { location: 'grid' } as any,
model: {} as any,
}) as DockviewGroupPanel;
});
@ -723,7 +723,7 @@ describe('tabsContainer', () => {
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { isFloating: true } as any,
api: { location: 'grid' } as any,
model: {} as any,
}) as DockviewGroupPanel;
});
@ -793,7 +793,7 @@ describe('tabsContainer', () => {
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { isFloating: true } as any,
api: { location: 'grid' } as any,
model: {} as any,
}) as DockviewGroupPanel;
});

View File

@ -2862,8 +2862,8 @@ describe('dockviewComponent', () => {
floating: true,
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(2);
@ -2874,8 +2874,8 @@ describe('dockviewComponent', () => {
'right'
);
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('grid');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(2);
});
@ -2907,8 +2907,8 @@ describe('dockviewComponent', () => {
floating: true,
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(2);
@ -2919,8 +2919,8 @@ describe('dockviewComponent', () => {
'center'
);
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('grid');
expect(dockview.groups.length).toBe(1);
expect(dockview.panels.length).toBe(2);
});
@ -2958,9 +2958,9 @@ describe('dockviewComponent', () => {
floating: true,
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(3);
expect(dockview.panels.length).toBe(3);
@ -2971,9 +2971,9 @@ describe('dockviewComponent', () => {
'center'
);
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(3);
});
@ -3011,9 +3011,9 @@ describe('dockviewComponent', () => {
position: { referencePanel: panel2 },
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(3);
@ -3024,9 +3024,9 @@ describe('dockviewComponent', () => {
'right'
);
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel3.group.api.isFloating).toBeFalsy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('grid');
expect(panel3.group.api.location).toBe('grid');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(3);
});
@ -3064,9 +3064,9 @@ describe('dockviewComponent', () => {
position: { referencePanel: panel2 },
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(3);
@ -3077,9 +3077,9 @@ describe('dockviewComponent', () => {
'center'
);
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel3.group.api.isFloating).toBeFalsy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('grid');
expect(panel3.group.api.location).toBe('grid');
expect(dockview.groups.length).toBe(1);
expect(dockview.panels.length).toBe(3);
});
@ -3123,10 +3123,10 @@ describe('dockviewComponent', () => {
floating: true,
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel4.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(panel3.group.api.location).toBe('floating');
expect(panel4.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(3);
expect(dockview.panels.length).toBe(4);
@ -3137,10 +3137,10 @@ describe('dockviewComponent', () => {
'center'
);
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel4.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(panel3.group.api.location).toBe('floating');
expect(panel4.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(4);
});
@ -3172,8 +3172,8 @@ describe('dockviewComponent', () => {
floating: true,
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(2);
@ -3184,8 +3184,8 @@ describe('dockviewComponent', () => {
'right'
);
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('grid');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(2);
});
@ -3217,8 +3217,8 @@ describe('dockviewComponent', () => {
floating: true,
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(2);
@ -3229,8 +3229,8 @@ describe('dockviewComponent', () => {
'center'
);
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('grid');
expect(dockview.groups.length).toBe(1);
expect(dockview.panels.length).toBe(2);
});
@ -3268,9 +3268,9 @@ describe('dockviewComponent', () => {
floating: true,
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(3);
expect(dockview.panels.length).toBe(3);
@ -3281,9 +3281,9 @@ describe('dockviewComponent', () => {
'center'
);
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(3);
});
@ -3321,9 +3321,9 @@ describe('dockviewComponent', () => {
position: { referencePanel: panel2 },
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(3);
@ -3334,9 +3334,9 @@ describe('dockviewComponent', () => {
'right'
);
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('grid');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(3);
expect(dockview.panels.length).toBe(3);
});
@ -3374,9 +3374,9 @@ describe('dockviewComponent', () => {
position: { referencePanel: panel2 },
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(3);
@ -3387,9 +3387,9 @@ describe('dockviewComponent', () => {
'center'
);
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('grid');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(3);
});
@ -3433,10 +3433,10 @@ describe('dockviewComponent', () => {
floating: true,
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel4.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(panel3.group.api.location).toBe('floating');
expect(panel4.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(3);
expect(dockview.panels.length).toBe(4);
@ -3447,10 +3447,10 @@ describe('dockviewComponent', () => {
'center'
);
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel4.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(panel3.group.api.location).toBe('floating');
expect(panel4.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(3);
expect(dockview.panels.length).toBe(4);
});
@ -3488,9 +3488,9 @@ describe('dockviewComponent', () => {
floating: true,
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('grid');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(3);
expect(dockview.panels.length).toBe(3);
@ -3501,9 +3501,9 @@ describe('dockviewComponent', () => {
'center'
);
expect(panel1.group.api.isFloating).toBeTruthy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('floating');
expect(panel2.group.api.location).toBe('grid');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(3);
});
@ -3540,9 +3540,9 @@ describe('dockviewComponent', () => {
floating: true,
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('grid');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(3);
@ -3553,9 +3553,9 @@ describe('dockviewComponent', () => {
'center'
);
expect(panel1.group.api.isFloating).toBeTruthy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('floating');
expect(panel2.group.api.location).toBe('grid');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(3);
});
@ -3593,9 +3593,9 @@ describe('dockviewComponent', () => {
floating: true,
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('grid');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(3);
expect(dockview.panels.length).toBe(3);
@ -3606,9 +3606,9 @@ describe('dockviewComponent', () => {
'center'
);
expect(panel1.group.api.isFloating).toBeTruthy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('floating');
expect(panel2.group.api.location).toBe('grid');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(3);
});
@ -3645,9 +3645,9 @@ describe('dockviewComponent', () => {
floating: true,
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('grid');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(3);
@ -3658,9 +3658,9 @@ describe('dockviewComponent', () => {
'center'
);
expect(panel1.group.api.isFloating).toBeTruthy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel3.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('floating');
expect(panel2.group.api.location).toBe('floating');
expect(panel3.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(1);
expect(dockview.panels.length).toBe(3);
});
@ -3692,15 +3692,15 @@ describe('dockviewComponent', () => {
position: { direction: 'right' },
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('grid');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(2);
dockview.addFloatingGroup(panel2);
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(2);
});
@ -3731,15 +3731,15 @@ describe('dockviewComponent', () => {
component: 'default',
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('grid');
expect(dockview.groups.length).toBe(1);
expect(dockview.panels.length).toBe(2);
dockview.addFloatingGroup(panel2);
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(2);
});
@ -3771,15 +3771,15 @@ describe('dockviewComponent', () => {
position: { direction: 'right' },
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('grid');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(2);
dockview.addFloatingGroup(panel2.group);
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(2);
});
@ -3810,15 +3810,15 @@ describe('dockviewComponent', () => {
component: 'default',
});
expect(panel1.group.api.isFloating).toBeFalsy();
expect(panel2.group.api.isFloating).toBeFalsy();
expect(panel1.group.api.location).toBe('grid');
expect(panel2.group.api.location).toBe('grid');
expect(dockview.groups.length).toBe(1);
expect(dockview.panels.length).toBe(2);
dockview.addFloatingGroup(panel2.group);
expect(panel1.group.api.isFloating).toBeTruthy();
expect(panel2.group.api.isFloating).toBeTruthy();
expect(panel1.group.api.location).toBe('floating');
expect(panel2.group.api.location).toBe('floating');
expect(dockview.groups.length).toBe(1);
expect(dockview.panels.length).toBe(2);
});

View File

@ -42,6 +42,7 @@ import {
GroupDragEvent,
TabDragEvent,
} from '../dockview/components/titlebar/tabsContainer';
import { Box } from '../types';
export interface CommonApi<T = any> {
readonly height: number;
@ -820,4 +821,17 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
get onDidMaxmizedGroupChange(): Event<void> {
return this.component.onDidMaxmizedGroupChange;
}
/**
* Add a popout group in a new Window
*/
addPopoutGroup(
item: IDockviewPanel | DockviewGroupPanel,
options?: {
position?: Box;
popoutUrl?: string;
}
): void {
this.component.addPopoutGroup(item, options);
}
}

View File

@ -1,38 +1,39 @@
import { Position } from '../dnd/droptarget';
import { DockviewComponent } from '../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { DockviewGroupLocation } from '../dockview/dockviewGroupPanelModel';
import { Emitter, Event } from '../events';
import { GridviewPanelApi, GridviewPanelApiImpl } from './gridviewPanelApi';
export interface DockviewGroupPanelApi extends GridviewPanelApi {
readonly onDidFloatingStateChange: Event<DockviewGroupPanelFloatingChangeEvent>;
readonly isFloating: boolean;
readonly onDidRenderPositionChange: Event<DockviewGroupPanelFloatingChangeEvent>;
readonly location: DockviewGroupLocation;
moveTo(options: { group: DockviewGroupPanel; position?: Position }): void;
}
export interface DockviewGroupPanelFloatingChangeEvent {
readonly isFloating: boolean;
readonly location: DockviewGroupLocation;
}
export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
private _group: DockviewGroupPanel | undefined;
readonly _onDidFloatingStateChange =
readonly _onDidRenderPositionChange =
new Emitter<DockviewGroupPanelFloatingChangeEvent>();
readonly onDidFloatingStateChange: Event<DockviewGroupPanelFloatingChangeEvent> =
this._onDidFloatingStateChange.event;
readonly onDidRenderPositionChange: Event<DockviewGroupPanelFloatingChangeEvent> =
this._onDidRenderPositionChange.event;
get isFloating() {
get location(): DockviewGroupLocation {
if (!this._group) {
throw new Error(`DockviewGroupPanelApiImpl not initialized`);
}
return this._group.model.isFloating;
return this._group.model.location;
}
constructor(id: string, private readonly accessor: DockviewComponent) {
super(id);
this.addDisposables(this._onDidFloatingStateChange);
this.addDisposables(this._onDidRenderPositionChange);
}
moveTo(options: { group: DockviewGroupPanel; position?: Position }): void {

View File

@ -38,7 +38,7 @@ export class GroupDragHandler extends DragHandler {
}
override isCancelled(_event: DragEvent): boolean {
if (this.group.api.isFloating && !_event.shiftKey) {
if (this.group.api.location === 'floating' && !_event.shiftKey) {
return true;
}
return false;

View File

@ -11,6 +11,7 @@ import {
} from '../events';
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { clamp } from '../math';
import { Box } from '../types';
const bringElementToFront = (() => {
let previous: HTMLElement | null = null;
@ -48,11 +49,7 @@ export class Overlay extends CompositeDisposable {
}
constructor(
private readonly options: {
height: number;
width: number;
left: number;
top: number;
private readonly options: Box & {
container: HTMLElement;
content: HTMLElement;
minimumInViewportWidth?: number;
@ -86,14 +83,7 @@ export class Overlay extends CompositeDisposable {
});
}
setBounds(
bounds: Partial<{
height: number;
width: number;
top: number;
left: number;
}> = {}
): void {
setBounds(bounds: Partial<Box> = {}): void {
if (typeof bounds.height === 'number') {
this._element.style.height = `${bounds.height}px`;
}
@ -139,7 +129,7 @@ export class Overlay extends CompositeDisposable {
this._onDidChange.fire();
}
toJSON(): { top: number; left: number; height: number; width: number } {
toJSON(): Box {
const container = this.options.container.getBoundingClientRect();
const element = this._element.getBoundingClientRect();

View File

@ -85,7 +85,7 @@ export class GreadyRenderContainer extends CompositeDisposable {
toggleClass(
focusContainer,
'dv-render-overlay-float',
panel.group.api.isFloating
panel.group.api.location === 'floating'
);
};

View File

@ -76,7 +76,11 @@ export class ContentContainer
const data = getPanelData();
if (!data && event.shiftKey && !this.group.isFloating) {
if (
!data &&
event.shiftKey &&
this.group.location !== 'floating'
) {
return false;
}

View File

@ -9,6 +9,24 @@
.tab {
flex-shrink: 0;
&:focus-within,
&:focus {
position: relative;
&::after {
position: absolute;
content: '';
height: 100%;
width: 100%;
top: 0px;
left: 0px;
pointer-events: none;
outline: 1px solid var(--dv-tab-divider-color) !important;
outline-offset: -1px;
z-index: 5;
}
}
&.dv-tab-dragging {
.tab-action {
background-color: var(--dv-activegroup-visiblepanel-tab-color);

View File

@ -247,7 +247,7 @@ export class TabsContainer
if (
isFloatingGroupsEnabled &&
event.shiftKey &&
!this.group.api.isFloating
this.group.api.location !== 'floating'
) {
event.preventDefault();
@ -350,7 +350,7 @@ export class TabsContainer
!this.accessor.options.disableFloatingGroups;
const isFloatingWithOnePanel =
this.group.api.isFloating && this.size === 1;
this.group.api.location === 'floating' && this.size === 1;
if (
isFloatingGroupsEnabled &&

View File

@ -47,18 +47,45 @@ import { getPanelData } from '../dnd/dataTransfer';
import { Parameters } from '../panel/types';
import { Overlay } from '../dnd/overlay';
import { toggleClass, watchElementResize } from '../dom';
import {
DockviewFloatingGroupPanel,
IDockviewFloatingGroupPanel,
} from './dockviewFloatingGroupPanel';
import { DockviewFloatingGroupPanel } from './dockviewFloatingGroupPanel';
import {
GroupDragEvent,
TabDragEvent,
} from './components/titlebar/tabsContainer';
import { PopoutWindow } from '../popoutWindow';
import { Box } from '../types';
import {
GreadyRenderContainer,
DockviewPanelRenderer,
} from './components/greadyRenderContainer';
import { DockviewPopoutGroupPanel } from './dockviewPopoutGroupPanel';
function getTheme(element: HTMLElement): string | undefined {
function toClassList(element: HTMLElement) {
const list: string[] = [];
for (let i = 0; i < element.classList.length; i++) {
list.push(element.classList.item(i)!);
}
return list;
}
let theme: string | undefined = undefined;
let parent: HTMLElement | null = element;
while (parent !== null) {
theme = toClassList(parent).find((cls) =>
cls.startsWith('dockview-theme-')
);
if (typeof theme === 'string') {
break;
}
parent = parent.parentElement;
}
return theme;
}
const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100;
@ -69,7 +96,12 @@ export interface PanelReference {
export interface SerializedFloatingGroup {
data: GroupPanelViewState;
position: { height: number; width: number; left: number; top: number };
position: Box;
}
export interface SerializedPopoutGroup {
data: GroupPanelViewState;
position: Box | null;
}
export interface SerializedDockview {
@ -82,6 +114,7 @@ export interface SerializedDockview {
panels: Record<string, GroupviewPanelState>;
activeGroup?: string;
floatingGroups?: SerializedFloatingGroup[];
popoutGroups?: SerializedPopoutGroup[];
}
function typeValidate3(data: GroupPanelViewState, path: string): void {
@ -196,7 +229,6 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
readonly activePanel: IDockviewPanel | undefined;
readonly totalPanels: number;
readonly panels: IDockviewPanel[];
readonly floatingGroups: IDockviewFloatingGroupPanel[];
readonly onDidDrop: Event<DockviewDropEvent>;
readonly orientation: Orientation;
updateOptions(options: DockviewComponentUpdateOptions): void;
@ -237,6 +269,13 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
item: IDockviewPanel | DockviewGroupPanel,
coord?: { x: number; y: number }
): void;
addPopoutGroup(
item: IDockviewPanel | DockviewGroupPanel,
options?: {
position?: Box;
popoutUrl?: string;
}
): void;
}
export class DockviewComponent
@ -277,7 +316,8 @@ export class DockviewComponent
readonly onDidActivePanelChange: Event<IDockviewPanel | undefined> =
this._onDidActivePanelChange.event;
readonly floatingGroups: DockviewFloatingGroupPanel[] = [];
private readonly _floatingGroups: DockviewFloatingGroupPanel[] = [];
private readonly _popoutGroups: DockviewPopoutGroupPanel[] = [];
get orientation(): Orientation {
return this.gridview.orientation;
@ -445,6 +485,73 @@ export class DockviewComponent
this.updateWatermark();
}
addPopoutGroup(
item: DockviewPanel | DockviewGroupPanel,
options?: {
skipRemoveGroup?: boolean;
position?: Box;
popoutUrl?: string;
}
): void {
let group: DockviewGroupPanel;
let box: Box | undefined = options?.position;
if (item instanceof DockviewPanel) {
group = this.createGroup();
this.removePanel(item, {
removeEmptyGroup: true,
skipDispose: true,
});
group.model.openPanel(item);
if (!box) {
box = this.element.getBoundingClientRect();
}
} else {
group = item;
if (!box) {
box = group.element.getBoundingClientRect();
}
const skip =
typeof options?.skipRemoveGroup === 'boolean' &&
options.skipRemoveGroup;
if (!skip) {
this.doRemoveGroup(item, { skipDispose: true });
}
}
const theme = getTheme(this.gridview.element);
const popoutWindow = new DockviewPopoutGroupPanel(group, {
className: theme ?? '',
popoutUrl: options?.popoutUrl ?? '/popout.html',
box: {
left: box.left,
top: box.top,
width: box.width,
height: box.height,
},
});
popoutWindow.addDisposables(
{
dispose: () => {
remove(this._popoutGroups, popoutWindow);
},
},
popoutWindow.window.onDidClose(() => {
this.doAddGroup(group, [0]);
})
);
this._popoutGroups.push(popoutWindow);
}
addFloatingGroup(
item: DockviewPanel | DockviewGroupPanel,
coord?: { x?: number; y?: number; height?: number; width?: number },
@ -473,7 +580,7 @@ export class DockviewComponent
}
}
group.model.isFloating = true;
group.model.location = 'floating';
const overlayLeft =
typeof coord?.x === 'number' ? Math.max(coord.x, 0) : 100;
@ -544,14 +651,14 @@ export class DockviewComponent
dispose: () => {
disposable.dispose();
group.model.isFloating = false;
remove(this.floatingGroups, floatingGroupPanel);
group.model.location = 'grid';
remove(this._floatingGroups, floatingGroupPanel);
this.updateWatermark();
},
}
);
this.floatingGroups.push(floatingGroupPanel);
this._floatingGroups.push(floatingGroupPanel);
this.updateWatermark();
}
@ -605,7 +712,7 @@ export class DockviewComponent
}
if (hasFloatingGroupOptionsChanged) {
for (const group of this.floatingGroups) {
for (const group of this._floatingGroups) {
switch (this.options.floatingGroupBounds) {
case 'boundedWithinViewport':
group.overlay.minimumInViewportHeight = undefined;
@ -638,8 +745,8 @@ export class DockviewComponent
): void {
super.layout(width, height, forceResize);
if (this.floatingGroups) {
for (const floating of this.floatingGroups) {
if (this._floatingGroups) {
for (const floating of this._floatingGroups) {
// ensure floting groups stay within visible boundaries
floating.overlay.setBounds();
}
@ -717,11 +824,20 @@ export class DockviewComponent
return collection;
}, {} as { [key: string]: GroupviewPanelState });
const floats: SerializedFloatingGroup[] = this.floatingGroups.map(
(floatingGroup) => {
const floats: SerializedFloatingGroup[] = this._floatingGroups.map(
(group) => {
return {
data: floatingGroup.group.toJSON() as GroupPanelViewState,
position: floatingGroup.overlay.toJSON(),
data: group.group.toJSON() as GroupPanelViewState,
position: group.overlay.toJSON(),
};
}
);
const popoutGroups: SerializedPopoutGroup[] = this._popoutGroups.map(
(group) => {
return {
data: group.group.toJSON() as GroupPanelViewState,
position: group.window.dimensions(),
};
}
);
@ -736,6 +852,10 @@ export class DockviewComponent
result.floatingGroups = floats;
}
if (popoutGroups.length > 0) {
result.popoutGroups = popoutGroups;
}
return result;
}
@ -841,7 +961,20 @@ export class DockviewComponent
);
}
for (const floatingGroup of this.floatingGroups) {
const serializedPopoutGroups = data.popoutGroups ?? [];
for (const serializedPopoutGroup of serializedPopoutGroups) {
const { data, position } = serializedPopoutGroup;
const group = createGroupFromSerializedState(data);
this.addPopoutGroup(group, {
skipRemoveGroup: true,
position: position ?? undefined,
});
}
for (const floatingGroup of this._floatingGroups) {
floatingGroup.overlay.setBounds();
}
@ -875,7 +1008,7 @@ export class DockviewComponent
}
// iterate over a reassigned array since original array will be modified
for (const floatingGroup of [...this.floatingGroups]) {
for (const floatingGroup of [...this._floatingGroups]) {
floatingGroup.dispose();
}
@ -999,7 +1132,10 @@ export class DockviewComponent
panel = this.createPanel(options, group);
group.model.openPanel(panel);
this.doSetGroupAndPanelActive(group);
} else if (referenceGroup.api.isFloating || target === 'center') {
} else if (
referenceGroup.api.location === 'floating' ||
target === 'center'
) {
panel = this.createPanel(options, referenceGroup);
referenceGroup.model.openPanel(panel);
} else {
@ -1083,7 +1219,7 @@ export class DockviewComponent
}
private updateWatermark(): void {
if (this.groups.filter((x) => !x.api.isFloating).length === 0) {
if (this.groups.filter((x) => x.api.location === 'grid').length === 0) {
if (!this.watermark) {
this.watermark = this.createWatermarkComponent();
@ -1201,27 +1337,61 @@ export class DockviewComponent
}
| undefined
): DockviewGroupPanel {
const floatingGroup = this.floatingGroups.find(
(_) => _.group === group
);
if (floatingGroup) {
if (!options?.skipDispose) {
floatingGroup.group.dispose();
this._groups.delete(group.id);
this._onDidRemoveGroup.fire(group);
if (group.api.location === 'floating') {
const floatingGroup = this._floatingGroups.find(
(_) => _.group === group
);
if (floatingGroup) {
if (!options?.skipDispose) {
floatingGroup.group.dispose();
this._groups.delete(group.id);
this._onDidRemoveGroup.fire(group);
}
remove(this._floatingGroups, floatingGroup);
floatingGroup.dispose();
if (!options?.skipActive && this._activeGroup === group) {
const groups = Array.from(this._groups.values());
this.doSetGroupActive(
groups.length > 0 ? groups[0].value : undefined
);
}
return floatingGroup.group;
}
floatingGroup.dispose();
throw new Error('failed to find floating group');
}
if (!options?.skipActive && this._activeGroup === group) {
const groups = Array.from(this._groups.values());
if (group.api.location === 'popout') {
const selectedGroup = this._popoutGroups.find(
(_) => _.group === group
);
this.doSetGroupActive(
groups.length > 0 ? groups[0].value : undefined
);
if (selectedGroup) {
if (!options?.skipDispose) {
selectedGroup.group.dispose();
this._groups.delete(group.id);
this._onDidRemoveGroup.fire(group);
}
selectedGroup.dispose();
if (!options?.skipActive && this._activeGroup === group) {
const groups = Array.from(this._groups.values());
this.doSetGroupActive(
groups.length > 0 ? groups[0].value : undefined
);
}
return selectedGroup.group;
}
return floatingGroup.group;
throw new Error('failed to find popout group');
}
return super.doRemoveGroup(group, options);
@ -1276,11 +1446,7 @@ export class DockviewComponent
if (sourceGroup && sourceGroup.size < 2) {
const [targetParentLocation, to] = tail(targetLocation);
const isFloating = this.floatingGroups.find(
(x) => x.group === sourceGroup
);
if (!isFloating) {
if (sourceGroup.api.location === 'grid') {
const sourceLocation = getGridLocation(sourceGroup.element);
const [sourceParentLocation, from] = tail(sourceLocation);
@ -1356,16 +1522,29 @@ export class DockviewComponent
});
}
} else {
const floatingGroup = this.floatingGroups.find(
(x) => x.group === sourceGroup
);
if (floatingGroup) {
floatingGroup.dispose();
} else {
this.gridview.removeView(
getGridLocation(sourceGroup.element)
);
switch (sourceGroup.api.location) {
case 'grid':
this.gridview.removeView(
getGridLocation(sourceGroup.element)
);
break;
case 'floating':
const selectedFloatingGroup = this._floatingGroups.find(
(x) => x.group === sourceGroup
);
if (!selectedFloatingGroup) {
throw new Error('failed to find floating group');
}
selectedFloatingGroup.dispose();
break;
case 'popout':
const selectedPopoutGroup = this._popoutGroups.find(
(x) => x.group === sourceGroup
);
if (!selectedPopoutGroup) {
throw new Error('failed to find popout group');
}
selectedPopoutGroup.dispose();
}
const referenceLocation = getGridLocation(

View File

@ -130,7 +130,7 @@ export interface IDockviewGroupPanelModel extends IPanel {
): boolean;
}
export type DockviewGroupMode = 'grid' | 'floating' | 'fullscreen' | 'popout';
export type DockviewGroupLocation = 'grid' | 'floating' | 'popout';
export class DockviewGroupPanelModel
extends CompositeDisposable
@ -143,11 +143,12 @@ export class DockviewGroupPanelModel
private watermark?: IWatermarkRenderer;
private _isGroupActive = false;
private _locked: DockviewGroupPanelLocked = false;
private _isFloating = false;
private _rightHeaderActions: IHeaderActionsRenderer | undefined;
private _leftHeaderActions: IHeaderActionsRenderer | undefined;
private _prefixHeaderActions: IHeaderActionsRenderer | undefined;
private _location: DockviewGroupLocation = 'grid';
private mostRecentlyUsed: IDockviewPanel[] = [];
private readonly _onDidChange = new Emitter<IViewSize | undefined>();
@ -243,21 +244,45 @@ export class DockviewGroupPanelModel
);
}
get isFloating(): boolean {
return this._isFloating;
get location(): DockviewGroupLocation {
return this._location;
}
set isFloating(value: boolean) {
this._isFloating = value;
set location(value: DockviewGroupLocation) {
this._location = value;
toggleClass(this.container, 'dv-groupview-floating', false);
toggleClass(this.container, 'dv-groupview-popout', false);
switch (value) {
case 'grid':
this.contentContainer.dropTarget.setTargetZones([
'top',
'bottom',
'left',
'right',
'center',
]);
break;
case 'floating':
this.contentContainer.dropTarget.setTargetZones(['center']);
this.contentContainer.dropTarget.setTargetZones(
value ? ['center'] : ['top', 'bottom', 'left', 'right', 'center']
);
toggleClass(this.container, 'dv-groupview-floating', value);
toggleClass(this.container, 'dv-groupview-floating', true);
this.groupPanel.api._onDidFloatingStateChange.fire({
isFloating: this.isFloating,
break;
case 'popout':
this.contentContainer.dropTarget.setTargetZones(['center']);
toggleClass(this.container, 'dv-groupview-popout', true);
break;
}
this.groupPanel.api._onDidRenderPositionChange.fire({
location: this.location,
});
}

View File

@ -0,0 +1,43 @@
import { CompositeDisposable } from '../lifecycle';
import { PopoutWindow } from '../popoutWindow';
import { Box } from '../types';
import { DockviewGroupPanel } from './dockviewGroupPanel';
export class DockviewPopoutGroupPanel extends CompositeDisposable {
readonly window: PopoutWindow;
constructor(
readonly group: DockviewGroupPanel,
private readonly options: {
className: string;
popoutUrl: string;
box: Box;
}
) {
super();
this.window = new PopoutWindow('test', options.className ?? '', {
url: this.options.popoutUrl,
left: this.options.box.left,
top: this.options.box.top,
width: this.options.box.width,
height: this.options.box.height,
});
group.model.location = 'popout';
this.addDisposables(
this.window,
{
dispose: () => {
group.model.location = 'grid';
},
},
this.window.onDidClose(() => {
this.dispose();
})
);
this.window.open(group.element);
}
}

View File

@ -97,6 +97,7 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions {
minimumHeightWithinViewport?: number;
minimumWidthWithinViewport?: number;
};
popoutUrl?: string;
defaultRenderer?: DockviewPanelRenderer;
debug?: boolean;
}

View File

@ -186,6 +186,38 @@ export function quasiDefaultPrevented(event: Event): boolean {
return (event as any)[QUASI_PREVENT_DEFAULT_KEY];
}
export function addStyles(document: Document, styleSheetList: StyleSheetList) {
const styleSheets = Array.from(styleSheetList);
for (const styleSheet of styleSheets) {
if (styleSheet.href) {
const link = document.createElement('link');
link.href = styleSheet.href;
link.type = styleSheet.type;
link.rel = 'stylesheet';
document.head.appendChild(link);
}
let cssTexts: string[] = [];
try {
if (styleSheet.cssRules) {
cssTexts = Array.from(styleSheet.cssRules).map(
(rule) => rule.cssText
);
}
} catch (err) {
// security errors (lack of permissions), ignore
}
for (const rule of cssTexts) {
const style = document.createElement('style');
style.appendChild(document.createTextNode(rule));
document.head.appendChild(style);
}
}
}
export function getDomNodePagePosition(domNode: Element): {
left: number;
top: number;

View File

@ -0,0 +1,121 @@
import { addStyles } from './dom';
import { Emitter, addDisposableWindowListener } from './events';
import { CompositeDisposable, IDisposable } from './lifecycle';
import { Box } from './types';
export type PopoutWindowOptions = {
url: string;
} & Box;
export class PopoutWindow extends CompositeDisposable {
private readonly _onDidClose = new Emitter<void>();
readonly onDidClose = this._onDidClose.event;
private _window: { value: Window; disposable: IDisposable } | null = null;
constructor(
private readonly id: string,
private readonly className: string,
private readonly options: PopoutWindowOptions
) {
super();
this.addDisposables(this._onDidClose, {
dispose: () => {
this.close();
},
});
}
dimensions(): Box | null {
if (!this._window) {
return null;
}
const left = this._window.value.screenX;
const top = this._window.value.screenY;
const width = this._window.value.innerWidth;
const height = this._window.value.innerHeight;
return { top, left, width, height };
}
close(): void {
if (this._window) {
this._window.disposable.dispose();
this._window.value.close();
this._window = null;
}
}
open(content: HTMLElement): void {
if (this._window) {
throw new Error('instance of popout window is already open');
}
const url = `${this.options.url}`;
const features = Object.entries({
top: this.options.top,
left: this.options.left,
width: this.options.width,
height: this.options.height,
})
.map(([key, value]) => `${key}=${value}`)
.join(',');
// https://developer.mozilla.org/en-US/docs/Web/API/Window/open
const externalWindow = window.open(url, this.id, features);
if (!externalWindow) {
return;
}
const disposable = new CompositeDisposable();
this._window = { value: externalWindow, disposable };
const grievingParent = content.parentElement;
const cleanUp = () => {
grievingParent?.appendChild(content);
this._onDidClose.fire();
this._window = null;
};
// prevent any default content from loading
// externalWindow.document.body.replaceWith(document.createElement('div'));
disposable.addDisposables(
addDisposableWindowListener(window, 'beforeunload', () => {
cleanUp();
this.close();
})
);
externalWindow.addEventListener('load', () => {
const externalDocument = externalWindow.document;
externalDocument.title = document.title;
const div = document.createElement('div');
div.classList.add('dv-popout-window');
div.style.position = 'absolute';
div.style.width = '100%';
div.style.height = '100%';
div.style.top = '0px';
div.style.left = '0px';
div.classList.add(this.className);
div.appendChild(content);
externalDocument.body.replaceChildren(div);
externalDocument.body.classList.add(this.className);
addStyles(externalDocument, window.document.styleSheets);
externalWindow.addEventListener('beforeunload', () => {
// TODO: indicate external window is closing
cleanUp();
});
});
}
}

View File

@ -1,3 +1,10 @@
export type FunctionOrValue<T> = (() => T) | T;
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
export interface Box {
left: number;
top: number;
height: number;
width: number;
}

View File

@ -28,6 +28,7 @@ import DockviewWithIFrames from '@site/sandboxes/iframe-dockview/src/app';
import DockviewFloating from '@site/sandboxes/floatinggroup-dockview/src/app';
import DockviewLockedGroup from '@site/sandboxes/lockedgroup-dockview/src/app';
import DockviewKeyboard from '@site/sandboxes/keyboard-dockview/src/app';
import DockviewPopoutGroup from '@site/sandboxes/popoutgroup-dockview/src/app';
import DockviewRenderMode from '@site/sandboxes/rendermode-dockview/src/app';
import { DocRef } from '@site/src/components/ui/reference/docRef';
@ -356,7 +357,7 @@ Floating groups can be interacted with whilst holding the `shift` key activating
<img style={{ width: '60%' }} src={useBaseUrl('/img/float_group.svg')} />
Floating groups can be programatically added through the dockview `api` method `api.addFloatingGroup(...)` and you can check whether
a group is floating via the `group.api.isFloating` property. See examples for full code.
a group is floating via the `group.api.location` property. See examples for full code.
You can control the bounding box of floating groups through the optional `floatingGroupBounds` options:
@ -370,6 +371,27 @@ You can control the bounding box of floating groups through the optional `floati
react={DockviewFloating}
/>
## Popout Groups
Dockview has built-in support for opening groups in new Windows.
Each popout window can contain a single group with many panels and you can have as many popout
windows as needed. You cannot dock multiple groups together in the same window.
To open an existing group in a new window
```tsx
api.addPopoutGroup(group);
```
From within a panel you may say
```tsx
props.containerApi.addPopoutGroup(props.api.group);
```
<DockviewPopoutGroup/>
## Panels
### Add Panel

View File

@ -255,13 +255,15 @@ const LeftComponent = (props: IDockviewHeaderActionsProps) => {
const RightComponent = (props: IDockviewHeaderActionsProps) => {
const [floating, setFloating] = React.useState<boolean>(
props.api.isFloating
props.api.location === 'floating'
);
React.useEffect(() => {
const disposable = props.group.api.onDidFloatingStateChange((event) => [
setFloating(event.isFloating),
]);
const disposable = props.group.api.onDidRenderPositionChange(
(event) => {
setFloating(event.location === 'floating');
}
);
return () => {
disposable.dispose();

View File

@ -0,0 +1,32 @@
{
"name": "popout-dockview",
"description": "",
"keywords": [
"dockview"
],
"version": "1.0.0",
"main": "src/index.tsx",
"dependencies": {
"dockview": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"typescript": "^4.9.5",
"react-scripts": "*"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,253 @@
import {
DockviewApi,
DockviewGroupPanel,
DockviewReact,
DockviewReadyEvent,
IDockviewHeaderActionsProps,
IDockviewPanelProps,
SerializedDockview,
} from 'dockview';
import * as React from 'react';
import { Icon } from './utils';
const components = {
default: (props: IDockviewPanelProps<{ title: string }>) => {
return (
<div
style={{
height: '100%',
padding: '20px',
background: 'var(--dv-group-view-background-color)',
}}
>
{props.params.title}
</div>
);
},
};
const counter = (() => {
let i = 0;
return {
next: () => ++i,
};
})();
function loadDefaultLayout(api: DockviewApi) {
api.addPanel({
id: 'panel_1',
component: 'default',
});
api.addPanel({
id: 'panel_2',
component: 'default',
});
api.addPanel({
id: 'panel_3',
component: 'default',
});
api.addPanel({
id: 'panel_4',
component: 'default',
});
api.addPanel({
id: 'panel_5',
component: 'default',
position: { direction: 'right' },
});
api.addPanel({
id: 'panel_6',
component: 'default',
});
}
let panelCount = 0;
function safeParse<T>(value: any): T | null {
try {
return JSON.parse(value) as T;
} catch (err) {
return null;
}
}
const useLocalStorage = <T,>(
key: string
): [T | null, (setter: T | null) => void] => {
const [state, setState] = React.useState<T | null>(
safeParse(localStorage.getItem(key))
);
React.useEffect(() => {
const _state = localStorage.getItem('key');
try {
if (_state !== null) {
setState(JSON.parse(_state));
}
} catch (err) {
//
}
}, [key]);
return [
state,
(_state: T | null) => {
if (_state === null) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify(_state));
setState(_state);
}
},
];
};
export const DockviewPersistance = (props: { theme?: string }) => {
const [api, setApi] = React.useState<DockviewApi>();
const [layout, setLayout] =
useLocalStorage<SerializedDockview>('floating.layout');
const [disableFloatingGroups, setDisableFloatingGroups] =
React.useState<boolean>(false);
const load = (api: DockviewApi) => {
api.clear();
if (layout) {
try {
api.fromJSON(layout);
} catch (err) {
console.error(err);
api.clear();
loadDefaultLayout(api);
}
} else {
loadDefaultLayout(api);
}
};
const onReady = (event: DockviewReadyEvent) => {
load(event.api);
setApi(event.api);
};
const [options, setOptions] = React.useState<
'boundedWithinViewport' | undefined
>(undefined);
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
height: '400px',
}}
>
<div style={{ height: '25px' }}>
<button
onClick={() => {
if (api) {
setLayout(api.toJSON());
}
}}
>
Save
</button>
<button
onClick={() => {
if (api) {
load(api);
}
}}
>
Load
</button>
<button
onClick={() => {
api!.clear();
setLayout(null);
}}
>
Clear
</button>
</div>
<div
style={{
flexGrow: 1,
}}
>
<DockviewReact
onReady={onReady}
components={components}
watermarkComponent={Watermark}
leftHeaderActionsComponent={LeftComponent}
rightHeaderActionsComponent={RightComponent}
disableFloatingGroups={disableFloatingGroups}
floatingGroupBounds={options}
className={`${props.theme || 'dockview-theme-abyss'}`}
/>
</div>
</div>
);
};
const LeftComponent = (props: IDockviewHeaderActionsProps) => {
const onClick = () => {
props.containerApi.addPanel({
id: (++panelCount).toString(),
title: `Tab ${panelCount}`,
component: 'default',
position: { referenceGroup: props.group },
});
};
return (
<div style={{ height: '100%', color: 'white', padding: '0px 4px' }}>
<Icon onClick={onClick} icon={'add'} />
</div>
);
};
const RightComponent = (props: IDockviewHeaderActionsProps) => {
const [popout, setPopout] = React.useState<boolean>(
props.api.location === 'popout'
);
React.useEffect(() => {
const disposable = props.group.api.onDidRenderPositionChange(
(event) => [setPopout(event.location === 'popout')]
);
return () => {
disposable.dispose();
};
}, [props.group.api]);
const onClick = () => {
if (popout) {
const group = props.containerApi.addGroup();
props.group.api.moveTo({ group });
} else {
props.containerApi.addPopoutGroup(props.group);
}
};
return (
<div style={{ height: '100%', color: 'white', padding: '0px 4px' }}>
<Icon
onClick={onClick}
icon={popout ? 'jump_to_element' : 'back_to_tab'}
/>
</div>
);
};
export default DockviewPersistance;
const Watermark = () => {
return <div style={{ color: 'white', padding: '8px' }}>watermark</div>;
};

View File

@ -0,0 +1,20 @@
import { StrictMode } from 'react';
import * as ReactDOMClient from 'react-dom/client';
import './styles.css';
import 'dockview/dist/styles/dockview.css';
import App from './app';
const rootElement = document.getElementById('root');
if (rootElement) {
const root = ReactDOMClient.createRoot(rootElement);
root.render(
<StrictMode>
<div className="app">
<App />
</div>
</StrictMode>
);
}

View File

@ -0,0 +1,16 @@
body {
margin: 0px;
color: white;
font-family: sans-serif;
text-align: center;
}
#root {
height: 100vh;
width: 100vw;
}
.app {
height: 100%;
}

View File

@ -0,0 +1,30 @@
import * as React from 'react';
export const Icon = (props: {
icon: string;
title?: string;
onClick?: (event: React.MouseEvent) => void;
}) => {
return (
<div
title={props.title}
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '30px',
height: '100%',
fontSize: '18px',
}}
onClick={props.onClick}
>
<span
style={{ fontSize: 'inherit', cursor: 'pointer' }}
className="material-symbols-outlined"
>
{props.icon}
</span>
</div>
);
};

View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"outDir": "build/dist",
"module": "esnext",
"target": "es5",
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"jsx": "react-jsx",
"moduleResolution": "node",
"rootDir": "src",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}

View File

@ -359,6 +359,11 @@
"signature": "<T extends object = Parameters>(options: AddPanelOptions<T>): IDockviewPanel",
"type": "method"
},
{
"name": "addPopoutGroup",
"signature": "(item: IDockviewPanel | DockviewGroupPanel, options?: { position: Box, skipRemoveGroup: boolean }): void",
"type": "method"
},
{
"name": "clear",
"comment": {
@ -1525,11 +1530,21 @@
"signature": "Event<void>",
"type": "property"
},
{
"name": "onDidRendererChange",
"signature": "Event<RendererChangedEvent>",
"type": "property"
},
{
"name": "onDidVisibilityChange",
"signature": "Event<VisibilityEvent>",
"type": "property"
},
{
"name": "renderer",
"signature": "DockviewPanelRenderer",
"type": "property"
},
{
"name": "title",
"signature": "string | undefined",
@ -1563,6 +1578,11 @@
"signature": "(): void",
"type": "method"
},
{
"name": "setRenderer",
"signature": "(renderer: DockviewPanelRenderer): void",
"type": "method"
},
{
"name": "setSize",
"signature": "(event: SizeEvent): void",
@ -2005,6 +2025,16 @@
"signature": "PanelCollection<IDockviewPanelProps<any>>",
"type": "property"
},
{
"name": "debug",
"signature": "boolean",
"type": "property"
},
{
"name": "defaultRenderer",
"signature": "DockviewPanelRenderer",
"type": "property"
},
{
"name": "defaultTabComponent",
"signature": "FunctionComponent<IDockviewPanelHeaderProps<any>>",

View File

@ -0,0 +1,5 @@
import React from 'react';
export default function Popout() {
return <div className="popout-anchor" />;
}