Merge branch 'master' of https://github.com/mathuo/dockview into 397-gready-rendering-mode

This commit is contained in:
mathuo 2024-01-03 19:08:37 +00:00
commit 43548618ba
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
47 changed files with 5412 additions and 2212 deletions

View File

@ -1,6 +1,7 @@
name: Deploy Docs name: Deploy Docs
on: on:
workflow_dispatch:
schedule: schedule:
- cron: '0 3 * * *' # every day at 3 am UTC - cron: '0 3 * * *' # every day at 3 am UTC

View File

@ -29,43 +29,46 @@
"version": "lerna version" "version": "lerna version"
}, },
"resolutions": { "resolutions": {
"@types/react": "^18.2.31", "@types/react": "^18.2.46",
"@types/react-dom": "^18.2.14" "@types/react-dom": "^18.2.18"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.5", "@rollup/plugin-typescript": "^11.1.5",
"@testing-library/dom": "^9.3.3", "@testing-library/dom": "^9.3.3",
"@testing-library/jest-dom": "^6.1.4", "@testing-library/jest-dom": "^6.1.6",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.1.2",
"@total-typescript/shoehorn": "^0.1.1", "@total-typescript/shoehorn": "^0.1.1",
"@types/jest": "^29.5.6", "@types/jest": "^29.5.11",
"@types/react": "^18.2.31", "@types/react": "^18.2.46",
"@types/react-dom": "^18.2.14", "@types/react-dom": "^18.2.18",
"@typescript-eslint/eslint-plugin": "^6.8.0", "@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.8.0", "@typescript-eslint/parser": "^6.17.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.52.0", "eslint": "^8.56.0",
"fs-extra": "^11.1.1", "fs-extra": "^11.2.0",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"gulp-dart-sass": "^1.1.0", "gulp-dart-sass": "^1.1.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"jest-sonar-reporter": "^2.0.0", "jest-sonar-reporter": "^2.0.0",
"jsdom": "^22.1.0", "jsdom": "^23.0.1",
"lerna": "^7.4.1", "lerna": "^8.0.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"rollup": "^4.1.4", "rollup": "^4.9.2",
"rollup-plugin-postcss": "^4.0.2", "rollup-plugin-postcss": "^4.0.2",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"ts-loader": "^9.5.0", "ts-loader": "^9.5.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.2",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"typedoc": "^0.25.2", "typedoc": "^0.25.6",
"typescript": "^5.2.2" "typescript": "^5.3.3"
},
"engines": {
"node": ">=18.0"
} }
} }

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

@ -19,13 +19,17 @@ class MockGridview implements IGridView {
>().event; >().event;
element: HTMLElement = document.createElement('div'); element: HTMLElement = document.createElement('div');
width: number = 0;
height: number = 0;
constructor(private id?: string) { constructor(private id?: string) {
this.element.className = 'mock-grid-view'; this.element.className = 'mock-grid-view';
this.element.id = `${id ?? ''}`; this.element.id = `${id ?? ''}`;
} }
layout(width: number, height: number): void { layout(width: number, height: number): void {
// this.width = width;
this.height = height;
} }
toJSON(): object { toJSON(): object {
@ -760,4 +764,255 @@ describe('gridview', () => {
el = gridview.element.querySelectorAll('.mock-grid-view'); el = gridview.element.querySelectorAll('.mock-grid-view');
expect(el.length).toBe(5); expect(el.length).toBe(5);
}); });
test('gridview nested proportional layouts', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
const view4 = new MockGridview('4');
const view5 = new MockGridview('5');
const view6 = new MockGridview('6');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
gridview.addView(view4, Sizing.Distribute, [1, 1, 0]);
gridview.addView(view5, Sizing.Distribute, [1, 1, 0, 0]);
gridview.addView(view6, Sizing.Distribute, [1, 1, 0, 0, 0]);
const views = [view1, view2, view3, view4, view5, view6];
const dimensions = [
{ width: 500, height: 1000 },
{ width: 500, height: 500 },
{ width: 250, height: 500 },
{ width: 250, height: 250 },
{ width: 125, height: 250 },
{ width: 125, height: 250 },
];
expect(
views.map((view) => ({
width: view.width,
height: view.height,
}))
).toEqual(dimensions);
gridview.layout(2000, 1500);
expect(
views.map((view) => ({
width: view.width,
height: view.height,
}))
).toEqual(
dimensions.map(({ width, height }) => ({
width: width * 2,
height: height * 1.5,
}))
);
gridview.layout(200, 2000);
expect(
views.map((view) => ({
width: view.width,
height: view.height,
}))
).toEqual(
dimensions.map(({ width, height }) => ({
width: width * 0.2,
height: height * 2,
}))
);
});
test('that maximizeView retains original dimensions when restored', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
let counter = 0;
const subscription = gridview.onDidMaxmizedNodeChange(() => {
counter++;
});
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
const view4 = new MockGridview('4');
const view5 = new MockGridview('5');
const view6 = new MockGridview('6');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
gridview.addView(view4, Sizing.Distribute, [1, 1, 0]);
gridview.addView(view5, Sizing.Distribute, [1, 1, 0, 0]);
gridview.addView(view6, Sizing.Distribute, [1, 1, 0, 0, 0]);
/**
* _____________________________________________
* | | |
* | | 2 |
* | | |
* | 1 |_______________________|
* | | | 4 |
* | | 3 |_____________|
* | | | 5 | 6 |
* |_____________________|_________|______|______|
*/
const views = [view1, view2, view3, view4, view5, view6];
const dimensions = [
{ width: 500, height: 1000 },
{ width: 500, height: 500 },
{ width: 250, height: 500 },
{ width: 250, height: 250 },
{ width: 125, height: 250 },
{ width: 125, height: 250 },
];
function assertLayout() {
expect(
views.map((view) => ({
width: view.width,
height: view.height,
}))
).toEqual(dimensions);
}
// base case assertions
assertLayout();
expect(gridview.hasMaximizedView()).toBeFalsy();
expect(counter).toBe(0);
/**
* maximize each view individually and then return to the standard view
* checking on each iteration that the original layout dimensions
* are restored
*/
for (let i = 0; i < views.length; i++) {
const view = views[i];
gridview.maximizeView(view);
expect(counter).toBe(i * 2 + 1);
expect(gridview.hasMaximizedView()).toBeTruthy();
gridview.exitMaximizedView();
expect(counter).toBe(i * 2 + 2);
assertLayout();
}
subscription.dispose();
});
test('that maximizedView is exited when a views visibility is changed', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
expect(gridview.hasMaximizedView()).toBeFalsy();
gridview.maximizeView(view2);
expect(gridview.hasMaximizedView()).toBeTruthy();
gridview.setViewVisible([0], true);
expect(gridview.hasMaximizedView()).toBeFalsy();
});
test('that maximizedView is exited when a view is moved', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
const view4 = new MockGridview('4');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
gridview.addView(view4, Sizing.Distribute, [1, 1, 0]);
expect(gridview.hasMaximizedView()).toBeFalsy();
gridview.maximizeView(view2);
expect(gridview.hasMaximizedView()).toBeTruthy();
gridview.moveView([1, 1], 0, 1);
expect(gridview.hasMaximizedView()).toBeFalsy();
});
test('that maximizedView is exited when a view is added', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
const view4 = new MockGridview('4');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
expect(gridview.hasMaximizedView()).toBeFalsy();
gridview.maximizeView(view2);
expect(gridview.hasMaximizedView()).toBeTruthy();
gridview.addView(view4, Sizing.Distribute, [1, 1, 0]);
expect(gridview.hasMaximizedView()).toBeFalsy();
});
test('that maximizedView is exited when a view is removed', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
expect(gridview.hasMaximizedView()).toBeFalsy();
gridview.maximizeView(view2);
expect(gridview.hasMaximizedView()).toBeTruthy();
gridview.removeView([1, 1]);
expect(gridview.hasMaximizedView()).toBeFalsy();
});
}); });

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,33 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
moveToPrevious(options?: MovementOptions): void { moveToPrevious(options?: MovementOptions): void {
this.component.moveToPrevious(options); this.component.moveToPrevious(options);
} }
maximizeGroup(panel: IDockviewPanel): void {
this.component.maximizeGroup(panel.group);
}
hasMaximizedGroup(): boolean {
return this.component.hasMaximizedGroup();
}
exitMaxmizedGroup(): void {
this.component.exitMaximizedGroup();
}
get onDidMaxmizedGroupChange(): Event<void> {
return this.component.onDidMaxmizedGroupChange;
}
/**
* Add a popout group in a new Window
*/
addPopoutGroup(
item: IDockviewPanel | DockviewGroupPanel,
options?: {
position?: Box;
popoutUrl?: string;
}
): void {
this.component.addPopoutGroup(item, options);
}
} }

View File

@ -1,43 +1,50 @@
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;
maximize(): void;
isMaximized(): boolean;
exitMaximized(): void;
} }
export interface DockviewGroupPanelFloatingChangeEvent { export interface DockviewGroupPanelFloatingChangeEvent {
readonly isFloating: boolean; readonly location: DockviewGroupLocation;
} }
// TODO find a better way to initialize and avoid needing null checks
const NOT_INITIALIZED_MESSAGE = 'DockviewGroupPanelApiImpl not initialized';
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(NOT_INITIALIZED_MESSAGE);
} }
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 {
if (!this._group) { if (!this._group) {
throw new Error(`DockviewGroupPanelApiImpl not initialized`); throw new Error(NOT_INITIALIZED_MESSAGE);
} }
this.accessor.moveGroupOrPanel( this.accessor.moveGroupOrPanel(
@ -48,6 +55,32 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
); );
} }
maximize(): void {
if (!this._group) {
throw new Error(NOT_INITIALIZED_MESSAGE);
}
this.accessor.maximizeGroup(this._group);
}
isMaximized(): boolean {
if (!this._group) {
throw new Error(NOT_INITIALIZED_MESSAGE);
}
return this.accessor.isMaximizedGroup(this._group);
}
exitMaximized(): void {
if (!this._group) {
throw new Error(NOT_INITIALIZED_MESSAGE);
}
if (this.isMaximized()) {
this.accessor.exitMaximizedGroup();
}
}
initialize(group: DockviewGroupPanel): void { initialize(group: DockviewGroupPanel): void {
this._group = group; this._group = group;
} }

View File

@ -2,7 +2,7 @@ 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 { MutableDisposable } from '../lifecycle';
import { DockviewPanel, IDockviewPanel } 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';
@ -15,13 +15,10 @@ export interface RendererChangedEvent {
renderer: DockviewPanelRenderer; renderer: DockviewPanelRenderer;
} }
/*
* omit visibility modifiers since the visibility of a single group doesn't make sense
* because it belongs to a groupview
*/
export interface DockviewPanelApi export interface DockviewPanelApi
extends Omit< extends Omit<
GridviewPanelApi, GridviewPanelApi,
// omit properties that do not make sense here
'setVisible' | 'onDidConstraintsChange' | 'setConstraints' 'setVisible' | 'onDidConstraintsChange' | 'setConstraints'
> { > {
readonly group: DockviewGroupPanel; readonly group: DockviewGroupPanel;
@ -39,6 +36,9 @@ export interface DockviewPanelApi
position?: Position; position?: Position;
index?: number; index?: number;
}): void; }): void;
maximize(): void;
isMaximized(): boolean;
exitMaximized(): void;
} }
export class DockviewPanelApiImpl export class DockviewPanelApiImpl
@ -66,7 +66,7 @@ export class DockviewPanelApiImpl
} }
get isGroupActive(): boolean { get isGroupActive(): boolean {
return !!this.group?.isActive; return this.group.isActive;
} }
get renderer(): DockviewPanelRenderer { get renderer(): DockviewPanelRenderer {
@ -140,4 +140,16 @@ export class DockviewPanelApiImpl
close(): void { close(): void {
this.group.model.closePanel(this.panel); this.group.model.closePanel(this.panel);
} }
maximize(): void {
this.group.api.maximize();
}
isMaximized(): boolean {
return this.group.api.isMaximized();
}
exitMaximized(): void {
this.group.api.exitMaximized();
}
} }

View File

@ -0,0 +1,3 @@
export const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100;
export const DEFAULT_FLOATING_GROUP_POSITION = { left: 100, top: 100 };

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

@ -1,6 +1,5 @@
import { import {
CompositeDisposable, CompositeDisposable,
Disposable,
IDisposable, IDisposable,
MutableDisposable, MutableDisposable,
} from '../../../lifecycle'; } from '../../../lifecycle';
@ -8,7 +7,6 @@ import { Emitter, Event } from '../../../events';
import { trackFocus } from '../../../dom'; import { trackFocus } from '../../../dom';
import { IDockviewPanel } from '../../dockviewPanel'; import { IDockviewPanel } from '../../dockviewPanel';
import { DockviewComponent } from '../../dockviewComponent'; import { DockviewComponent } from '../../dockviewComponent';
import { DragAndDropObserver } from '../../../dnd/dnd';
import { Droptarget } from '../../../dnd/droptarget'; import { Droptarget } from '../../../dnd/droptarget';
import { DockviewGroupPanelModel } from '../../dockviewGroupPanelModel'; import { DockviewGroupPanelModel } from '../../dockviewGroupPanelModel';
import { getPanelData } from '../../../dnd/dataTransfer'; import { getPanelData } from '../../../dnd/dataTransfer';
@ -70,7 +68,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

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

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

@ -23,9 +23,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0px 8px; padding: 0px 8px;
// padding: 0px;
// margin: 0px;
// justify-content: flex-end;
.close-action { .close-action {
padding: 4px; padding: 4px;

View File

@ -47,29 +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 { import { Box } from '../types';
OverlayRenderContainer,
DockviewPanelRenderer,
<<<<<<< Updated upstream
} from './components/greadyRenderContainer';
=======
} from '../overlayRenderContainer';
import { DockviewPopoutGroupPanel } from './dockviewPopoutGroupPanel'; import { DockviewPopoutGroupPanel } from './dockviewPopoutGroupPanel';
import { import {
DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE,
DEFAULT_FLOATING_GROUP_POSITION, DEFAULT_FLOATING_GROUP_POSITION,
} from '../constants'; } from '../constants';
>>>>>>> Stashed changes import { DockviewPanelRenderer, OverlayRenderContainer } from '../overlayRenderContainer';
const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100; 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;
}
export interface PanelReference { export interface PanelReference {
update: (event: { params: { [key: string]: any } }) => void; update: (event: { params: { [key: string]: any } }) => void;
@ -78,7 +94,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 {
@ -91,6 +112,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 {
@ -205,7 +227,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;
@ -246,6 +267,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
@ -286,7 +314,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;
@ -454,6 +483,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 },
@ -482,12 +578,16 @@ 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)
: DEFAULT_FLOATING_GROUP_POSITION.left;
const overlayTop = const overlayTop =
typeof coord?.y === 'number' ? Math.max(coord.y, 0) : 100; typeof coord?.y === 'number'
? Math.max(coord.y, 0)
: DEFAULT_FLOATING_GROUP_POSITION.top;
const overlay = new Overlay({ const overlay = new Overlay({
container: this.gridview.element, container: this.gridview.element,
@ -553,14 +653,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();
} }
@ -614,7 +714,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;
@ -647,8 +747,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();
} }
@ -726,11 +826,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(),
}; };
} }
); );
@ -745,6 +854,10 @@ export class DockviewComponent
result.floatingGroups = floats; result.floatingGroups = floats;
} }
if (popoutGroups.length > 0) {
result.popoutGroups = popoutGroups;
}
return result; return result;
} }
@ -850,7 +963,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();
} }
@ -884,7 +1010,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();
} }
@ -1008,7 +1134,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 {
@ -1092,7 +1221,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();
@ -1210,27 +1339,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);
@ -1285,11 +1448,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);
@ -1365,16 +1524,31 @@ 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

@ -68,8 +68,8 @@ export class DockviewGroupPanel
id, id,
'groupview_default', 'groupview_default',
{ {
minimumHeight: 100, minimumHeight: 0,
minimumWidth: 100, minimumWidth: 0,
}, },
new DockviewGroupPanelApiImpl(id, accessor) new DockviewGroupPanelApiImpl(id, accessor)
); );

View File

@ -1,6 +1,6 @@
import { DockviewApi } from '../api/component.api'; import { DockviewApi } from '../api/component.api';
import { getPanelData, PanelTransfer } from '../dnd/dataTransfer'; import { getPanelData, PanelTransfer } from '../dnd/dataTransfer';
import { Droptarget, Position } from '../dnd/droptarget'; import { Position } from '../dnd/droptarget';
import { DockviewComponent } from './dockviewComponent'; import { DockviewComponent } from './dockviewComponent';
import { isAncestor, toggleClass } from '../dom'; import { isAncestor, toggleClass } from '../dom';
import { addDisposableListener, Emitter, Event } from '../events'; import { addDisposableListener, Emitter, Event } from '../events';
@ -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,47 @@ 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;
this.contentContainer.dropTarget.setTargetZones( toggleClass(this.container, 'dv-groupview-floating', false);
value ? ['center'] : ['top', 'bottom', 'left', 'right', 'center'] toggleClass(this.container, 'dv-groupview-popout', false);
);
toggleClass(this.container, 'dv-groupview-floating', value); switch (value) {
case 'grid':
this.contentContainer.dropTarget.setTargetZones([
'top',
'bottom',
'left',
'right',
'center',
]);
break;
case 'floating':
this.contentContainer.dropTarget.setTargetZones(['center']);
this.contentContainer.dropTarget.setTargetZones(
value
? ['center']
: ['top', 'bottom', 'left', 'right', 'center']
);
this.groupPanel.api._onDidFloatingStateChange.fire({ toggleClass(this.container, 'dv-groupview-floating', true);
isFloating: this.isFloating,
break;
case 'popout':
this.contentContainer.dropTarget.setTargetZones(['center']);
toggleClass(this.container, 'dv-groupview-popout', true);
break;
}
this.groupPanel.api._onDidRenderPositionChange.fire({
location: this.location,
}); });
} }
@ -811,7 +840,6 @@ export class DockviewGroupPanelModel
panel.dispose(); panel.dispose();
} }
// this.dropTarget.dispose();
this.tabsContainer.dispose(); this.tabsContainer.dispose();
this.contentContainer.dispose(); this.contentContainer.dispose();
} }

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

@ -64,6 +64,11 @@ export interface IBaseGrid<T extends IGridPanelView> {
layout(width: number, height: number, force?: boolean): void; layout(width: number, height: number, force?: boolean): void;
setVisible(panel: T, visible: boolean): void; setVisible(panel: T, visible: boolean): void;
isVisible(panel: T): boolean; isVisible(panel: T): boolean;
maximizeGroup(panel: T): void;
isMaximizedGroup(panel: T): boolean;
exitMaximizedGroup(): void;
hasMaximizedGroup(): boolean;
readonly onDidMaxmizedGroupChange: Event<void>;
} }
export abstract class BaseGrid<T extends IGridPanelView> export abstract class BaseGrid<T extends IGridPanelView>
@ -174,6 +179,26 @@ export abstract class BaseGrid<T extends IGridPanelView>
return this.gridview.isViewVisible(getGridLocation(panel.element)); return this.gridview.isViewVisible(getGridLocation(panel.element));
} }
maximizeGroup(panel: T): void {
this.gridview.maximizeView(panel);
}
isMaximizedGroup(panel: T): boolean {
return this.gridview.maximizedView() === panel;
}
exitMaximizedGroup(): void {
this.gridview.exitMaximizedView();
}
hasMaximizedGroup(): boolean {
return this.gridview.hasMaximizedView();
}
get onDidMaxmizedGroupChange(): Event<void> {
return this.gridview.onDidMaxmizedNodeChange;
}
protected doAddGroup( protected doAddGroup(
group: T, group: T,
location: number[] = [0], location: number[] = [0],

View File

@ -33,6 +33,10 @@ export class BranchNode extends CompositeDisposable implements IView {
readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> = readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> =
this._onDidChange.event; this._onDidChange.event;
private readonly _onDidVisibilityChange = new Emitter<boolean>();
readonly onDidVisibilityChange: Event<boolean> =
this._onDidVisibilityChange.event;
get width(): number { get width(): number {
return this.orientation === Orientation.HORIZONTAL return this.orientation === Orientation.HORIZONTAL
? this.size ? this.size
@ -48,11 +52,23 @@ export class BranchNode extends CompositeDisposable implements IView {
get minimumSize(): number { get minimumSize(): number {
return this.children.length === 0 return this.children.length === 0
? 0 ? 0
: Math.max(...this.children.map((c) => c.minimumOrthogonalSize)); : Math.max(
...this.children.map((c, index) =>
this.splitview.isViewVisible(index)
? c.minimumOrthogonalSize
: 0
)
);
} }
get maximumSize(): number { get maximumSize(): number {
return Math.min(...this.children.map((c) => c.maximumOrthogonalSize)); return Math.min(
...this.children.map((c, index) =>
this.splitview.isViewVisible(index)
? c.maximumOrthogonalSize
: Number.POSITIVE_INFINITY
)
);
} }
get minimumOrthogonalSize(): number { get minimumOrthogonalSize(): number {
@ -163,6 +179,7 @@ export class BranchNode extends CompositeDisposable implements IView {
this.addDisposables( this.addDisposables(
this._onDidChange, this._onDidChange,
this._onDidVisibilityChange,
this.splitview.onDidSashEnd(() => { this.splitview.onDidSashEnd(() => {
this._onDidChange.fire({}); this._onDidChange.fire({});
}) })
@ -185,7 +202,7 @@ export class BranchNode extends CompositeDisposable implements IView {
return this.splitview.isViewVisible(index); return this.splitview.isViewVisible(index);
} }
setChildVisible(index: number, visible: boolean): void { setChildVisible(index: number, visible: boolean): void {
if (index < 0 || index >= this.children.length) { if (index < 0 || index >= this.children.length) {
throw new Error('Invalid index'); throw new Error('Invalid index');
} }
@ -194,7 +211,18 @@ export class BranchNode extends CompositeDisposable implements IView {
return; return;
} }
const wereAllChildrenHidden = this.splitview.contentSize === 0;
this.splitview.setViewVisible(index, visible); this.splitview.setViewVisible(index, visible);
const areAllChildrenHidden = this.splitview.contentSize === 0;
// If all children are hidden then the parent should hide the entire splitview
// If the entire splitview is hidden then the parent should show the splitview when a child is shown
if (
(visible && wereAllChildrenHidden) ||
(!visible && areAllChildrenHidden)
) {
this._onDidVisibilityChange.fire(visible);
}
} }
moveChild(from: number, to: number): void { moveChild(from: number, to: number): void {
@ -285,15 +313,23 @@ export class BranchNode extends CompositeDisposable implements IView {
private setupChildrenEvents(): void { private setupChildrenEvents(): void {
this._childrenDisposable.dispose(); this._childrenDisposable.dispose();
this._childrenDisposable = Event.any( this._childrenDisposable = new CompositeDisposable(
...this.children.map((c) => c.onDidChange) Event.any(...this.children.map((c) => c.onDidChange))((e) => {
)((e) => { /**
/** * indicate a change has occured to allows any re-rendering but don't bubble
* indicate a change has occured to allows any re-rendering but don't bubble * event because that was specific to this branch
* event because that was specific to this branch */
*/ this._onDidChange.fire({ size: e.orthogonalSize });
this._onDidChange.fire({ size: e.orthogonalSize }); }),
}); ...this.children.map((c, i) => {
if (c instanceof BranchNode) {
return c.onDidVisibilityChange((visible) => {
this.setChildVisible(i, visible);
});
}
return Disposable.NONE;
})
);
} }
public dispose(): void { public dispose(): void {

View File

@ -270,9 +270,11 @@ export interface SerializedGridview<T> {
} }
export class Gridview implements IDisposable { export class Gridview implements IDisposable {
readonly element: HTMLElement;
private _root: BranchNode | undefined; private _root: BranchNode | undefined;
public readonly element: HTMLElement; private _maximizedNode: LeafNode | undefined = undefined;
private disposable: MutableDisposable = new MutableDisposable(); private readonly disposable: MutableDisposable = new MutableDisposable();
private readonly _onDidChange = new Emitter<{ private readonly _onDidChange = new Emitter<{
size?: number; size?: number;
@ -281,6 +283,9 @@ export class Gridview implements IDisposable {
readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> = readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> =
this._onDidChange.event; this._onDidChange.event;
private readonly _onDidMaxmizedNodeChange = new Emitter<void>();
readonly onDidMaxmizedNodeChange = this._onDidMaxmizedNodeChange.event;
public get length(): number { public get length(): number {
return this._root ? this._root.children.length : 0; return this._root ? this._root.children.length : 0;
} }
@ -302,6 +307,7 @@ export class Gridview implements IDisposable {
get width(): number { get width(): number {
return this.root.width; return this.root.width;
} }
get height(): number { get height(): number {
return this.root.height; return this.root.height;
} }
@ -309,16 +315,83 @@ export class Gridview implements IDisposable {
get minimumWidth(): number { get minimumWidth(): number {
return this.root.minimumWidth; return this.root.minimumWidth;
} }
get minimumHeight(): number { get minimumHeight(): number {
return this.root.minimumHeight; return this.root.minimumHeight;
} }
get maximumWidth(): number { get maximumWidth(): number {
return this.root.maximumHeight; return this.root.maximumHeight;
} }
get maximumHeight(): number { get maximumHeight(): number {
return this.root.maximumHeight; return this.root.maximumHeight;
} }
maximizedView(): IGridView | undefined {
return this._maximizedNode?.view;
}
hasMaximizedView(): boolean {
return this._maximizedNode !== undefined;
}
maximizeView(view: IGridView): void {
const location = getGridLocation(view.element);
const [_, node] = this.getNode(location);
if (!(node instanceof LeafNode)) {
return;
}
if (this._maximizedNode === node) {
return;
}
if (this.hasMaximizedView()) {
this.exitMaximizedView();
}
function hideAllViewsBut(parent: BranchNode, exclude: LeafNode): void {
for (let i = 0; i < parent.children.length; i++) {
const child = parent.children[i];
if (child instanceof LeafNode) {
if (child !== exclude) {
parent.setChildVisible(i, false);
}
} else {
hideAllViewsBut(child, exclude);
}
}
}
hideAllViewsBut(this.root, node);
this._maximizedNode = node;
this._onDidMaxmizedNodeChange.fire();
}
exitMaximizedView(): void {
if (!this._maximizedNode) {
return;
}
function showViewsInReverseOrder(parent: BranchNode): void {
for (let index = parent.children.length - 1; index >= 0; index--) {
const child = parent.children[index];
if (child instanceof LeafNode) {
parent.setChildVisible(index, true);
} else {
showViewsInReverseOrder(child);
}
}
}
showViewsInReverseOrder(this.root);
this._maximizedNode = undefined;
this._onDidMaxmizedNodeChange.fire();
}
public serialize(): SerializedGridview<any> { public serialize(): SerializedGridview<any> {
const root = serializeBranchNode(this.getView(), this.orientation); const root = serializeBranchNode(this.getView(), this.orientation);
@ -333,6 +406,7 @@ export class Gridview implements IDisposable {
public dispose(): void { public dispose(): void {
this.disposable.dispose(); this.disposable.dispose();
this._onDidChange.dispose(); this._onDidChange.dispose();
this._onDidMaxmizedNodeChange.dispose();
this.root.dispose(); this.root.dispose();
this.element.remove(); this.element.remove();
@ -584,6 +658,10 @@ export class Gridview implements IDisposable {
} }
setViewVisible(location: number[], visible: boolean): void { setViewVisible(location: number[], visible: boolean): void {
if (this.hasMaximizedView()) {
this.exitMaximizedView();
}
const [rest, index] = tail(location); const [rest, index] = tail(location);
const [, parent] = this.getNode(rest); const [, parent] = this.getNode(rest);
@ -595,6 +673,10 @@ export class Gridview implements IDisposable {
} }
public moveView(parentLocation: number[], from: number, to: number): void { public moveView(parentLocation: number[], from: number, to: number): void {
if (this.hasMaximizedView()) {
this.exitMaximizedView();
}
const [, parent] = this.getNode(parentLocation); const [, parent] = this.getNode(parentLocation);
if (!(parent instanceof BranchNode)) { if (!(parent instanceof BranchNode)) {
@ -609,6 +691,10 @@ export class Gridview implements IDisposable {
size: number | Sizing, size: number | Sizing,
location: number[] location: number[]
): void { ): void {
if (this.hasMaximizedView()) {
this.exitMaximizedView();
}
const [rest, index] = tail(location); const [rest, index] = tail(location);
const [pathToParent, parent] = this.getNode(rest); const [pathToParent, parent] = this.getNode(rest);
@ -670,6 +756,10 @@ export class Gridview implements IDisposable {
} }
removeView(location: number[], sizing?: Sizing): IGridView { removeView(location: number[], sizing?: Sizing): IGridView {
if (this.hasMaximizedView()) {
this.exitMaximizedView();
}
const [rest, index] = tail(location); const [rest, index] = tail(location);
const [pathToParent, parent] = this.getNode(rest); const [pathToParent, parent] = this.getNode(rest);

View File

@ -121,7 +121,6 @@ export class LeafNode implements IView {
public setVisible(visible: boolean): void { public setVisible(visible: boolean): void {
if (this.view.setVisible) { if (this.view.setVisible) {
this.view.setVisible(visible); this.view.setVisible(visible);
this._onDidChange.fire({});
} }
} }

View File

@ -107,11 +107,6 @@
outline-offset: -1px; outline-offset: -1px;
outline-color: var(--dv-paneview-active-outline-color); outline-color: var(--dv-paneview-active-outline-color);
} }
// outline-width: 1px;
// outline-style: solid;
// outline-offset: -1px;
// opacity: 1 !important;
// outline-color: dodgerblue;
} }
} }
} }

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

@ -55,7 +55,6 @@
& > .view-container > .view { & > .view-container > .view {
&:not(:first-child) { &:not(:first-child) {
// padding-left: 1px;
&::before { &::before {
height: 100%; height: 100%;
width: 1px; width: 1px;
@ -89,7 +88,6 @@
width: 100%; width: 100%;
&:not(:first-child) { &:not(:first-child) {
// padding-top: 1px;
&::before { &::before {
height: 1px; height: 1px;
width: 100%; width: 100%;
@ -113,12 +111,12 @@
-ms-user-select: none; // IE 10 and IE 11 -ms-user-select: none; // IE 10 and IE 11
touch-action: none; touch-action: none;
&:active { &:not(.disabled):active {
transition: background-color 0.1s ease-in-out; transition: background-color 0.1s ease-in-out;
background-color: var(--dv-active-sash-color, transparent); background-color: var(--dv-active-sash-color, transparent);
} }
&:hover { &:not(.disabled):hover {
background-color: var(--dv-active-sash-color, transparent); background-color: var(--dv-active-sash-color, transparent);
transition: background-color 0.1s ease-in-out; transition: background-color 0.1s ease-in-out;
transition-delay: 0.5s; transition-delay: 0.5s;

View File

@ -104,8 +104,8 @@ export class Splitview {
private _orientation: Orientation; private _orientation: Orientation;
private _size = 0; private _size = 0;
private _orthogonalSize = 0; private _orthogonalSize = 0;
private contentSize = 0; private _contentSize = 0;
private _proportions: number[] | undefined = undefined; private _proportions: (number | undefined)[] | undefined = undefined;
private proportionalLayout: boolean; private proportionalLayout: boolean;
private _startSnappingEnabled = true; private _startSnappingEnabled = true;
private _endSnappingEnabled = true; private _endSnappingEnabled = true;
@ -117,6 +117,10 @@ export class Splitview {
private readonly _onDidRemoveView = new Emitter<IView>(); private readonly _onDidRemoveView = new Emitter<IView>();
readonly onDidRemoveView = this._onDidRemoveView.event; readonly onDidRemoveView = this._onDidRemoveView.event;
get contentSize(): number {
return this._contentSize;
}
get size(): number { get size(): number {
return this._size; return this._size;
} }
@ -137,7 +141,7 @@ export class Splitview {
return this.viewItems.length; return this.viewItems.length;
} }
public get proportions(): number[] | undefined { public get proportions(): (number | undefined)[] | undefined {
return this._proportions ? [...this._proportions] : undefined; return this._proportions ? [...this._proportions] : undefined;
} }
@ -242,7 +246,7 @@ export class Splitview {
}); });
// Initialize content size and proportions for first layout // Initialize content size and proportions for first layout
this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); this._contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
this.saveProportions(); this.saveProportions();
} }
} }
@ -654,7 +658,7 @@ export class Splitview {
} }
public layout(size: number, orthogonalSize: number): void { public layout(size: number, orthogonalSize: number): void {
const previousSize = Math.max(this.size, this.contentSize); const previousSize = Math.max(this.size, this._contentSize);
this.size = size; this.size = size;
this.orthogonalSize = orthogonalSize; this.orthogonalSize = orthogonalSize;
@ -675,14 +679,30 @@ export class Splitview {
highPriorityIndexes highPriorityIndexes
); );
} else { } else {
let total = 0;
for (let i = 0; i < this.viewItems.length; i++) { for (let i = 0; i < this.viewItems.length; i++) {
const item = this.viewItems[i]; const item = this.viewItems[i];
const proportion = this.proportions[i];
item.size = clamp( if (typeof proportion === 'number') {
Math.round(this.proportions[i] * size), total += proportion;
item.minimumSize, } else {
item.maximumSize size -= item.size;
); }
}
for (let i = 0; i < this.viewItems.length; i++) {
const item = this.viewItems[i];
const proportion = this.proportions[i];
if (typeof proportion === 'number' && total > 0) {
item.size = clamp(
Math.round((proportion * size) / total),
item.minimumSize,
item.maximumSize
);
}
} }
} }
@ -747,15 +767,15 @@ export class Splitview {
} }
private saveProportions(): void { private saveProportions(): void {
if (this.proportionalLayout && this.contentSize > 0) { if (this.proportionalLayout && this._contentSize > 0) {
this._proportions = this.viewItems.map( this._proportions = this.viewItems.map((i) =>
(i) => i.size / this.contentSize i.visible ? i.size / this._contentSize : undefined
); );
} }
} }
private layoutViews(): void { private layoutViews(): void {
this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); this._contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
let sum = 0; let sum = 0;
const x: number[] = []; const x: number[] = [];
@ -880,7 +900,7 @@ export class Splitview {
} else if ( } else if (
snappedAfter && snappedAfter &&
collapsesDown[index] && collapsesDown[index] &&
(position < this.contentSize || this.endSnappingEnabled) (position < this._contentSize || this.endSnappingEnabled)
) { ) {
this.updateSash(sash, SashState.MAXIMUM); this.updateSash(sash, SashState.MAXIMUM);
} else { } else {

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,39 @@ 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);
```
<MultiFrameworkContainer
height={600}
sandboxId="popoutgroup-dockview"
react={DockviewPopoutGroup}
/>
## Maximized Groups
To maximize a group you can all
```tsx
api.maxmimizeGroup(group);
```
## Panels ## Panels
### Add Panel ### Add Panel
@ -889,4 +923,7 @@ If you wish to interact with the drop event from one dockview instance in anothe
### Window-like mananger with tabs ### Window-like mananger with tabs
<DockviewNative2 />
<MultiFrameworkContainer sandboxId="nativeapp-dockview" react={DockviewNative2} />

View File

@ -1,9 +1,9 @@
// @ts-check // @ts-check
// Note: type annotations allow type checking and IDEs autocompletion // Note: type annotations allow type checking and IDEs autocompletion
const lightCodeTheme = require('prism-react-renderer/themes/nightOwlLight'); const { themes } = require('prism-react-renderer');
// const lightCodeTheme = require('prism-react-renderer/themes/dracula'); const lightCodeTheme = themes.nightOwlLight;
const darkCodeTheme = require('prism-react-renderer/themes/vsDark'); const darkCodeTheme = themes.vsDark;
const path = require('path'); const path = require('path');

View File

@ -21,25 +21,25 @@
] ]
}, },
"dependencies": { "dependencies": {
"@docusaurus/core": "^2.4.3", "@docusaurus/core": "^3.0.1",
"@docusaurus/module-type-aliases": "^2.4.3", "@docusaurus/module-type-aliases": "^3.0.1",
"@docusaurus/preset-classic": "^2.4.3", "@docusaurus/preset-classic": "^3.0.1",
"@mdx-js/react": "^1.6.22", "@mdx-js/react": "^3.0.0",
"@minoru/react-dnd-treeview": "^3.4.3", "@minoru/react-dnd-treeview": "^3.4.4",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"axios": "^1.3.3", "axios": "^1.6.3",
"clsx": "^1.2.1", "clsx": "^2.1.0",
"dockview": "^1.8.5", "dockview": "^1.8.5",
"prism-react-renderer": "^1.3.5", "prism-react-renderer": "^2.3.1",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"recoil": "^0.7.6", "recoil": "^0.7.7",
"source-map-loader": "^4.0.1", "source-map-loader": "^4.0.2",
"uuid": "^9.0.0" "uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/docusaurus": "^1.0.6", "@tsconfig/docusaurus": "^2.0.2",
"@types/uuid": "^9.0.0", "@types/uuid": "^9.0.7",
"docusaurus-plugin-sass": "^0.2.3" "docusaurus-plugin-sass": "^0.2.5"
} }
} }

View File

@ -109,29 +109,6 @@ const Icon = (props: {
); );
}; };
const Button = () => {
const [position, setPosition] = React.useState<
{ x: number; y: number } | undefined
>(undefined);
const close = () => setPosition(undefined);
const onClick = (event: React.MouseEvent) => {
setPosition({ x: event.pageX, y: event.pageY });
};
return (
<>
<Icon icon="more_vert" onClick={onClick} />
{position && (
<Popover position={position} close={close}>
<div>hello</div>
</Popover>
)}
</>
);
};
const groupControlsComponents = { const groupControlsComponents = {
panel_1: () => { panel_1: () => {
return <Icon icon="file_download" />; return <Icon icon="file_download" />;
@ -147,6 +124,34 @@ const RightControls = (props: IDockviewHeaderActionsProps) => {
return groupControlsComponents[props.activePanel.id]; return groupControlsComponents[props.activePanel.id];
}, [props.isGroupActive, props.activePanel]); }, [props.isGroupActive, props.activePanel]);
const [icon, setIcon] = React.useState<string>(
props.containerApi.hasMaximizedGroup()
? 'collapse_content'
: 'expand_content'
);
React.useEffect(() => {
const disposable = props.containerApi.onDidMaxmizedGroupChange(() => {
setIcon(
props.containerApi.hasMaximizedGroup()
? 'collapse_content'
: 'expand_content'
);
});
return () => {
disposable.dispose();
};
}, [props.containerApi]);
const onClick = () => {
if (props.containerApi.hasMaximizedGroup()) {
props.containerApi.exitMaxmizedGroup();
} else {
props.activePanel?.api.maximize();
}
};
return ( return (
<div <div
className="group-control" className="group-control"
@ -160,7 +165,7 @@ const RightControls = (props: IDockviewHeaderActionsProps) => {
> >
{props.isGroupActive && <Icon icon="star" />} {props.isGroupActive && <Icon icon="star" />}
{Component && <Component />} {Component && <Component />}
<Button /> <Icon icon={icon} onClick={onClick} />
</div> </div>
); );
}; };

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

@ -39,7 +39,7 @@ For example if n=4 then our split view controls has 4 views. This generic approa
</ul> </ul>
</div> </div>
Additional by definition we can known V<sup>min</sup><sub>n</sub> <= V<sub>n</sub> <= V<sup>max</sup><sub>n</sub> Additional by definition we can known V<sup>min</sup><sub>n</sub> \<= V<sub>n</sub> \<= V<sup>max</sup><sub>n</sub>
To be able to resize a view we need to be able to drag on the edge of a view to increase or decrease it's size. To be able to resize a view we need to be able to drag on the edge of a view to increase or decrease it's size.
This can be achieved by introducing a narrow component that sits between each view acting as a 'drag handle'. This can be achieved by introducing a narrow component that sits between each view acting as a 'drag handle'.

View File

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

5561
yarn.lock

File diff suppressed because it is too large Load Diff