Merge pull request #364 from mathuo/360-investigate-opening-tabs-in-new-browser-window

work-in-progress: popout windows
This commit is contained in:
mathuo 2024-01-01 22:28:00 +00:00 committed by GitHub
commit eb0172f06c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1103 additions and 212 deletions

View File

@ -11,7 +11,7 @@ describe('groupDragHandler', () => {
const groupMock = jest.fn<DockviewGroupPanel, []>(() => { const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = { const partial: Partial<DockviewGroupPanel> = {
id: 'test_group_id', id: 'test_group_id',
api: { isFloating: false } as any, api: { location: 'grid' } as any,
}; };
return partial as DockviewGroupPanel; return partial as DockviewGroupPanel;
}); });
@ -48,12 +48,12 @@ describe('groupDragHandler', () => {
cut.dispose(); 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 element = document.createElement('div');
const groupMock = jest.fn<DockviewGroupPanel, []>(() => { const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = { const partial: Partial<DockviewGroupPanel> = {
api: { isFloating: true } as any, api: { location: 'floating' } as any,
}; };
return partial as DockviewGroupPanel; return partial as DockviewGroupPanel;
}); });
@ -85,7 +85,7 @@ describe('groupDragHandler', () => {
const groupMock = jest.fn<DockviewGroupPanel, []>(() => { const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = { const partial: Partial<DockviewGroupPanel> = {
api: { isFloating: false } as any, api: { location: 'grid' } as any,
}; };
return partial as DockviewGroupPanel; return partial as DockviewGroupPanel;
}); });

View File

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

View File

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

View File

@ -42,6 +42,7 @@ import {
GroupDragEvent, GroupDragEvent,
TabDragEvent, TabDragEvent,
} from '../dockview/components/titlebar/tabsContainer'; } from '../dockview/components/titlebar/tabsContainer';
import { Box } from '../types';
export interface CommonApi<T = any> { export interface CommonApi<T = any> {
readonly height: number; readonly height: number;
@ -804,4 +805,17 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
moveToPrevious(options?: MovementOptions): void { moveToPrevious(options?: MovementOptions): void {
this.component.moveToPrevious(options); this.component.moveToPrevious(options);
} }
/**
* 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 { Position } from '../dnd/droptarget';
import { DockviewComponent } from '../dockview/dockviewComponent'; import { DockviewComponent } from '../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel'; import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { DockviewGroupLocation } from '../dockview/dockviewGroupPanelModel';
import { Emitter, Event } from '../events'; import { Emitter, Event } from '../events';
import { GridviewPanelApi, GridviewPanelApiImpl } from './gridviewPanelApi'; import { GridviewPanelApi, GridviewPanelApiImpl } from './gridviewPanelApi';
export interface DockviewGroupPanelApi extends GridviewPanelApi { export interface DockviewGroupPanelApi extends GridviewPanelApi {
readonly onDidFloatingStateChange: Event<DockviewGroupPanelFloatingChangeEvent>; readonly onDidRenderPositionChange: Event<DockviewGroupPanelFloatingChangeEvent>;
readonly isFloating: boolean; readonly location: DockviewGroupLocation;
moveTo(options: { group: DockviewGroupPanel; position?: Position }): void; moveTo(options: { group: DockviewGroupPanel; position?: Position }): void;
} }
export interface DockviewGroupPanelFloatingChangeEvent { export interface DockviewGroupPanelFloatingChangeEvent {
readonly isFloating: boolean; readonly location: DockviewGroupLocation;
} }
export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl { export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
private _group: DockviewGroupPanel | undefined; private _group: DockviewGroupPanel | undefined;
readonly _onDidFloatingStateChange = readonly _onDidRenderPositionChange =
new Emitter<DockviewGroupPanelFloatingChangeEvent>(); new Emitter<DockviewGroupPanelFloatingChangeEvent>();
readonly onDidFloatingStateChange: Event<DockviewGroupPanelFloatingChangeEvent> = readonly onDidRenderPositionChange: Event<DockviewGroupPanelFloatingChangeEvent> =
this._onDidFloatingStateChange.event; this._onDidRenderPositionChange.event;
get isFloating() { get location(): DockviewGroupLocation {
if (!this._group) { if (!this._group) {
throw new Error(`DockviewGroupPanelApiImpl not initialized`); throw new Error(`DockviewGroupPanelApiImpl not initialized`);
} }
return this._group.model.isFloating; return this._group.model.location;
} }
constructor(id: string, private readonly accessor: DockviewComponent) { constructor(id: string, private readonly accessor: DockviewComponent) {
super(id); super(id);
this.addDisposables(this._onDidFloatingStateChange); this.addDisposables(this._onDidRenderPositionChange);
} }
moveTo(options: { group: DockviewGroupPanel; position?: Position }): void { moveTo(options: { group: DockviewGroupPanel; position?: Position }): void {

View File

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

View File

@ -11,6 +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';
const bringElementToFront = (() => { const bringElementToFront = (() => {
let previous: HTMLElement | null = null; let previous: HTMLElement | null = null;
@ -48,11 +49,7 @@ export class Overlay extends CompositeDisposable {
} }
constructor( constructor(
private readonly options: { private readonly options: Box & {
height: number;
width: number;
left: number;
top: number;
container: HTMLElement; container: HTMLElement;
content: HTMLElement; content: HTMLElement;
minimumInViewportWidth?: number; minimumInViewportWidth?: number;
@ -86,14 +83,7 @@ export class Overlay extends CompositeDisposable {
}); });
} }
setBounds( setBounds(bounds: Partial<Box> = {}): void {
bounds: Partial<{
height: number;
width: number;
top: number;
left: number;
}> = {}
): 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`;
} }
@ -139,7 +129,7 @@ export class Overlay extends CompositeDisposable {
this._onDidChange.fire(); this._onDidChange.fire();
} }
toJSON(): { top: number; left: number; height: number; width: number } { toJSON(): Box {
const container = this.options.container.getBoundingClientRect(); const container = this.options.container.getBoundingClientRect();
const element = this._element.getBoundingClientRect(); const element = this._element.getBoundingClientRect();

View File

@ -85,7 +85,7 @@ export class GreadyRenderContainer extends CompositeDisposable {
toggleClass( toggleClass(
focusContainer, focusContainer,
'dv-render-overlay-float', '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(); const data = getPanelData();
if (!data && event.shiftKey && !this.group.isFloating) { if (
!data &&
event.shiftKey &&
this.group.location !== 'floating'
) {
return false; return false;
} }

View File

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

View File

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

View File

@ -130,6 +130,8 @@ export interface IDockviewGroupPanelModel extends IPanel {
): boolean; ): boolean;
} }
export type DockviewGroupLocation = 'grid' | 'floating' | 'popout';
export class DockviewGroupPanelModel export class DockviewGroupPanelModel
extends CompositeDisposable extends CompositeDisposable
implements IDockviewGroupPanelModel implements IDockviewGroupPanelModel
@ -141,11 +143,12 @@ export class DockviewGroupPanelModel
private watermark?: IWatermarkRenderer; private watermark?: IWatermarkRenderer;
private _isGroupActive = false; private _isGroupActive = false;
private _locked: DockviewGroupPanelLocked = false; private _locked: DockviewGroupPanelLocked = false;
private _isFloating = false;
private _rightHeaderActions: IHeaderActionsRenderer | undefined; private _rightHeaderActions: IHeaderActionsRenderer | undefined;
private _leftHeaderActions: IHeaderActionsRenderer | undefined; private _leftHeaderActions: IHeaderActionsRenderer | undefined;
private _prefixHeaderActions: IHeaderActionsRenderer | undefined; private _prefixHeaderActions: IHeaderActionsRenderer | undefined;
private _location: DockviewGroupLocation = 'grid';
private mostRecentlyUsed: IDockviewPanel[] = []; private mostRecentlyUsed: IDockviewPanel[] = [];
private readonly _onDidChange = new Emitter<IViewSize | undefined>(); private readonly _onDidChange = new Emitter<IViewSize | undefined>();
@ -241,21 +244,45 @@ export class DockviewGroupPanelModel
); );
} }
get isFloating(): boolean { get location(): DockviewGroupLocation {
return this._isFloating; return this._location;
} }
set isFloating(value: boolean) { set location(value: DockviewGroupLocation) {
this._isFloating = value; 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( this.contentContainer.dropTarget.setTargetZones(
value ? ['center'] : ['top', 'bottom', 'left', 'right', 'center'] 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({ break;
isFloating: this.isFloating, 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; minimumHeightWithinViewport?: number;
minimumWidthWithinViewport?: number; minimumWidthWithinViewport?: number;
}; };
popoutUrl?: string;
defaultRenderer?: DockviewPanelRenderer; defaultRenderer?: DockviewPanelRenderer;
debug?: boolean; debug?: boolean;
} }

View File

@ -186,6 +186,38 @@ export function quasiDefaultPrevented(event: Event): boolean {
return (event as any)[QUASI_PREVENT_DEFAULT_KEY]; 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): { export function getDomNodePagePosition(domNode: Element): {
left: number; left: number;
top: 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 FunctionOrValue<T> = (() => T) | T;
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>; 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 DockviewFloating from '@site/sandboxes/floatinggroup-dockview/src/app';
import DockviewLockedGroup from '@site/sandboxes/lockedgroup-dockview/src/app'; import DockviewLockedGroup from '@site/sandboxes/lockedgroup-dockview/src/app';
import DockviewKeyboard from '@site/sandboxes/keyboard-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 DockviewRenderMode from '@site/sandboxes/rendermode-dockview/src/app';
import { DocRef } from '@site/src/components/ui/reference/docRef'; 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')} /> <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 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: 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} 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 ## Panels
### Add Panel ### Add Panel

View File

@ -255,13 +255,15 @@ const LeftComponent = (props: IDockviewHeaderActionsProps) => {
const RightComponent = (props: IDockviewHeaderActionsProps) => { const RightComponent = (props: IDockviewHeaderActionsProps) => {
const [floating, setFloating] = React.useState<boolean>( const [floating, setFloating] = React.useState<boolean>(
props.api.isFloating props.api.location === 'floating'
); );
React.useEffect(() => { React.useEffect(() => {
const disposable = props.group.api.onDidFloatingStateChange((event) => [ const disposable = props.group.api.onDidRenderPositionChange(
setFloating(event.isFloating), (event) => {
]); setFloating(event.location === 'floating');
}
);
return () => { return () => {
disposable.dispose(); 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", "signature": "<T extends object = Parameters>(options: AddPanelOptions<T>): IDockviewPanel",
"type": "method" "type": "method"
}, },
{
"name": "addPopoutGroup",
"signature": "(item: IDockviewPanel | DockviewGroupPanel, options?: { position: Box, skipRemoveGroup: boolean }): void",
"type": "method"
},
{ {
"name": "clear", "name": "clear",
"comment": { "comment": {
@ -1525,11 +1530,21 @@
"signature": "Event<void>", "signature": "Event<void>",
"type": "property" "type": "property"
}, },
{
"name": "onDidRendererChange",
"signature": "Event<RendererChangedEvent>",
"type": "property"
},
{ {
"name": "onDidVisibilityChange", "name": "onDidVisibilityChange",
"signature": "Event<VisibilityEvent>", "signature": "Event<VisibilityEvent>",
"type": "property" "type": "property"
}, },
{
"name": "renderer",
"signature": "DockviewPanelRenderer",
"type": "property"
},
{ {
"name": "title", "name": "title",
"signature": "string | undefined", "signature": "string | undefined",
@ -1563,6 +1578,11 @@
"signature": "(): void", "signature": "(): void",
"type": "method" "type": "method"
}, },
{
"name": "setRenderer",
"signature": "(renderer: DockviewPanelRenderer): void",
"type": "method"
},
{ {
"name": "setSize", "name": "setSize",
"signature": "(event: SizeEvent): void", "signature": "(event: SizeEvent): void",
@ -2005,6 +2025,16 @@
"signature": "PanelCollection<IDockviewPanelProps<any>>", "signature": "PanelCollection<IDockviewPanelProps<any>>",
"type": "property" "type": "property"
}, },
{
"name": "debug",
"signature": "boolean",
"type": "property"
},
{
"name": "defaultRenderer",
"signature": "DockviewPanelRenderer",
"type": "property"
},
{ {
"name": "defaultTabComponent", "name": "defaultTabComponent",
"signature": "FunctionComponent<IDockviewPanelHeaderProps<any>>", "signature": "FunctionComponent<IDockviewPanelHeaderProps<any>>",

View File

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