Merge pull request #481 from mathuo/469-add-window-lifecycle-callbacks-1

feat: provide means to obtain popoutWindow document
This commit is contained in:
mathuo 2024-01-29 22:27:45 +00:00 committed by GitHub
commit aa8e7e09e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 806 additions and 484 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: { location: 'grid' } as any, api: { location: { type: 'grid' } } as any,
}; };
return partial as DockviewGroupPanel; return partial as DockviewGroupPanel;
}); });
@ -53,7 +53,7 @@ describe('groupDragHandler', () => {
const groupMock = jest.fn<DockviewGroupPanel, []>(() => { const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = { const partial: Partial<DockviewGroupPanel> = {
api: { location: 'floating' } as any, api: { location: { type: '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: { location: 'grid' } as any, api: { location: { type: 'grid' } } as any,
}; };
return partial as DockviewGroupPanel; return partial as DockviewGroupPanel;
}); });

View File

@ -9,6 +9,7 @@ import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanel
import { fireEvent } from '@testing-library/dom'; import { fireEvent } from '@testing-library/dom';
import { TestPanel } from '../../dockviewGroupPanelModel.spec'; import { TestPanel } from '../../dockviewGroupPanelModel.spec';
import { IDockviewPanel } from '../../../../dockview/dockviewPanel'; import { IDockviewPanel } from '../../../../dockview/dockviewPanel';
import { fromPartial } from '@total-typescript/shoehorn';
describe('tabsContainer', () => { describe('tabsContainer', () => {
test('that an external event does not render a drop target and calls through to the group mode', () => { test('that an external event does not render a drop target and calls through to the group mode', () => {
@ -478,7 +479,7 @@ describe('tabsContainer', () => {
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{ return (<Partial<DockviewGroupPanel>>{
api: { location: 'grid' } as any, api: { location: { type: 'grid' } } as any,
}) as DockviewGroupPanel; }) as DockviewGroupPanel;
}); });
@ -538,7 +539,7 @@ describe('tabsContainer', () => {
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{ return (<Partial<DockviewGroupPanel>>{
api: { location: 'floating' } as any, api: { location: { type: 'floating' } } as any,
}) as DockviewGroupPanel; }) as DockviewGroupPanel;
}); });
@ -591,7 +592,7 @@ describe('tabsContainer', () => {
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{ return (<Partial<DockviewGroupPanel>>{
api: { location: 'floating' } as any, api: { location: { type: 'floating' } } as any,
model: {} as any, model: {} as any,
}) as DockviewGroupPanel; }) as DockviewGroupPanel;
}); });
@ -601,23 +602,20 @@ describe('tabsContainer', () => {
const cut = new TabsContainer(accessor, groupPanel); const cut = new TabsContainer(accessor, groupPanel);
const panelMock = jest.fn<IDockviewPanel, [string]>((id: string) => { const createPanel = (id: string) =>
const partial: Partial<IDockviewPanel> = { fromPartial<IDockviewPanel>({
id, id,
view: { view: {
tab: { tab: {
element: document.createElement('div'), element: document.createElement('div'),
} as any, },
content: { content: {
element: document.createElement('div'), element: document.createElement('div'),
} as any, },
} as any, },
};
return partial as IDockviewPanel;
}); });
const panel = new panelMock('test_id'); const panel = createPanel('test_id');
cut.openPanel(panel); cut.openPanel(panel);
const el = cut.element.querySelector('.tab')!; const el = cut.element.querySelector('.tab')!;
@ -628,15 +626,15 @@ describe('tabsContainer', () => {
fireEvent(el, event); fireEvent(el, event);
// a floating group with a single tab shouldn't be eligible // a floating group with a single tab shouldn't be eligible
expect(preventDefaultSpy).toBeCalledTimes(0); expect(preventDefaultSpy).toHaveBeenCalledTimes(0);
expect(accessor.addFloatingGroup).toBeCalledTimes(0); expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(0);
const panel2 = new panelMock('test_id_2'); const panel2 = createPanel('test_id_2');
cut.openPanel(panel2); cut.openPanel(panel2);
fireEvent(el, event); fireEvent(el, event);
expect(preventDefaultSpy).toBeCalledTimes(1); expect(preventDefaultSpy).toHaveBeenCalledTimes(1);
expect(accessor.addFloatingGroup).toBeCalledTimes(1); expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(1);
}); });
test('pre header actions', () => { test('pre header actions', () => {
@ -653,7 +651,7 @@ describe('tabsContainer', () => {
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{ return (<Partial<DockviewGroupPanel>>{
api: { location: 'grid' } as any, api: { location: { type: 'grid' } } as any,
model: {} as any, model: {} as any,
}) as DockviewGroupPanel; }) as DockviewGroupPanel;
}); });
@ -723,7 +721,7 @@ describe('tabsContainer', () => {
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{ return (<Partial<DockviewGroupPanel>>{
api: { location: 'grid' } as any, api: { location: { type: 'grid' } } as any,
model: {} as any, model: {} as any,
}) as DockviewGroupPanel; }) as DockviewGroupPanel;
}); });
@ -793,7 +791,7 @@ describe('tabsContainer', () => {
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{ return (<Partial<DockviewGroupPanel>>{
api: { location: 'grid' } as any, api: { location: { type: 'grid' } } as any,
model: {} as any, model: {} as any,
}) as DockviewGroupPanel; }) as DockviewGroupPanel;
}); });

View File

@ -110,102 +110,109 @@ describe('dockviewComponent', () => {
window.open = jest.fn(); // not implemented by jest window.open = jest.fn(); // not implemented by jest
}); });
describe('memory leakage', () => { // describe('memory leakage', () => {
test('event leakage', () => { // beforeEach(() => {
Emitter.setLeakageMonitorEnabled(true); // window.open = () => fromPartial<Window>({
// addEventListener: jest.fn(),
// close: jest.fn(),
// });
// });
dockview = new DockviewComponent({ // test('event leakage', () => {
parentElement: container, // Emitter.setLeakageMonitorEnabled(true);
components: {
default: PanelContentPartTest,
},
});
dockview.layout(500, 1000); // dockview = new DockviewComponent({
// parentElement: container,
// components: {
// default: PanelContentPartTest,
// },
// });
const panel1 = dockview.addPanel({ // dockview.layout(500, 1000);
id: 'panel1',
component: 'default',
});
const panel2 = dockview.addPanel({ // const panel1 = dockview.addPanel({
id: 'panel2', // id: 'panel1',
component: 'default', // component: 'default',
}); // });
dockview.removePanel(panel2); // const panel2 = dockview.addPanel({
// id: 'panel2',
// component: 'default',
// });
const panel3 = dockview.addPanel({ // dockview.removePanel(panel2);
id: 'panel3',
component: 'default',
position: {
direction: 'right',
referencePanel: 'panel1',
},
});
const panel4 = dockview.addPanel({ // const panel3 = dockview.addPanel({
id: 'panel4', // id: 'panel3',
component: 'default', // component: 'default',
position: { // position: {
direction: 'above', // direction: 'right',
}, // referencePanel: 'panel1',
}); // },
// });
dockview.moveGroupOrPanel( // const panel4 = dockview.addPanel({
panel4.group, // id: 'panel4',
panel3.group.id, // component: 'default',
panel3.id, // position: {
'center' // direction: 'above',
); // },
// });
dockview.addPanel({ // dockview.moveGroupOrPanel(
id: 'panel5', // panel4.group,
component: 'default', // panel3.group.id,
floating: true, // panel3.id,
}); // 'center'
// );
const panel6 = dockview.addPanel({ // dockview.addPanel({
id: 'panel6', // id: 'panel5',
component: 'default', // component: 'default',
position: { // floating: true,
referencePanel: 'panel5', // });
direction: 'within',
},
});
dockview.addFloatingGroup(panel4.api.group); // const panel6 = dockview.addPanel({
// id: 'panel6',
// component: 'default',
// position: {
// referencePanel: 'panel5',
// direction: 'within',
// },
// });
dockview.addPopoutGroup(panel6); // dockview.addFloatingGroup(panel4.api.group);
dockview.moveGroupOrPanel( // dockview.addPopoutGroup(panel6);
panel1.group,
panel6.group.id,
panel6.id,
'center'
);
dockview.moveGroupOrPanel( // dockview.moveGroupOrPanel(
panel4.group, // panel1.group,
panel6.group.id, // panel6.group.id,
panel6.id, // panel6.id,
'center' // 'center'
); // );
dockview.dispose(); // dockview.moveGroupOrPanel(
// panel4.group,
// panel6.group.id,
// panel6.id,
// 'center'
// );
if (Emitter.MEMORY_LEAK_WATCHER.size > 0) { // dockview.dispose();
for (const entry of Array.from(
Emitter.MEMORY_LEAK_WATCHER.events
)) {
console.log('disposal', entry[1]);
}
throw new Error('not all listeners disposed');
}
Emitter.setLeakageMonitorEnabled(false); // if (Emitter.MEMORY_LEAK_WATCHER.size > 0) {
}); // for (const entry of Array.from(
}); // Emitter.MEMORY_LEAK_WATCHER.events
// )) {
// console.log('disposal', entry[1]);
// }
// throw new Error('not all listeners disposed');
// }
// Emitter.setLeakageMonitorEnabled(false);
// });
// });
test('duplicate panel', () => { test('duplicate panel', () => {
dockview.layout(500, 1000); dockview.layout(500, 1000);
@ -3452,8 +3459,8 @@ describe('dockviewComponent', () => {
floating: true, floating: true,
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).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);
@ -3464,8 +3471,8 @@ describe('dockviewComponent', () => {
'right' 'right'
); );
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).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);
}); });
@ -3497,8 +3504,8 @@ describe('dockviewComponent', () => {
floating: true, floating: true,
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).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);
@ -3509,8 +3516,8 @@ describe('dockviewComponent', () => {
'center' 'center'
); );
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).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);
}); });
@ -3548,9 +3555,9 @@ describe('dockviewComponent', () => {
floating: true, floating: true,
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).toBe('floating');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
@ -3561,9 +3568,9 @@ describe('dockviewComponent', () => {
'center' 'center'
); );
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).toBe('floating');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
}); });
@ -3601,9 +3608,9 @@ describe('dockviewComponent', () => {
position: { referencePanel: panel2 }, position: { referencePanel: panel2 },
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).toBe('floating');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
@ -3614,9 +3621,9 @@ describe('dockviewComponent', () => {
'right' 'right'
); );
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).toBe('grid');
expect(panel3.group.api.location).toBe('grid'); expect(panel3.group.api.location.type).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);
}); });
@ -3654,9 +3661,9 @@ describe('dockviewComponent', () => {
position: { referencePanel: panel2 }, position: { referencePanel: panel2 },
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).toBe('floating');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
@ -3667,9 +3674,9 @@ describe('dockviewComponent', () => {
'center' 'center'
); );
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).toBe('grid');
expect(panel3.group.api.location).toBe('grid'); expect(panel3.group.api.location.type).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);
}); });
@ -3713,10 +3720,10 @@ describe('dockviewComponent', () => {
floating: true, floating: true,
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).toBe('floating');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).toBe('floating');
expect(panel4.group.api.location).toBe('floating'); expect(panel4.group.api.location.type).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);
@ -3727,10 +3734,10 @@ describe('dockviewComponent', () => {
'center' 'center'
); );
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).toBe('floating');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).toBe('floating');
expect(panel4.group.api.location).toBe('floating'); expect(panel4.group.api.location.type).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);
}); });
@ -3762,8 +3769,8 @@ describe('dockviewComponent', () => {
floating: true, floating: true,
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).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);
@ -3774,8 +3781,8 @@ describe('dockviewComponent', () => {
'right' 'right'
); );
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).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);
}); });
@ -3807,8 +3814,8 @@ describe('dockviewComponent', () => {
floating: true, floating: true,
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).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);
@ -3819,8 +3826,8 @@ describe('dockviewComponent', () => {
'center' 'center'
); );
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).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);
}); });
@ -3858,9 +3865,9 @@ describe('dockviewComponent', () => {
floating: true, floating: true,
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).toBe('floating');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
@ -3871,9 +3878,9 @@ describe('dockviewComponent', () => {
'center' 'center'
); );
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).toBe('floating');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
}); });
@ -3911,9 +3918,9 @@ describe('dockviewComponent', () => {
position: { referencePanel: panel2 }, position: { referencePanel: panel2 },
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).toBe('floating');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
@ -3924,9 +3931,9 @@ describe('dockviewComponent', () => {
'right' 'right'
); );
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).toBe('grid');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
}); });
@ -3964,9 +3971,9 @@ describe('dockviewComponent', () => {
position: { referencePanel: panel2 }, position: { referencePanel: panel2 },
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).toBe('floating');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
@ -3977,9 +3984,9 @@ describe('dockviewComponent', () => {
'center' 'center'
); );
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).toBe('grid');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
}); });
@ -4023,10 +4030,10 @@ describe('dockviewComponent', () => {
floating: true, floating: true,
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).toBe('floating');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).toBe('floating');
expect(panel4.group.api.location).toBe('floating'); expect(panel4.group.api.location.type).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);
@ -4037,10 +4044,10 @@ describe('dockviewComponent', () => {
'center' 'center'
); );
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).toBe('floating');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).toBe('floating');
expect(panel4.group.api.location).toBe('floating'); expect(panel4.group.api.location.type).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);
}); });
@ -4078,9 +4085,9 @@ describe('dockviewComponent', () => {
floating: true, floating: true,
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).toBe('grid');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
@ -4091,9 +4098,9 @@ describe('dockviewComponent', () => {
'center' 'center'
); );
expect(panel1.group.api.location).toBe('floating'); expect(panel1.group.api.location.type).toBe('floating');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).toBe('grid');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
}); });
@ -4130,9 +4137,9 @@ describe('dockviewComponent', () => {
floating: true, floating: true,
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).toBe('grid');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
@ -4143,9 +4150,9 @@ describe('dockviewComponent', () => {
'center' 'center'
); );
expect(panel1.group.api.location).toBe('floating'); expect(panel1.group.api.location.type).toBe('floating');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).toBe('grid');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
}); });
@ -4183,9 +4190,9 @@ describe('dockviewComponent', () => {
floating: true, floating: true,
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).toBe('grid');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
@ -4196,9 +4203,9 @@ describe('dockviewComponent', () => {
'center' 'center'
); );
expect(panel1.group.api.location).toBe('floating'); expect(panel1.group.api.location.type).toBe('floating');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).toBe('grid');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
}); });
@ -4235,9 +4242,9 @@ describe('dockviewComponent', () => {
floating: true, floating: true,
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).toBe('grid');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
@ -4248,9 +4255,9 @@ describe('dockviewComponent', () => {
'center' 'center'
); );
expect(panel1.group.api.location).toBe('floating'); expect(panel1.group.api.location.type).toBe('floating');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).toBe('floating');
expect(panel3.group.api.location).toBe('floating'); expect(panel3.group.api.location.type).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);
}); });
@ -4282,15 +4289,15 @@ describe('dockviewComponent', () => {
position: { direction: 'right' }, position: { direction: 'right' },
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).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.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).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);
}); });
@ -4321,15 +4328,15 @@ describe('dockviewComponent', () => {
component: 'default', component: 'default',
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).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.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).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);
}); });
@ -4361,15 +4368,15 @@ describe('dockviewComponent', () => {
position: { direction: 'right' }, position: { direction: 'right' },
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).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.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).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);
}); });
@ -4400,22 +4407,40 @@ describe('dockviewComponent', () => {
component: 'default', component: 'default',
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).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.location).toBe('floating'); expect(panel1.group.api.location.type).toBe('floating');
expect(panel2.group.api.location).toBe('floating'); expect(panel2.group.api.location.type).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);
}); });
}); });
describe('popout group', () => { describe('popout group', () => {
test('that can remove a popout group', () => { beforeEach(() => {
jest.spyOn(window, 'open').mockReturnValue(
fromPartial<Window>({
document: fromPartial<Document>({
body: document.createElement('body'),
}),
addEventListener: jest
.fn()
.mockImplementation((name, cb) => {
if (name === 'load') {
cb();
}
}),
close: jest.fn(),
})
);
});
test('that can remove a popout group', async () => {
const container = document.createElement('div'); const container = document.createElement('div');
const dockview = new DockviewComponent({ const dockview = new DockviewComponent({
@ -4436,11 +4461,11 @@ describe('dockviewComponent', () => {
component: 'default', component: 'default',
}); });
dockview.addPopoutGroup(panel1); await dockview.addPopoutGroup(panel1);
expect(dockview.panels.length).toBe(1); expect(dockview.panels.length).toBe(1);
expect(dockview.groups.length).toBe(1); expect(dockview.groups.length).toBe(2);
expect(panel1.api.group.api.location).toBe('popout'); expect(panel1.api.group.api.location.type).toBe('popout');
dockview.removePanel(panel1); dockview.removePanel(panel1);
@ -4448,7 +4473,7 @@ describe('dockviewComponent', () => {
expect(dockview.groups.length).toBe(0); expect(dockview.groups.length).toBe(0);
}); });
test('add a popout group', () => { test('add a popout group', async () => {
const container = document.createElement('div'); const container = document.createElement('div');
const dockview = new DockviewComponent({ const dockview = new DockviewComponent({
@ -4474,20 +4499,20 @@ describe('dockviewComponent', () => {
component: 'default', component: 'default',
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).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.addPopoutGroup(panel2.group); await dockview.addPopoutGroup(panel2.group);
expect(panel1.group.api.location).toBe('popout'); expect(panel1.group.api.location.type).toBe('popout');
expect(panel2.group.api.location).toBe('popout'); expect(panel2.group.api.location.type).toBe('popout');
expect(dockview.groups.length).toBe(1); expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(2); expect(dockview.panels.length).toBe(2);
}); });
test('move from fixed to popout group and back', () => { test('move from fixed to popout group and back', async () => {
const container = document.createElement('div'); const container = document.createElement('div');
const dockview = new DockviewComponent({ const dockview = new DockviewComponent({
@ -4521,18 +4546,18 @@ describe('dockviewComponent', () => {
}, },
}); });
expect(panel1.group.api.location).toBe('grid'); expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).toBe('grid');
expect(panel3.group.api.location).toBe('grid'); expect(panel3.group.api.location.type).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);
dockview.addPopoutGroup(panel2.group); await dockview.addPopoutGroup(panel2.group);
expect(panel1.group.api.location).toBe('popout'); expect(panel1.group.api.location.type).toBe('popout');
expect(panel2.group.api.location).toBe('popout'); expect(panel2.group.api.location.type).toBe('popout');
expect(panel3.group.api.location).toBe('grid'); expect(panel3.group.api.location.type).toBe('grid');
expect(dockview.groups.length).toBe(2); expect(dockview.groups.length).toBe(3);
expect(dockview.panels.length).toBe(3); expect(dockview.panels.length).toBe(3);
dockview.moveGroupOrPanel( dockview.moveGroupOrPanel(
@ -4542,10 +4567,23 @@ describe('dockviewComponent', () => {
'right' 'right'
); );
expect(panel1.group.api.location).toBe('popout'); expect(panel1.group.api.location.type).toBe('popout');
expect(panel2.group.api.location).toBe('grid'); expect(panel2.group.api.location.type).toBe('grid');
expect(panel3.group.api.location).toBe('grid'); expect(panel3.group.api.location.type).toBe('grid');
expect(dockview.groups.length).toBe(3); expect(dockview.groups.length).toBe(4);
expect(dockview.panels.length).toBe(3);
dockview.moveGroupOrPanel(
panel3.api.group,
panel1.api.group.id,
panel1.api.id,
'center'
);
expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location.type).toBe('grid');
expect(panel3.group.api.location.type).toBe('grid');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(3); expect(dockview.panels.length).toBe(3);
}); });
}); });

View File

@ -268,7 +268,7 @@ describe('gridview', () => {
], ],
}, },
}, },
activePanel: 'panel_1', activePanel: 'panel_2',
}); });
}); });

View File

@ -41,7 +41,7 @@ describe('overlayRenderContainer', () => {
}, },
group: { group: {
api: { api: {
location: 'grid', location: { type: 'grid' },
}, },
}, },
}); });
@ -77,7 +77,7 @@ describe('overlayRenderContainer', () => {
}, },
group: { group: {
api: { api: {
location: 'grid', location: { type: 'grid' },
}, },
}, },
}); });

View File

@ -830,8 +830,10 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
options?: { options?: {
position?: Box; position?: Box;
popoutUrl?: string; popoutUrl?: string;
onDidOpen?: (event: { id: string; window: Window }) => void;
onWillClose?: (event: { id: string; window: Window }) => void;
} }
): void { ): Promise<void> {
this.component.addPopoutGroup(item, options); return this.component.addPopoutGroup(item, options);
} }
} }

View File

@ -8,6 +8,10 @@ import { GridviewPanelApi, GridviewPanelApiImpl } from './gridviewPanelApi';
export interface DockviewGroupPanelApi extends GridviewPanelApi { export interface DockviewGroupPanelApi extends GridviewPanelApi {
readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent>; readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent>;
readonly location: DockviewGroupLocation; readonly location: DockviewGroupLocation;
/**
* If you require the Window object
*/
getWindow(): Window;
moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void; moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void;
maximize(): void; maximize(): void;
isMaximized(): boolean; isMaximized(): boolean;
@ -42,6 +46,12 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
this.addDisposables(this._onDidLocationChange); this.addDisposables(this._onDidLocationChange);
} }
getWindow(): Window {
return this.location.type === 'popout'
? this.location.getWindow()
: window;
}
moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void { moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void {
if (!this._group) { if (!this._group) {
throw new Error(NOT_INITIALIZED_MESSAGE); throw new Error(NOT_INITIALIZED_MESSAGE);
@ -66,7 +76,7 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
throw new Error(NOT_INITIALIZED_MESSAGE); throw new Error(NOT_INITIALIZED_MESSAGE);
} }
if (this.location !== 'grid') { if (this.location.type !== 'grid') {
// only grid groups can be maximized // only grid groups can be maximized
return; return;
} }

View File

@ -1,11 +1,13 @@
import { Emitter, Event } from '../events'; import { Emitter, Event } from '../events';
import { GridviewPanelApiImpl, GridviewPanelApi } from './gridviewPanelApi'; import { GridviewPanelApiImpl, GridviewPanelApi } from './gridviewPanelApi';
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel'; import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { MutableDisposable } from '../lifecycle'; import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { DockviewPanel } from '../dockview/dockviewPanel'; import { DockviewPanel } from '../dockview/dockviewPanel';
import { DockviewComponent } from '../dockview/dockviewComponent'; import { DockviewComponent } from '../dockview/dockviewComponent';
import { Position } from '../dnd/droptarget'; import { Position } from '../dnd/droptarget';
import { DockviewPanelRenderer } from '../overlayRenderContainer'; import { DockviewPanelRenderer } from '../overlayRenderContainer';
import { DockviewGroupPanelFloatingChangeEvent } from './dockviewGroupPanelApi';
import { DockviewGroupLocation } from '../dockview/dockviewGroupPanelModel';
export interface TitleEvent { export interface TitleEvent {
readonly title: string; readonly title: string;
@ -28,6 +30,8 @@ export interface DockviewPanelApi
readonly onDidActiveGroupChange: Event<void>; readonly onDidActiveGroupChange: Event<void>;
readonly onDidGroupChange: Event<void>; readonly onDidGroupChange: Event<void>;
readonly onDidRendererChange: Event<RendererChangedEvent>; readonly onDidRendererChange: Event<RendererChangedEvent>;
readonly location: DockviewGroupLocation;
readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent>;
close(): void; close(): void;
setTitle(title: string): void; setTitle(title: string): void;
setRenderer(renderer: DockviewPanelRenderer): void; setRenderer(renderer: DockviewPanelRenderer): void;
@ -39,6 +43,10 @@ export interface DockviewPanelApi
maximize(): void; maximize(): void;
isMaximized(): boolean; isMaximized(): boolean;
exitMaximized(): void; exitMaximized(): void;
/**
* If you require the Window object
*/
getWindow(): Window;
} }
export class DockviewPanelApiImpl export class DockviewPanelApiImpl
@ -59,7 +67,16 @@ export class DockviewPanelApiImpl
readonly _onDidRendererChange = new Emitter<RendererChangedEvent>(); readonly _onDidRendererChange = new Emitter<RendererChangedEvent>();
readonly onDidRendererChange = this._onDidRendererChange.event; readonly onDidRendererChange = this._onDidRendererChange.event;
private readonly disposable = new MutableDisposable(); private readonly _onDidLocationChange =
new Emitter<DockviewGroupPanelFloatingChangeEvent>();
readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent> =
this._onDidLocationChange.event;
private readonly groupEventsDisposable = new MutableDisposable();
get location(): DockviewGroupLocation {
return this.group.api.location;
}
get title(): string | undefined { get title(): string | undefined {
return this.panel.title; return this.panel.title;
@ -81,13 +98,22 @@ export class DockviewPanelApiImpl
this._onDidGroupChange.fire(); this._onDidGroupChange.fire();
if (this._group) { if (this._group) {
this.disposable.value = this._group.api.onDidActiveChange(() => { this.groupEventsDisposable.value = new CompositeDisposable(
this.group.api.onDidLocationChange((event) => {
this._onDidLocationChange.fire(event);
}),
this.group.api.onDidActiveChange(() => {
this._onDidActiveGroupChange.fire(); this._onDidActiveGroupChange.fire();
}); })
);
if (this.isGroupActive !== isOldGroupActive) { if (this.isGroupActive !== isOldGroupActive) {
this._onDidActiveGroupChange.fire(); this._onDidActiveGroupChange.fire();
} }
this._onDidLocationChange.fire({
location: this.group.api.location,
});
} }
} }
@ -107,14 +133,19 @@ export class DockviewPanelApiImpl
this._group = group; this._group = group;
this.addDisposables( this.addDisposables(
this.disposable, this.groupEventsDisposable,
this._onDidRendererChange, this._onDidRendererChange,
this._onDidTitleChange, this._onDidTitleChange,
this._onDidGroupChange, this._onDidGroupChange,
this._onDidActiveGroupChange this._onDidActiveGroupChange,
this._onDidLocationChange
); );
} }
getWindow(): Window {
return this.group.api.getWindow();
}
moveTo(options: { moveTo(options: {
group: DockviewGroupPanel; group: DockviewGroupPanel;
position?: Position; position?: Position;

View File

@ -14,6 +14,10 @@ export interface VisibilityEvent {
readonly isVisible: boolean; readonly isVisible: boolean;
} }
export interface HiddenEvent {
readonly isHidden: boolean;
}
export interface ActiveEvent { export interface ActiveEvent {
readonly isActive: boolean; readonly isActive: boolean;
} }
@ -24,7 +28,7 @@ export interface PanelApi {
readonly onDidFocusChange: Event<FocusEvent>; readonly onDidFocusChange: Event<FocusEvent>;
readonly onDidVisibilityChange: Event<VisibilityEvent>; readonly onDidVisibilityChange: Event<VisibilityEvent>;
readonly onDidActiveChange: Event<ActiveEvent>; readonly onDidActiveChange: Event<ActiveEvent>;
setVisible(isVisible: boolean): void; readonly onDidHiddenChange: Event<HiddenEvent>;
setActive(): void; setActive(): void;
updateParameters(parameters: Parameters): void; updateParameters(parameters: Parameters): void;
/** /**
@ -43,6 +47,10 @@ export interface PanelApi {
* Whether the panel is visible * Whether the panel is visible
*/ */
readonly isVisible: boolean; readonly isVisible: boolean;
/**
* Whether the panel is hidden
*/
readonly isHidden: boolean;
/** /**
* The panel width in pixels * The panel width in pixels
*/ */
@ -60,6 +68,7 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
private _isFocused = false; private _isFocused = false;
private _isActive = false; private _isActive = false;
private _isVisible = true; private _isVisible = true;
private _isHidden = false;
private _width = 0; private _width = 0;
private _height = 0; private _height = 0;
@ -69,56 +78,59 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
replay: true, replay: true,
}); });
readonly onDidDimensionsChange = this._onDidDimensionChange.event; readonly onDidDimensionsChange = this._onDidDimensionChange.event;
//
readonly _onDidChangeFocus = new Emitter<FocusEvent>({ readonly _onDidChangeFocus = new Emitter<FocusEvent>({
replay: true, replay: true,
}); });
readonly onDidFocusChange: Event<FocusEvent> = this._onDidChangeFocus.event; readonly onDidFocusChange: Event<FocusEvent> = this._onDidChangeFocus.event;
//
readonly _onFocusEvent = new Emitter<void>(); readonly _onFocusEvent = new Emitter<void>();
readonly onFocusEvent: Event<void> = this._onFocusEvent.event; readonly onFocusEvent: Event<void> = this._onFocusEvent.event;
//
readonly _onDidVisibilityChange = new Emitter<VisibilityEvent>({ readonly _onDidVisibilityChange = new Emitter<VisibilityEvent>({
replay: true, replay: true,
}); });
readonly onDidVisibilityChange: Event<VisibilityEvent> = readonly onDidVisibilityChange: Event<VisibilityEvent> =
this._onDidVisibilityChange.event; this._onDidVisibilityChange.event;
//
readonly _onVisibilityChange = new Emitter<VisibilityEvent>(); readonly _onDidHiddenChange = new Emitter<HiddenEvent>();
readonly onVisibilityChange: Event<VisibilityEvent> = readonly onDidHiddenChange: Event<HiddenEvent> =
this._onVisibilityChange.event; this._onDidHiddenChange.event;
//
readonly _onDidActiveChange = new Emitter<ActiveEvent>({ readonly _onDidActiveChange = new Emitter<ActiveEvent>({
replay: true, replay: true,
}); });
readonly onDidActiveChange: Event<ActiveEvent> = readonly onDidActiveChange: Event<ActiveEvent> =
this._onDidActiveChange.event; this._onDidActiveChange.event;
//
readonly _onActiveChange = new Emitter<void>(); readonly _onActiveChange = new Emitter<void>();
readonly onActiveChange: Event<void> = this._onActiveChange.event; readonly onActiveChange: Event<void> = this._onActiveChange.event;
//
readonly _onUpdateParameters = new Emitter<Parameters>(); readonly _onUpdateParameters = new Emitter<Parameters>();
readonly onUpdateParameters: Event<Parameters> = readonly onUpdateParameters: Event<Parameters> =
this._onUpdateParameters.event; this._onUpdateParameters.event;
//
get isFocused() { get isFocused(): boolean {
return this._isFocused; return this._isFocused;
} }
get isActive() { get isActive(): boolean {
return this._isActive; return this._isActive;
} }
get isVisible() {
get isVisible(): boolean {
return this._isVisible; return this._isVisible;
} }
get width() { get isHidden(): boolean {
return this._isHidden;
}
get width(): number {
return this._width; return this._width;
} }
get height() { get height(): number {
return this._height; return this._height;
} }
@ -135,6 +147,9 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
this.onDidVisibilityChange((event) => { this.onDidVisibilityChange((event) => {
this._isVisible = event.isVisible; this._isVisible = event.isVisible;
}), }),
this.onDidHiddenChange((event) => {
this._isHidden = event.isHidden;
}),
this.onDidDimensionsChange((event) => { this.onDidDimensionsChange((event) => {
this._width = event.width; this._width = event.width;
this._height = event.height; this._height = event.height;
@ -146,7 +161,7 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
this._onDidActiveChange, this._onDidActiveChange,
this._onFocusEvent, this._onFocusEvent,
this._onActiveChange, this._onActiveChange,
this._onVisibilityChange, this._onDidHiddenChange,
this._onUpdateParameters this._onUpdateParameters
); );
} }
@ -161,8 +176,8 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
); );
} }
setVisible(isVisible: boolean) { setHidden(isHidden: boolean): void {
this._onVisibilityChange.fire({ isVisible }); this._onDidHiddenChange.fire({ isHidden });
} }
setActive(): void { setActive(): void {
@ -172,8 +187,4 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
updateParameters(parameters: Parameters): void { updateParameters(parameters: Parameters): void {
this._onUpdateParameters.fire(parameters); this._onUpdateParameters.fire(parameters);
} }
dispose() {
super.dispose();
}
} }

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.location === 'floating' && !_event.shiftKey) { if (this.group.api.location.type === 'floating' && !_event.shiftKey) {
return true; return true;
} }
return false; return false;

View File

@ -71,7 +71,7 @@ export class ContentContainer
if ( if (
!data && !data &&
event.shiftKey && event.shiftKey &&
this.group.location !== 'floating' this.group.location.type !== '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.location !== 'floating' this.group.api.location.type !== 'floating'
) { ) {
event.preventDefault(); event.preventDefault();
@ -350,7 +350,8 @@ export class TabsContainer
!this.accessor.options.disableFloatingGroups; !this.accessor.options.disableFloatingGroups;
const isFloatingWithOnePanel = const isFloatingWithOnePanel =
this.group.api.location === 'floating' && this.size === 1; this.group.api.location.type === 'floating' &&
this.size === 1;
if ( if (
isFloatingGroupsEnabled && isFloatingGroupsEnabled &&

View File

@ -12,7 +12,7 @@ import {
} from '../dnd/droptarget'; } 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, Disposable } from '../lifecycle'; import { CompositeDisposable, Disposable, 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 {
@ -58,7 +58,6 @@ import {
TabDragEvent, TabDragEvent,
} from './components/titlebar/tabsContainer'; } from './components/titlebar/tabsContainer';
import { Box } from '../types'; import { Box } from '../types';
import { DockviewPopoutGroupPanel } from './dockviewPopoutGroupPanel';
import { import {
DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE,
DEFAULT_FLOATING_GROUP_POSITION, DEFAULT_FLOATING_GROUP_POSITION,
@ -67,13 +66,14 @@ import {
DockviewPanelRenderer, DockviewPanelRenderer,
OverlayRenderContainer, OverlayRenderContainer,
} from '../overlayRenderContainer'; } from '../overlayRenderContainer';
import { PopoutWindow } from '../popoutWindow';
const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = { const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = {
activationSize: { type: 'pixels', value: 10 }, activationSize: { type: 'pixels', value: 10 },
size: { type: 'pixels', value: 20 }, size: { type: 'pixels', value: 20 },
}; };
function getTheme(element: HTMLElement): string | undefined { function getDockviewTheme(element: HTMLElement): string | undefined {
function toClassList(element: HTMLElement) { function toClassList(element: HTMLElement) {
const list: string[] = []; const list: string[] = [];
@ -286,8 +286,10 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
options?: { options?: {
position?: Box; position?: Box;
popoutUrl?: string; popoutUrl?: string;
onDidOpen?: (event: { id: string; window: Window }) => void;
onWillClose?: (event: { id: string; window: Window }) => void;
} }
): void; ): Promise<void>;
} }
export class DockviewComponent export class DockviewComponent
@ -329,7 +331,12 @@ export class DockviewComponent
this._onDidActivePanelChange.event; this._onDidActivePanelChange.event;
private readonly _floatingGroups: DockviewFloatingGroupPanel[] = []; private readonly _floatingGroups: DockviewFloatingGroupPanel[] = [];
private readonly _popoutGroups: DockviewPopoutGroupPanel[] = []; private readonly _popoutGroups: {
window: PopoutWindow;
popoutGroup: DockviewGroupPanel;
referenceGroup: DockviewGroupPanel;
disposable: IDisposable;
}[] = [];
private readonly _rootDropTarget: Droptarget; private readonly _rootDropTarget: Droptarget;
get orientation(): Orientation { get orientation(): Orientation {
@ -410,7 +417,7 @@ export class DockviewComponent
// iterate over a copy of the array since .dispose() mutates the original array // iterate over a copy of the array since .dispose() mutates the original array
for (const group of [...this._popoutGroups]) { for (const group of [...this._popoutGroups]) {
group.dispose(); group.disposable.dispose();
} }
}) })
); );
@ -513,71 +520,144 @@ export class DockviewComponent
skipRemoveGroup?: boolean; skipRemoveGroup?: boolean;
position?: Box; position?: Box;
popoutUrl?: string; popoutUrl?: string;
onDidOpen?: (event: { id: string; window: Window }) => void;
onWillClose?: (event: { id: string; window: Window }) => void;
}
): Promise<void> {
if (item instanceof DockviewPanel && item.group.size === 1) {
return this.addPopoutGroup(item.group);
} }
): void {
let group: DockviewGroupPanel;
let box: Box | undefined = options?.position;
if (item instanceof DockviewPanel) { const theme = getDockviewTheme(this.gridview.element);
group = this.createGroup(); const element = this.element;
this.removePanel(item, { function moveGroupWithoutDestroying(options: {
removeEmptyGroup: true, from: DockviewGroupPanel;
skipDispose: true, to: DockviewGroupPanel;
}) {
const panels = [...options.from.panels].map((panel) =>
options.from.model.removePanel(panel)
);
panels.forEach((panel) => {
options.to.model.openPanel(panel);
}); });
group.model.openPanel(item);
if (!box) {
box = this.element.getBoundingClientRect();
}
} else {
group = item;
if (!box) {
box = group.element.getBoundingClientRect();
} }
const skip = function getBox(): Box {
typeof options?.skipRemoveGroup === 'boolean' && if (options?.position) {
options.skipRemoveGroup; return options.position;
if (!skip) {
this.doRemoveGroup(item, { skipDispose: true });
}
} }
const theme = getTheme(this.gridview.element); if (item instanceof DockviewGroupPanel) {
return item.element.getBoundingClientRect();
}
const popoutWindow = new DockviewPopoutGroupPanel( if (item.group) {
`${this.id}-${group.id}`, // globally unique within dockview return item.group.element.getBoundingClientRect();
group, }
return element.getBoundingClientRect();
}
const box: Box = getBox();
const groupId = this.getNextGroupId(); //item.id;
item.api.setHidden(true);
const _window = new PopoutWindow(
`${this.id}-${groupId}`, // unique id
theme ?? '',
{ {
className: theme ?? '', url: options?.popoutUrl ?? '/popout.html',
popoutUrl: options?.popoutUrl ?? '/popout.html',
box: {
left: window.screenX + box.left, left: window.screenX + box.left,
top: window.screenY + box.top, top: window.screenY + box.top,
width: box.width, width: box.width,
height: box.height, height: box.height,
}, onDidOpen: options?.onDidOpen,
onWillClose: options?.onWillClose,
} }
); );
popoutWindow.addDisposables( const popoutWindowDisposable = new CompositeDisposable(
{ _window,
dispose: () => { _window.onDidClose(() => {
remove(this._popoutGroups, popoutWindow); popoutWindowDisposable.dispose();
this.updateWatermark();
},
},
popoutWindow.window.onDidClose(() => {
this.doAddGroup(group, [0]);
}) })
); );
this._popoutGroups.push(popoutWindow); return _window
.open()
.then((popoutContainer) => {
if (_window.isDisposed) {
return;
}
if (popoutContainer === null) {
popoutWindowDisposable.dispose();
return;
}
const referenceGroup =
item instanceof DockviewPanel ? item.group : item;
const group = this.createGroup({ id: groupId });
if (item instanceof DockviewPanel) {
const panel = referenceGroup.model.removePanel(item);
group.model.openPanel(panel);
} else {
moveGroupWithoutDestroying({
from: referenceGroup,
to: group,
});
referenceGroup.api.setHidden(false);
}
popoutContainer.appendChild(group.element);
group.model.location = {
type: 'popout',
getWindow: () => _window.window!,
};
const value = {
window: _window,
popoutGroup: group,
referenceGroup,
disposable: popoutWindowDisposable,
};
popoutWindowDisposable.addDisposables(
Disposable.from(() => {
if (this.getPanel(referenceGroup.id)) {
moveGroupWithoutDestroying({
from: group,
to: referenceGroup,
});
if (referenceGroup.api.isHidden) {
referenceGroup.api.setHidden(false);
}
this.doRemoveGroup(group);
} else {
const removedGroup = this.doRemoveGroup(group, {
skipDispose: true,
skipActive: true,
});
removedGroup.model.location = { type: 'grid' };
this.doAddGroup(removedGroup, [0]);
}
})
);
this._popoutGroups.push(value);
this.updateWatermark(); this.updateWatermark();
})
.catch((err) => {
console.error(err);
});
} }
addFloatingGroup( addFloatingGroup(
@ -608,7 +688,7 @@ export class DockviewComponent
} }
} }
group.model.location = 'floating'; group.model.location = { type: 'floating' };
const overlayLeft = const overlayLeft =
typeof coord?.x === 'number' typeof coord?.x === 'number'
@ -683,7 +763,7 @@ export class DockviewComponent
dispose: () => { dispose: () => {
disposable.dispose(); disposable.dispose();
group.model.location = 'grid'; group.model.location = { type: 'grid' };
remove(this._floatingGroups, floatingGroupPanel); remove(this._floatingGroups, floatingGroupPanel);
this.updateWatermark(); this.updateWatermark();
}, },
@ -876,7 +956,7 @@ export class DockviewComponent
const popoutGroups: SerializedPopoutGroup[] = this._popoutGroups.map( const popoutGroups: SerializedPopoutGroup[] = this._popoutGroups.map(
(group) => { (group) => {
return { return {
data: group.group.toJSON() as GroupPanelViewState, data: group.popoutGroup.toJSON() as GroupPanelViewState,
position: group.window.dimensions(), position: group.window.dimensions(),
}; };
} }
@ -1173,7 +1253,7 @@ export class DockviewComponent
group.model.openPanel(panel); group.model.openPanel(panel);
this.doSetGroupAndPanelActive(group); this.doSetGroupAndPanelActive(group);
} else if ( } else if (
referenceGroup.api.location === 'floating' || referenceGroup.api.location.type === 'floating' ||
target === 'center' target === 'center'
) { ) {
panel = this.createPanel(options, referenceGroup); panel = this.createPanel(options, referenceGroup);
@ -1259,7 +1339,11 @@ export class DockviewComponent
} }
private updateWatermark(): void { private updateWatermark(): void {
if (this.groups.filter((x) => x.api.location === 'grid').length === 0) { if (
this.groups.filter(
(x) => x.api.location.type === 'grid' && !x.api.isHidden
).length === 0
) {
if (!this.watermark) { if (!this.watermark) {
this.watermark = this.createWatermarkComponent(); this.watermark = this.createWatermarkComponent();
@ -1377,7 +1461,7 @@ export class DockviewComponent
} }
| undefined | undefined
): DockviewGroupPanel { ): DockviewGroupPanel {
if (group.api.location === 'floating') { if (group.api.location.type === 'floating') {
const floatingGroup = this._floatingGroups.find( const floatingGroup = this._floatingGroups.find(
(_) => _.group === group (_) => _.group === group
); );
@ -1406,19 +1490,21 @@ export class DockviewComponent
throw new Error('failed to find floating group'); throw new Error('failed to find floating group');
} }
if (group.api.location === 'popout') { if (group.api.location.type === 'popout') {
const selectedGroup = this._popoutGroups.find( const selectedGroup = this._popoutGroups.find(
(_) => _.group === group (_) => _.popoutGroup === group
); );
if (selectedGroup) { if (selectedGroup) {
if (!options?.skipDispose) { if (!options?.skipDispose) {
selectedGroup.group.dispose(); this.doRemoveGroup(selectedGroup.referenceGroup);
selectedGroup.popoutGroup.dispose();
this._groups.delete(group.id); this._groups.delete(group.id);
this._onDidRemoveGroup.fire(group); this._onDidRemoveGroup.fire(group);
} }
selectedGroup.dispose(); selectedGroup.disposable.dispose();
if (!options?.skipActive && this._activeGroup === group) { if (!options?.skipActive && this._activeGroup === group) {
const groups = Array.from(this._groups.values()); const groups = Array.from(this._groups.values());
@ -1428,7 +1514,8 @@ export class DockviewComponent
); );
} }
return selectedGroup.group; this.updateWatermark();
return selectedGroup.popoutGroup;
} }
throw new Error('failed to find popout group'); throw new Error('failed to find popout group');
@ -1486,7 +1573,7 @@ export class DockviewComponent
if (sourceGroup && sourceGroup.size < 2) { if (sourceGroup && sourceGroup.size < 2) {
const [targetParentLocation, to] = tail(targetLocation); const [targetParentLocation, to] = tail(targetLocation);
if (sourceGroup.api.location === 'grid') { if (sourceGroup.api.location.type === 'grid') {
const sourceLocation = getGridLocation(sourceGroup.element); const sourceLocation = getGridLocation(sourceGroup.element);
const [sourceParentLocation, from] = tail(sourceLocation); const [sourceParentLocation, from] = tail(sourceLocation);
@ -1562,7 +1649,7 @@ export class DockviewComponent
}); });
} }
} else { } else {
switch (sourceGroup.api.location) { switch (sourceGroup.api.location.type) {
case 'grid': case 'grid':
this.gridview.removeView( this.gridview.removeView(
getGridLocation(sourceGroup.element) getGridLocation(sourceGroup.element)
@ -1580,12 +1667,12 @@ export class DockviewComponent
} }
case 'popout': { case 'popout': {
const selectedPopoutGroup = this._popoutGroups.find( const selectedPopoutGroup = this._popoutGroups.find(
(x) => x.group === sourceGroup (x) => x.popoutGroup === sourceGroup
); );
if (!selectedPopoutGroup) { if (!selectedPopoutGroup) {
throw new Error('failed to find popout group'); throw new Error('failed to find popout group');
} }
selectedPopoutGroup.dispose(); selectedPopoutGroup.disposable.dispose();
} }
} }
@ -1619,6 +1706,15 @@ export class DockviewComponent
} }
} }
private getNextGroupId(): string {
let id = this.nextGroupId.next();
while (this._groups.has(id)) {
id = this.nextGroupId.next();
}
return id;
}
createGroup(options?: GroupOptions): DockviewGroupPanel { createGroup(options?: GroupOptions): DockviewGroupPanel {
if (!options) { if (!options) {
options = {}; options = {};
@ -1641,7 +1737,7 @@ export class DockviewComponent
} }
const view = new DockviewGroupPanel(this, id, options); const view = new DockviewGroupPanel(this, id, options);
view.init({ params: {}, accessor: <any>null }); // required to initialized .part and allow for correct disposal of group view.init({ params: {}, accessor: this });
if (!this._groups.has(view.id)) { if (!this._groups.has(view.id)) {
const disposable = new CompositeDisposable( const disposable = new CompositeDisposable(
@ -1676,8 +1772,7 @@ export class DockviewComponent
this._groups.set(view.id, { value: view, disposable }); this._groups.set(view.id, { value: view, disposable });
} }
// TODO: must be called after the above listeners have been setup, // TODO: must be called after the above listeners have been setup, not an ideal pattern
// not an ideal pattern
view.initialize(); view.initialize();
return view; return view;

View File

@ -130,7 +130,10 @@ export interface IDockviewGroupPanelModel extends IPanel {
): boolean; ): boolean;
} }
export type DockviewGroupLocation = 'grid' | 'floating' | 'popout'; export type DockviewGroupLocation =
| { type: 'grid' }
| { type: 'floating' }
| { type: 'popout'; getWindow: () => Window };
export class DockviewGroupPanelModel export class DockviewGroupPanelModel
extends CompositeDisposable extends CompositeDisposable
@ -146,7 +149,7 @@ export class DockviewGroupPanelModel
private _leftHeaderActions: IHeaderActionsRenderer | undefined; private _leftHeaderActions: IHeaderActionsRenderer | undefined;
private _prefixHeaderActions: IHeaderActionsRenderer | undefined; private _prefixHeaderActions: IHeaderActionsRenderer | undefined;
private _location: DockviewGroupLocation = 'grid'; private _location: DockviewGroupLocation = { type: 'grid' };
private mostRecentlyUsed: IDockviewPanel[] = []; private mostRecentlyUsed: IDockviewPanel[] = [];
@ -253,7 +256,7 @@ export class DockviewGroupPanelModel
toggleClass(this.container, 'dv-groupview-floating', false); toggleClass(this.container, 'dv-groupview-floating', false);
toggleClass(this.container, 'dv-groupview-popout', false); toggleClass(this.container, 'dv-groupview-popout', false);
switch (value) { switch (value.type) {
case 'grid': case 'grid':
this.contentContainer.dropTarget.setTargetZones([ this.contentContainer.dropTarget.setTargetZones([
'top', 'top',
@ -835,6 +838,7 @@ export class DockviewGroupPanelModel
this.watermark?.element.remove(); this.watermark?.element.remove();
this.watermark?.dispose?.(); this.watermark?.dispose?.();
this.watermark = undefined;
for (const panel of this.panels) { for (const panel of this.panels) {
panel.dispose(); panel.dispose();

View File

@ -1,44 +0,0 @@
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 id: string,
readonly group: DockviewGroupPanel,
private readonly options: {
className: string;
popoutUrl: string;
box: Box;
}
) {
super();
this.window = new PopoutWindow(id, 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

@ -273,7 +273,9 @@ export class Gridview implements IDisposable {
readonly element: HTMLElement; readonly element: HTMLElement;
private _root: BranchNode | undefined; private _root: BranchNode | undefined;
private _maximizedNode: LeafNode | undefined = undefined; private _maximizedNode:
| { leaf: LeafNode; hiddenOnMaximize: LeafNode[] }
| undefined = undefined;
private readonly disposable: MutableDisposable = new MutableDisposable(); private readonly disposable: MutableDisposable = new MutableDisposable();
private readonly _onDidChange = new Emitter<{ private readonly _onDidChange = new Emitter<{
@ -329,7 +331,7 @@ export class Gridview implements IDisposable {
} }
maximizedView(): IGridView | undefined { maximizedView(): IGridView | undefined {
return this._maximizedNode?.view; return this._maximizedNode?.leaf.view;
} }
hasMaximizedView(): boolean { hasMaximizedView(): boolean {
@ -344,7 +346,7 @@ export class Gridview implements IDisposable {
return; return;
} }
if (this._maximizedNode === node) { if (this._maximizedNode?.leaf === node) {
return; return;
} }
@ -352,12 +354,18 @@ export class Gridview implements IDisposable {
this.exitMaximizedView(); this.exitMaximizedView();
} }
const hiddenOnMaximize: LeafNode[] = [];
function hideAllViewsBut(parent: BranchNode, exclude: LeafNode): void { function hideAllViewsBut(parent: BranchNode, exclude: LeafNode): void {
for (let i = 0; i < parent.children.length; i++) { for (let i = 0; i < parent.children.length; i++) {
const child = parent.children[i]; const child = parent.children[i];
if (child instanceof LeafNode) { if (child instanceof LeafNode) {
if (child !== exclude) { if (child !== exclude) {
if (parent.isChildVisible(i)) {
parent.setChildVisible(i, false); parent.setChildVisible(i, false);
} else {
hiddenOnMaximize.push(child);
}
} }
} else { } else {
hideAllViewsBut(child, exclude); hideAllViewsBut(child, exclude);
@ -366,7 +374,7 @@ export class Gridview implements IDisposable {
} }
hideAllViewsBut(this.root, node); hideAllViewsBut(this.root, node);
this._maximizedNode = node; this._maximizedNode = { leaf: node, hiddenOnMaximize };
this._onDidMaxmizedNodeChange.fire(); this._onDidMaxmizedNodeChange.fire();
} }
@ -375,11 +383,15 @@ export class Gridview implements IDisposable {
return; return;
} }
const hiddenOnMaximize = this._maximizedNode.hiddenOnMaximize;
function showViewsInReverseOrder(parent: BranchNode): void { function showViewsInReverseOrder(parent: BranchNode): void {
for (let index = parent.children.length - 1; index >= 0; index--) { for (let index = parent.children.length - 1; index >= 0; index--) {
const child = parent.children[index]; const child = parent.children[index];
if (child instanceof LeafNode) { if (child instanceof LeafNode) {
if (!hiddenOnMaximize.includes(child)) {
parent.setChildVisible(index, true); parent.setChildVisible(index, true);
}
} else { } else {
showViewsInReverseOrder(child); showViewsInReverseOrder(child);
} }
@ -395,8 +407,8 @@ export class Gridview implements IDisposable {
public serialize(): SerializedGridview<any> { public serialize(): SerializedGridview<any> {
if (this.hasMaximizedView()) { if (this.hasMaximizedView()) {
/** /**
* do not persist maximized view state but we must first exit any maximized views * do not persist maximized view state
* before serialization to ensure the correct dimensions are persisted * firstly exit any maximized views to ensure the correct dimensions are persisted
*/ */
this.exitMaximizedView(); this.exitMaximizedView();
} }

View File

@ -16,6 +16,7 @@ import {
import { LayoutPriority } from '../splitview/splitview'; import { LayoutPriority } from '../splitview/splitview';
import { Emitter, Event } from '../events'; import { Emitter, Event } from '../events';
import { IViewSize } from './gridview'; import { IViewSize } from './gridview';
import { BaseGrid, IGridPanelView } from './baseComponentGridview';
export interface GridviewInitParameters extends PanelInitParameters { export interface GridviewInitParameters extends PanelInitParameters {
minimumWidth?: number; minimumWidth?: number;
@ -24,7 +25,7 @@ export interface GridviewInitParameters extends PanelInitParameters {
maximumHeight?: number; maximumHeight?: number;
priority?: LayoutPriority; priority?: LayoutPriority;
snap?: boolean; snap?: boolean;
accessor: GridviewComponent; accessor: BaseGrid<IGridPanelView>;
isVisible?: boolean; isVisible?: boolean;
} }
@ -157,14 +158,16 @@ export abstract class GridviewPanel<
this.api.initialize(this); // TODO: required to by-pass 'super before this' requirement this.api.initialize(this); // TODO: required to by-pass 'super before this' requirement
this.addDisposables( this.addDisposables(
this.api.onVisibilityChange((event) => { this.api.onDidHiddenChange((event) => {
const { isVisible } = event; const { isHidden } = event;
const { accessor } = this._params as GridviewInitParameters; const { accessor } = this._params as GridviewInitParameters;
accessor.setVisible(this, isVisible);
accessor.setVisible(this, !isHidden);
}), }),
this.api.onActiveChange(() => { this.api.onActiveChange(() => {
const { accessor } = this._params as GridviewInitParameters; const { accessor } = this._params as GridviewInitParameters;
accessor.setActive(this);
accessor.doSetGroupActive(this);
}), }),
this.api.onDidConstraintsChangeInternal((event) => { this.api.onDidConstraintsChangeInternal((event) => {
if ( if (

View File

@ -24,10 +24,10 @@ export namespace Disposable {
} }
export class CompositeDisposable { export class CompositeDisposable {
private readonly _disposables: IDisposable[]; private _disposables: IDisposable[];
private _isDisposed = false; private _isDisposed = false;
protected get isDisposed(): boolean { get isDisposed(): boolean {
return this._isDisposed; return this._isDisposed;
} }
@ -40,9 +40,13 @@ export class CompositeDisposable {
} }
public dispose(): void { public dispose(): void {
this._disposables.forEach((arg) => arg.dispose()); if (this._isDisposed) {
return;
}
this._isDisposed = true; this._isDisposed = true;
this._disposables.forEach((arg) => arg.dispose());
this._disposables = [];
} }
} }

View File

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

View File

@ -5,22 +5,31 @@ import { Box } from './types';
export type PopoutWindowOptions = { export type PopoutWindowOptions = {
url: string; url: string;
onDidOpen?: (event: { id: string; window: Window }) => void;
onWillClose?: (event: { id: string; window: Window }) => void;
} & Box; } & Box;
export class PopoutWindow extends CompositeDisposable { export class PopoutWindow extends CompositeDisposable {
private readonly _onWillClose = new Emitter<void>();
readonly onWillClose = this._onWillClose.event;
private readonly _onDidClose = new Emitter<void>(); private readonly _onDidClose = new Emitter<void>();
readonly onDidClose = this._onDidClose.event; readonly onDidClose = this._onDidClose.event;
private _window: { value: Window; disposable: IDisposable } | null = null; private _window: { value: Window; disposable: IDisposable } | null = null;
get window(): Window | null {
return this._window?.value ?? null;
}
constructor( constructor(
private readonly id: string, private readonly target: string,
private readonly className: string, private readonly className: string,
private readonly options: PopoutWindowOptions private readonly options: PopoutWindowOptions
) { ) {
super(); super();
this.addDisposables(this._onDidClose, { this.addDisposables(this._onWillClose, this._onDidClose, {
dispose: () => { dispose: () => {
this.close(); this.close();
}, },
@ -42,13 +51,22 @@ export class PopoutWindow extends CompositeDisposable {
close(): void { close(): void {
if (this._window) { if (this._window) {
this._onWillClose.fire();
this.options.onWillClose?.({
id: this.target,
window: this._window.value,
});
this._window.disposable.dispose(); this._window.disposable.dispose();
this._window.value.close(); this._window.value.close();
this._window = null; this._window = null;
this._onDidClose.fire();
} }
} }
open(content: HTMLElement): void { async open(): Promise<HTMLElement | null> {
if (this._window) { if (this._window) {
throw new Error('instance of popout window is already open'); throw new Error('instance of popout window is already open');
} }
@ -64,55 +82,93 @@ export class PopoutWindow extends CompositeDisposable {
.map(([key, value]) => `${key}=${value}`) .map(([key, value]) => `${key}=${value}`)
.join(','); .join(',');
// https://developer.mozilla.org/en-US/docs/Web/API/Window/open /**
const externalWindow = window.open(url, this.id, features); * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/open
*/
const externalWindow = window.open(url, this.target, features);
if (!externalWindow) { if (!externalWindow) {
return; /**
* Popup blocked
*/
return null;
} }
const disposable = new CompositeDisposable(); const disposable = new CompositeDisposable();
this._window = { value: externalWindow, disposable }; this._window = { value: externalWindow, disposable };
const cleanUp = () => {
this._onDidClose.fire();
this._window = null;
};
// prevent any default content from loading
// externalWindow.document.body.replaceWith(document.createElement('div'));
disposable.addDisposables( disposable.addDisposables(
addDisposableWindowListener(window, 'beforeunload', () => { addDisposableWindowListener(window, 'beforeunload', () => {
cleanUp(); /**
* before the main window closes we should close this popup too
* to be good citizens
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
*/
this.close(); this.close();
}) })
); );
const container = this.createPopoutWindowContainer();
if (this.className) {
container.classList.add(this.className);
}
this.options.onDidOpen?.({
id: this.target,
window: externalWindow,
});
return new Promise<HTMLElement | null>((resolve) => {
externalWindow.addEventListener('unload', (e) => {
// if page fails to load before unloading
// this.close();
});
externalWindow.addEventListener('load', () => { externalWindow.addEventListener('load', () => {
/**
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
*/
const externalDocument = externalWindow.document; const externalDocument = externalWindow.document;
externalDocument.title = document.title; externalDocument.title = document.title;
const div = document.createElement('div'); externalDocument.body.appendChild(container);
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); addStyles(externalDocument, window.document.styleSheets);
externalWindow.addEventListener('beforeunload', () => { /**
// TODO: indicate external window is closing * beforeunload must be registered after load for reasons I could not determine
cleanUp(); * otherwise the beforeunload event will not fire when the window is closed
*/
addDisposableWindowListener(
externalWindow,
'beforeunload',
() => {
/**
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
*/
this.close();
}
);
resolve(container);
}); });
}); });
} }
private createPopoutWindowContainer(): HTMLElement {
const el = document.createElement('div');
el.classList.add('dv-popout-window');
el.id = 'dv-popout-window';
el.style.position = 'absolute';
el.style.width = '100%';
el.style.height = '100%';
el.style.top = '0px';
el.style.left = '0px';
return el;
}
} }

View File

@ -89,10 +89,10 @@ export abstract class SplitviewPanel
this.addDisposables( this.addDisposables(
this._onDidChange, this._onDidChange,
this.api.onVisibilityChange((event) => { this.api.onDidHiddenChange((event) => {
const { isVisible } = event; const { isHidden } = event;
const { accessor } = this._params as PanelViewInitParameters; const { accessor } = this._params as PanelViewInitParameters;
accessor.setVisible(this, isVisible); accessor.setVisible(this, !isHidden);
}), }),
this.api.onActiveChange(() => { this.api.onActiveChange(() => {
const { accessor } = this._params as PanelViewInitParameters; const { accessor } = this._params as PanelViewInitParameters;

View File

@ -3,6 +3,7 @@ import {
GridviewPanel, GridviewPanel,
GridviewInitParameters, GridviewInitParameters,
IFrameworkPart, IFrameworkPart,
GridviewComponent,
} from 'dockview-core'; } from 'dockview-core';
import { ReactPart, ReactPortalStore } from '../react'; import { ReactPart, ReactPortalStore } from '../react';
import { IGridviewPanelProps } from './gridview'; import { IGridviewPanelProps } from './gridview';
@ -25,8 +26,10 @@ export class ReactGridPanelView extends GridviewPanel {
{ {
params: this._params?.params ?? {}, params: this._params?.params ?? {},
api: this.api, api: this.api,
// TODO: fix casting hack
containerApi: new GridviewApi( containerApi: new GridviewApi(
(this._params as GridviewInitParameters).accessor (this._params as GridviewInitParameters)
.accessor as GridviewComponent
), ),
} }
); );

View File

@ -34,6 +34,7 @@
"dockview": "^1.9.2", "dockview": "^1.9.2",
"prism-react-renderer": "^2.3.1", "prism-react-renderer": "^2.3.1",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-laag": "^2.0.5",
"recoil": "^0.7.7", "recoil": "^0.7.7",
"source-map-loader": "^4.0.2", "source-map-loader": "^4.0.2",
"uuid": "^9.0.1" "uuid": "^9.0.1"

View File

@ -87,7 +87,7 @@ const RightControls = (props: IDockviewHeaderActionsProps) => {
); );
const [isPopout, setIsPopout] = React.useState<boolean>( const [isPopout, setIsPopout] = React.useState<boolean>(
props.api.location === 'popout' props.api.location.type === 'popout'
); );
React.useEffect(() => { React.useEffect(() => {
@ -96,7 +96,7 @@ const RightControls = (props: IDockviewHeaderActionsProps) => {
}); });
const disposable2 = props.api.onDidLocationChange(() => { const disposable2 = props.api.onDidLocationChange(() => {
setIsPopout(props.api.location === 'popout'); setIsPopout(props.api.location.type === 'popout');
}); });
return () => { return () => {

View File

@ -255,13 +255,13 @@ 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.location === 'floating' props.api.location.type === 'floating'
); );
React.useEffect(() => { React.useEffect(() => {
const disposable = props.group.api.onDidLocationChange( const disposable = props.group.api.onDidLocationChange(
(event) => { (event) => {
setFloating(event.location === 'floating'); setFloating(event.location.type === 'floating');
} }
); );

View File

@ -9,7 +9,8 @@
"dependencies": { "dependencies": {
"dockview": "*", "dockview": "*",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0",
"react-laag": "^2.0.5"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.0.28", "@types/react": "^18.0.28",

View File

@ -5,12 +5,34 @@ import {
IDockviewHeaderActionsProps, IDockviewHeaderActionsProps,
IDockviewPanelProps, IDockviewPanelProps,
SerializedDockview, SerializedDockview,
DockviewPanelApi,
} from 'dockview'; } from 'dockview';
import * as React from 'react'; import * as React from 'react';
import { Icon } from './utils'; import { Icon } from './utils';
import { PopoverMenu } from './popover';
function usePanelWindowObject(api: DockviewPanelApi): Window {
const [document, setDocument] = React.useState<Window>(api.getWindow());
React.useEffect(() => {
const disposable = api.onDidLocationChange((event) => {
setDocument(api.getWindow());
});
return () => {
disposable.dispose();
};
}, [api]);
return document;
}
const components = { const components = {
default: (props: IDockviewPanelProps<{ title: string }>) => { default: (props: IDockviewPanelProps<{ title: string }>) => {
const _window = usePanelWindowObject(props.api);
const [reset, setReset] = React.useState<boolean>(false);
return ( return (
<div <div
style={{ style={{
@ -19,7 +41,19 @@ const components = {
background: 'var(--dv-group-view-background-color)', background: 'var(--dv-group-view-background-color)',
}} }}
> >
{props.params.title} <button
onClick={() => {
console.log(_window);
setReset(true);
setTimeout(() => {
setReset(false);
}, 2000);
}}
>
Print
</button>
{!reset && <PopoverMenu window={_window} />}
{props.api.title}
</div> </div>
); );
}, },
@ -31,31 +65,31 @@ function loadDefaultLayout(api: DockviewApi) {
component: 'default', component: 'default',
}); });
api.addPanel({ // api.addPanel({
id: 'panel_2', // id: 'panel_2',
component: 'default', // component: 'default',
}); // });
api.addPanel({ // api.addPanel({
id: 'panel_3', // id: 'panel_3',
component: 'default', // component: 'default',
}); // });
api.addPanel({ // api.addPanel({
id: 'panel_4', // id: 'panel_4',
component: 'default', // component: 'default',
}); // });
api.addPanel({ // api.addPanel({
id: 'panel_5', // id: 'panel_5',
component: 'default', // component: 'default',
position: { direction: 'right' }, // position: { direction: 'right' },
}); // });
api.addPanel({ // api.addPanel({
id: 'panel_6', // id: 'panel_6',
component: 'default', // component: 'default',
}); // });
} }
let panelCount = 0; let panelCount = 0;
@ -205,12 +239,12 @@ const LeftComponent = (props: IDockviewHeaderActionsProps) => {
const RightComponent = (props: IDockviewHeaderActionsProps) => { const RightComponent = (props: IDockviewHeaderActionsProps) => {
const [popout, setPopout] = React.useState<boolean>( const [popout, setPopout] = React.useState<boolean>(
props.api.location === 'popout' props.api.location.type === 'popout'
); );
React.useEffect(() => { React.useEffect(() => {
const disposable = props.group.api.onDidLocationChange((event) => [ const disposable = props.group.api.onDidLocationChange((event) => [
setPopout(event.location === 'popout'), setPopout(event.location.type === 'popout'),
]); ]);
return () => { return () => {
@ -223,7 +257,7 @@ const RightComponent = (props: IDockviewHeaderActionsProps) => {
const group = props.containerApi.addGroup(); const group = props.containerApi.addGroup();
props.group.api.moveTo({ group }); props.group.api.moveTo({ group });
} else { } else {
props.containerApi.addPopoutGroup(props.group, { const window = props.containerApi.addPopoutGroup(props.group, {
popoutUrl: '/popout/index.html', popoutUrl: '/popout/index.html',
}); });
} }

View File

@ -0,0 +1,55 @@
import { useLayer, Arrow } from 'react-laag';
import { motion, AnimatePresence } from 'framer-motion';
import * as React from 'react';
import { DockviewPanelApi } from 'dockview';
export function PopoverMenu(props: { window: Window }) {
const [isOpen, setOpen] = React.useState(false);
// helper function to close the menu
function close() {
setOpen(false);
}
const { renderLayer, triggerProps, layerProps, arrowProps } = useLayer({
isOpen,
onOutsideClick: close, // close the menu when the user clicks outside
onDisappear: close, // close the menu when the menu gets scrolled out of sight
overflowContainer: false, // keep the menu positioned inside the container
auto: true, // automatically find the best placement
placement: 'top-end', // we prefer to place the menu "top-end"
triggerOffset: 12, // keep some distance to the trigger
containerOffset: 16, // give the menu some room to breath relative to the container
arrowOffset: 16, // let the arrow have some room to breath also,
environment: props.window,
container: props.window
? () => {
const el = props.window.document.body;
Object.setPrototypeOf(el, HTMLElement.prototype);
return el;
}
: undefined,
});
// Again, we're using framer-motion for the transition effect
return (
<>
<button {...triggerProps} onClick={() => setOpen(!isOpen)}>
{isOpen ? 'Hide' : 'Show'}
</button>
{renderLayer(
<AnimatePresence>
{isOpen && (
<motion.ul {...layerProps}>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
<Arrow {...arrowProps} />
</motion.ul>
)}
</AnimatePresence>
)}
</>
);
}

View File

@ -13882,6 +13882,13 @@ react-json-view-lite@^1.2.0:
resolved "https://registry.yarnpkg.com/react-json-view-lite/-/react-json-view-lite-1.2.1.tgz#c59a0bea4ede394db331d482ee02e293d38f8218" resolved "https://registry.yarnpkg.com/react-json-view-lite/-/react-json-view-lite-1.2.1.tgz#c59a0bea4ede394db331d482ee02e293d38f8218"
integrity sha512-Itc0g86fytOmKZoIoJyGgvNqohWSbh3NXIKNgH6W6FT9PC1ck4xas1tT3Rr/b3UlFXyA9Jjaw9QSXdZy2JwGMQ== integrity sha512-Itc0g86fytOmKZoIoJyGgvNqohWSbh3NXIKNgH6W6FT9PC1ck4xas1tT3Rr/b3UlFXyA9Jjaw9QSXdZy2JwGMQ==
react-laag@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/react-laag/-/react-laag-2.0.5.tgz#549f1035b761b9ba09ac98fd128ccad63464c877"
integrity sha512-RCvublJhdcgGRHU1wMYJ8kRtnYsKUgYusLvVhMuftg65POnnOB4+fwXvnETm6adc0cMnc1spujlrK6bGIz6aug==
dependencies:
tiny-warning "^1.0.3"
react-loadable-ssr-addon-v5-slorber@^1.0.1: react-loadable-ssr-addon-v5-slorber@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz#2cdc91e8a744ffdf9e3556caabeb6e4278689883" resolved "https://registry.yarnpkg.com/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz#2cdc91e8a744ffdf9e3556caabeb6e4278689883"
@ -15678,7 +15685,7 @@ tiny-invariant@^1.0.2:
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642"
integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==
tiny-warning@^1.0.0: tiny-warning@^1.0.0, tiny-warning@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==