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
on:
workflow_dispatch:
schedule:
- cron: '0 3 * * *' # every day at 3 am UTC

View File

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

View File

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

View File

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

View File

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

View File

@ -19,13 +19,17 @@ class MockGridview implements IGridView {
>().event;
element: HTMLElement = document.createElement('div');
width: number = 0;
height: number = 0;
constructor(private id?: string) {
this.element.className = 'mock-grid-view';
this.element.id = `${id ?? ''}`;
}
layout(width: number, height: number): void {
//
this.width = width;
this.height = height;
}
toJSON(): object {
@ -760,4 +764,255 @@ describe('gridview', () => {
el = gridview.element.querySelectorAll('.mock-grid-view');
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,
TabDragEvent,
} from '../dockview/components/titlebar/tabsContainer';
import { Box } from '../types';
export interface CommonApi<T = any> {
readonly height: number;
@ -804,4 +805,33 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
moveToPrevious(options?: MovementOptions): void {
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 { DockviewComponent } from '../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { DockviewGroupLocation } from '../dockview/dockviewGroupPanelModel';
import { Emitter, Event } from '../events';
import { GridviewPanelApi, GridviewPanelApiImpl } from './gridviewPanelApi';
export interface DockviewGroupPanelApi extends GridviewPanelApi {
readonly onDidFloatingStateChange: Event<DockviewGroupPanelFloatingChangeEvent>;
readonly isFloating: boolean;
readonly onDidRenderPositionChange: Event<DockviewGroupPanelFloatingChangeEvent>;
readonly location: DockviewGroupLocation;
moveTo(options: { group: DockviewGroupPanel; position?: Position }): void;
maximize(): void;
isMaximized(): boolean;
exitMaximized(): void;
}
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 {
private _group: DockviewGroupPanel | undefined;
readonly _onDidFloatingStateChange =
readonly _onDidRenderPositionChange =
new Emitter<DockviewGroupPanelFloatingChangeEvent>();
readonly onDidFloatingStateChange: Event<DockviewGroupPanelFloatingChangeEvent> =
this._onDidFloatingStateChange.event;
readonly onDidRenderPositionChange: Event<DockviewGroupPanelFloatingChangeEvent> =
this._onDidRenderPositionChange.event;
get isFloating() {
get location(): DockviewGroupLocation {
if (!this._group) {
throw new Error(`DockviewGroupPanelApiImpl not initialized`);
throw new Error(NOT_INITIALIZED_MESSAGE);
}
return this._group.model.isFloating;
return this._group.model.location;
}
constructor(id: string, private readonly accessor: DockviewComponent) {
super(id);
this.addDisposables(this._onDidFloatingStateChange);
this.addDisposables(this._onDidRenderPositionChange);
}
moveTo(options: { group: DockviewGroupPanel; position?: Position }): void {
if (!this._group) {
throw new Error(`DockviewGroupPanelApiImpl not initialized`);
throw new Error(NOT_INITIALIZED_MESSAGE);
}
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 {
this._group = group;
}

View File

@ -2,7 +2,7 @@ import { Emitter, Event } from '../events';
import { GridviewPanelApiImpl, GridviewPanelApi } from './gridviewPanelApi';
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { MutableDisposable } from '../lifecycle';
import { DockviewPanel, IDockviewPanel } from '../dockview/dockviewPanel';
import { DockviewPanel } from '../dockview/dockviewPanel';
import { DockviewComponent } from '../dockview/dockviewComponent';
import { Position } from '../dnd/droptarget';
import { DockviewPanelRenderer } from '../overlayRenderContainer';
@ -15,13 +15,10 @@ export interface RendererChangedEvent {
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
extends Omit<
GridviewPanelApi,
// omit properties that do not make sense here
'setVisible' | 'onDidConstraintsChange' | 'setConstraints'
> {
readonly group: DockviewGroupPanel;
@ -39,6 +36,9 @@ export interface DockviewPanelApi
position?: Position;
index?: number;
}): void;
maximize(): void;
isMaximized(): boolean;
exitMaximized(): void;
}
export class DockviewPanelApiImpl
@ -66,7 +66,7 @@ export class DockviewPanelApiImpl
}
get isGroupActive(): boolean {
return !!this.group?.isActive;
return this.group.isActive;
}
get renderer(): DockviewPanelRenderer {
@ -140,4 +140,16 @@ export class DockviewPanelApiImpl
close(): void {
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 {
if (this.group.api.isFloating && !_event.shiftKey) {
if (this.group.api.location === 'floating' && !_event.shiftKey) {
return true;
}
return false;

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import {
CompositeDisposable,
Disposable,
IDisposable,
MutableDisposable,
} from '../../../lifecycle';
@ -8,7 +7,6 @@ import { Emitter, Event } from '../../../events';
import { trackFocus } from '../../../dom';
import { IDockviewPanel } from '../../dockviewPanel';
import { DockviewComponent } from '../../dockviewComponent';
import { DragAndDropObserver } from '../../../dnd/dnd';
import { Droptarget } from '../../../dnd/droptarget';
import { DockviewGroupPanelModel } from '../../dockviewGroupPanelModel';
import { getPanelData } from '../../../dnd/dataTransfer';
@ -70,7 +68,11 @@ export class ContentContainer
const data = getPanelData();
if (!data && event.shiftKey && !this.group.isFloating) {
if (
!data &&
event.shiftKey &&
this.group.location !== 'floating'
) {
return false;
}

View File

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

View File

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

View File

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

View File

@ -47,29 +47,45 @@ import { getPanelData } from '../dnd/dataTransfer';
import { Parameters } from '../panel/types';
import { Overlay } from '../dnd/overlay';
import { toggleClass, watchElementResize } from '../dom';
import {
DockviewFloatingGroupPanel,
IDockviewFloatingGroupPanel,
} from './dockviewFloatingGroupPanel';
import { DockviewFloatingGroupPanel } from './dockviewFloatingGroupPanel';
import {
GroupDragEvent,
TabDragEvent,
} from './components/titlebar/tabsContainer';
import {
OverlayRenderContainer,
DockviewPanelRenderer,
<<<<<<< Updated upstream
} from './components/greadyRenderContainer';
=======
} from '../overlayRenderContainer';
import { Box } from '../types';
import { DockviewPopoutGroupPanel } from './dockviewPopoutGroupPanel';
import {
DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE,
DEFAULT_FLOATING_GROUP_POSITION,
} 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 {
update: (event: { params: { [key: string]: any } }) => void;
@ -78,7 +94,12 @@ export interface PanelReference {
export interface SerializedFloatingGroup {
data: GroupPanelViewState;
position: { height: number; width: number; left: number; top: number };
position: Box;
}
export interface SerializedPopoutGroup {
data: GroupPanelViewState;
position: Box | null;
}
export interface SerializedDockview {
@ -91,6 +112,7 @@ export interface SerializedDockview {
panels: Record<string, GroupviewPanelState>;
activeGroup?: string;
floatingGroups?: SerializedFloatingGroup[];
popoutGroups?: SerializedPopoutGroup[];
}
function typeValidate3(data: GroupPanelViewState, path: string): void {
@ -205,7 +227,6 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
readonly activePanel: IDockviewPanel | undefined;
readonly totalPanels: number;
readonly panels: IDockviewPanel[];
readonly floatingGroups: IDockviewFloatingGroupPanel[];
readonly onDidDrop: Event<DockviewDropEvent>;
readonly orientation: Orientation;
updateOptions(options: DockviewComponentUpdateOptions): void;
@ -246,6 +267,13 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
item: IDockviewPanel | DockviewGroupPanel,
coord?: { x: number; y: number }
): void;
addPopoutGroup(
item: IDockviewPanel | DockviewGroupPanel,
options?: {
position?: Box;
popoutUrl?: string;
}
): void;
}
export class DockviewComponent
@ -286,7 +314,8 @@ export class DockviewComponent
readonly onDidActivePanelChange: Event<IDockviewPanel | undefined> =
this._onDidActivePanelChange.event;
readonly floatingGroups: DockviewFloatingGroupPanel[] = [];
private readonly _floatingGroups: DockviewFloatingGroupPanel[] = [];
private readonly _popoutGroups: DockviewPopoutGroupPanel[] = [];
get orientation(): Orientation {
return this.gridview.orientation;
@ -454,6 +483,73 @@ export class DockviewComponent
this.updateWatermark();
}
addPopoutGroup(
item: DockviewPanel | DockviewGroupPanel,
options?: {
skipRemoveGroup?: boolean;
position?: Box;
popoutUrl?: string;
}
): void {
let group: DockviewGroupPanel;
let box: Box | undefined = options?.position;
if (item instanceof DockviewPanel) {
group = this.createGroup();
this.removePanel(item, {
removeEmptyGroup: true,
skipDispose: true,
});
group.model.openPanel(item);
if (!box) {
box = this.element.getBoundingClientRect();
}
} else {
group = item;
if (!box) {
box = group.element.getBoundingClientRect();
}
const skip =
typeof options?.skipRemoveGroup === 'boolean' &&
options.skipRemoveGroup;
if (!skip) {
this.doRemoveGroup(item, { skipDispose: true });
}
}
const theme = getTheme(this.gridview.element);
const popoutWindow = new DockviewPopoutGroupPanel(group, {
className: theme ?? '',
popoutUrl: options?.popoutUrl ?? '/popout.html',
box: {
left: box.left,
top: box.top,
width: box.width,
height: box.height,
},
});
popoutWindow.addDisposables(
{
dispose: () => {
remove(this._popoutGroups, popoutWindow);
},
},
popoutWindow.window.onDidClose(() => {
this.doAddGroup(group, [0]);
})
);
this._popoutGroups.push(popoutWindow);
}
addFloatingGroup(
item: DockviewPanel | DockviewGroupPanel,
coord?: { x?: number; y?: number; height?: number; width?: number },
@ -482,12 +578,16 @@ export class DockviewComponent
}
}
group.model.isFloating = true;
group.model.location = 'floating';
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 =
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({
container: this.gridview.element,
@ -553,14 +653,14 @@ export class DockviewComponent
dispose: () => {
disposable.dispose();
group.model.isFloating = false;
remove(this.floatingGroups, floatingGroupPanel);
group.model.location = 'grid';
remove(this._floatingGroups, floatingGroupPanel);
this.updateWatermark();
},
}
);
this.floatingGroups.push(floatingGroupPanel);
this._floatingGroups.push(floatingGroupPanel);
this.updateWatermark();
}
@ -614,7 +714,7 @@ export class DockviewComponent
}
if (hasFloatingGroupOptionsChanged) {
for (const group of this.floatingGroups) {
for (const group of this._floatingGroups) {
switch (this.options.floatingGroupBounds) {
case 'boundedWithinViewport':
group.overlay.minimumInViewportHeight = undefined;
@ -647,8 +747,8 @@ export class DockviewComponent
): void {
super.layout(width, height, forceResize);
if (this.floatingGroups) {
for (const floating of this.floatingGroups) {
if (this._floatingGroups) {
for (const floating of this._floatingGroups) {
// ensure floting groups stay within visible boundaries
floating.overlay.setBounds();
}
@ -726,11 +826,20 @@ export class DockviewComponent
return collection;
}, {} as { [key: string]: GroupviewPanelState });
const floats: SerializedFloatingGroup[] = this.floatingGroups.map(
(floatingGroup) => {
const floats: SerializedFloatingGroup[] = this._floatingGroups.map(
(group) => {
return {
data: floatingGroup.group.toJSON() as GroupPanelViewState,
position: floatingGroup.overlay.toJSON(),
data: group.group.toJSON() as GroupPanelViewState,
position: group.overlay.toJSON(),
};
}
);
const popoutGroups: SerializedPopoutGroup[] = this._popoutGroups.map(
(group) => {
return {
data: group.group.toJSON() as GroupPanelViewState,
position: group.window.dimensions(),
};
}
);
@ -745,6 +854,10 @@ export class DockviewComponent
result.floatingGroups = floats;
}
if (popoutGroups.length > 0) {
result.popoutGroups = popoutGroups;
}
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();
}
@ -884,7 +1010,7 @@ export class DockviewComponent
}
// iterate over a reassigned array since original array will be modified
for (const floatingGroup of [...this.floatingGroups]) {
for (const floatingGroup of [...this._floatingGroups]) {
floatingGroup.dispose();
}
@ -1008,7 +1134,10 @@ export class DockviewComponent
panel = this.createPanel(options, group);
group.model.openPanel(panel);
this.doSetGroupAndPanelActive(group);
} else if (referenceGroup.api.isFloating || target === 'center') {
} else if (
referenceGroup.api.location === 'floating' ||
target === 'center'
) {
panel = this.createPanel(options, referenceGroup);
referenceGroup.model.openPanel(panel);
} else {
@ -1092,7 +1221,7 @@ export class DockviewComponent
}
private updateWatermark(): void {
if (this.groups.filter((x) => !x.api.isFloating).length === 0) {
if (this.groups.filter((x) => x.api.location === 'grid').length === 0) {
if (!this.watermark) {
this.watermark = this.createWatermarkComponent();
@ -1210,27 +1339,61 @@ export class DockviewComponent
}
| undefined
): DockviewGroupPanel {
const floatingGroup = this.floatingGroups.find(
(_) => _.group === group
);
if (floatingGroup) {
if (!options?.skipDispose) {
floatingGroup.group.dispose();
this._groups.delete(group.id);
this._onDidRemoveGroup.fire(group);
if (group.api.location === 'floating') {
const floatingGroup = this._floatingGroups.find(
(_) => _.group === group
);
if (floatingGroup) {
if (!options?.skipDispose) {
floatingGroup.group.dispose();
this._groups.delete(group.id);
this._onDidRemoveGroup.fire(group);
}
remove(this._floatingGroups, floatingGroup);
floatingGroup.dispose();
if (!options?.skipActive && this._activeGroup === group) {
const groups = Array.from(this._groups.values());
this.doSetGroupActive(
groups.length > 0 ? groups[0].value : undefined
);
}
return floatingGroup.group;
}
floatingGroup.dispose();
throw new Error('failed to find floating group');
}
if (!options?.skipActive && this._activeGroup === group) {
const groups = Array.from(this._groups.values());
if (group.api.location === 'popout') {
const selectedGroup = this._popoutGroups.find(
(_) => _.group === group
);
this.doSetGroupActive(
groups.length > 0 ? groups[0].value : undefined
);
if (selectedGroup) {
if (!options?.skipDispose) {
selectedGroup.group.dispose();
this._groups.delete(group.id);
this._onDidRemoveGroup.fire(group);
}
selectedGroup.dispose();
if (!options?.skipActive && this._activeGroup === group) {
const groups = Array.from(this._groups.values());
this.doSetGroupActive(
groups.length > 0 ? groups[0].value : undefined
);
}
return selectedGroup.group;
}
return floatingGroup.group;
throw new Error('failed to find popout group');
}
return super.doRemoveGroup(group, options);
@ -1285,11 +1448,7 @@ export class DockviewComponent
if (sourceGroup && sourceGroup.size < 2) {
const [targetParentLocation, to] = tail(targetLocation);
const isFloating = this.floatingGroups.find(
(x) => x.group === sourceGroup
);
if (!isFloating) {
if (sourceGroup.api.location === 'grid') {
const sourceLocation = getGridLocation(sourceGroup.element);
const [sourceParentLocation, from] = tail(sourceLocation);
@ -1365,16 +1524,31 @@ export class DockviewComponent
});
}
} else {
const floatingGroup = this.floatingGroups.find(
(x) => x.group === sourceGroup
);
if (floatingGroup) {
floatingGroup.dispose();
} else {
this.gridview.removeView(
getGridLocation(sourceGroup.element)
);
switch (sourceGroup.api.location) {
case 'grid':
this.gridview.removeView(
getGridLocation(sourceGroup.element)
);
break;
case 'floating': {
const selectedFloatingGroup = this._floatingGroups.find(
(x) => x.group === sourceGroup
);
if (!selectedFloatingGroup) {
throw new Error('failed to find floating group');
}
selectedFloatingGroup.dispose();
break;
}
case 'popout': {
const selectedPopoutGroup = this._popoutGroups.find(
(x) => x.group === sourceGroup
);
if (!selectedPopoutGroup) {
throw new Error('failed to find popout group');
}
selectedPopoutGroup.dispose();
}
}
const referenceLocation = getGridLocation(

View File

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

View File

@ -1,6 +1,6 @@
import { DockviewApi } from '../api/component.api';
import { getPanelData, PanelTransfer } from '../dnd/dataTransfer';
import { Droptarget, Position } from '../dnd/droptarget';
import { Position } from '../dnd/droptarget';
import { DockviewComponent } from './dockviewComponent';
import { isAncestor, toggleClass } from '../dom';
import { addDisposableListener, Emitter, Event } from '../events';
@ -130,6 +130,8 @@ export interface IDockviewGroupPanelModel extends IPanel {
): boolean;
}
export type DockviewGroupLocation = 'grid' | 'floating' | 'popout';
export class DockviewGroupPanelModel
extends CompositeDisposable
implements IDockviewGroupPanelModel
@ -141,11 +143,12 @@ export class DockviewGroupPanelModel
private watermark?: IWatermarkRenderer;
private _isGroupActive = false;
private _locked: DockviewGroupPanelLocked = false;
private _isFloating = false;
private _rightHeaderActions: IHeaderActionsRenderer | undefined;
private _leftHeaderActions: IHeaderActionsRenderer | undefined;
private _prefixHeaderActions: IHeaderActionsRenderer | undefined;
private _location: DockviewGroupLocation = 'grid';
private mostRecentlyUsed: IDockviewPanel[] = [];
private readonly _onDidChange = new Emitter<IViewSize | undefined>();
@ -241,21 +244,47 @@ export class DockviewGroupPanelModel
);
}
get isFloating(): boolean {
return this._isFloating;
get location(): DockviewGroupLocation {
return this._location;
}
set isFloating(value: boolean) {
this._isFloating = value;
set location(value: DockviewGroupLocation) {
this._location = value;
this.contentContainer.dropTarget.setTargetZones(
value ? ['center'] : ['top', 'bottom', 'left', 'right', 'center']
);
toggleClass(this.container, 'dv-groupview-floating', false);
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({
isFloating: this.isFloating,
toggleClass(this.container, 'dv-groupview-floating', true);
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();
}
// this.dropTarget.dispose();
this.tabsContainer.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;
minimumWidthWithinViewport?: number;
};
popoutUrl?: string;
defaultRenderer?: DockviewPanelRenderer;
debug?: boolean;
}

View File

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

View File

@ -64,6 +64,11 @@ export interface IBaseGrid<T extends IGridPanelView> {
layout(width: number, height: number, force?: boolean): void;
setVisible(panel: T, visible: boolean): void;
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>
@ -174,6 +179,26 @@ export abstract class BaseGrid<T extends IGridPanelView>
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(
group: T,
location: number[] = [0],

View File

@ -33,6 +33,10 @@ export class BranchNode extends CompositeDisposable implements IView {
readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> =
this._onDidChange.event;
private readonly _onDidVisibilityChange = new Emitter<boolean>();
readonly onDidVisibilityChange: Event<boolean> =
this._onDidVisibilityChange.event;
get width(): number {
return this.orientation === Orientation.HORIZONTAL
? this.size
@ -48,11 +52,23 @@ export class BranchNode extends CompositeDisposable implements IView {
get minimumSize(): number {
return this.children.length === 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 {
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 {
@ -163,6 +179,7 @@ export class BranchNode extends CompositeDisposable implements IView {
this.addDisposables(
this._onDidChange,
this._onDidVisibilityChange,
this.splitview.onDidSashEnd(() => {
this._onDidChange.fire({});
})
@ -185,7 +202,7 @@ export class BranchNode extends CompositeDisposable implements IView {
return this.splitview.isViewVisible(index);
}
setChildVisible(index: number, visible: boolean): void {
setChildVisible(index: number, visible: boolean): void {
if (index < 0 || index >= this.children.length) {
throw new Error('Invalid index');
}
@ -194,7 +211,18 @@ export class BranchNode extends CompositeDisposable implements IView {
return;
}
const wereAllChildrenHidden = this.splitview.contentSize === 0;
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 {
@ -285,15 +313,23 @@ export class BranchNode extends CompositeDisposable implements IView {
private setupChildrenEvents(): void {
this._childrenDisposable.dispose();
this._childrenDisposable = Event.any(
...this.children.map((c) => c.onDidChange)
)((e) => {
/**
* indicate a change has occured to allows any re-rendering but don't bubble
* event because that was specific to this branch
*/
this._onDidChange.fire({ size: e.orthogonalSize });
});
this._childrenDisposable = new CompositeDisposable(
Event.any(...this.children.map((c) => c.onDidChange))((e) => {
/**
* indicate a change has occured to allows any re-rendering but don't bubble
* event because that was specific to this branch
*/
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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,25 +21,25 @@
]
},
"dependencies": {
"@docusaurus/core": "^2.4.3",
"@docusaurus/module-type-aliases": "^2.4.3",
"@docusaurus/preset-classic": "^2.4.3",
"@mdx-js/react": "^1.6.22",
"@minoru/react-dnd-treeview": "^3.4.3",
"@docusaurus/core": "^3.0.1",
"@docusaurus/module-type-aliases": "^3.0.1",
"@docusaurus/preset-classic": "^3.0.1",
"@mdx-js/react": "^3.0.0",
"@minoru/react-dnd-treeview": "^3.4.4",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-popover": "^1.0.7",
"axios": "^1.3.3",
"clsx": "^1.2.1",
"axios": "^1.6.3",
"clsx": "^2.1.0",
"dockview": "^1.8.5",
"prism-react-renderer": "^1.3.5",
"prism-react-renderer": "^2.3.1",
"react-dnd": "^16.0.1",
"recoil": "^0.7.6",
"source-map-loader": "^4.0.1",
"uuid": "^9.0.0"
"recoil": "^0.7.7",
"source-map-loader": "^4.0.2",
"uuid": "^9.0.1"
},
"devDependencies": {
"@tsconfig/docusaurus": "^1.0.6",
"@types/uuid": "^9.0.0",
"docusaurus-plugin-sass": "^0.2.3"
"@tsconfig/docusaurus": "^2.0.2",
"@types/uuid": "^9.0.7",
"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 = {
panel_1: () => {
return <Icon icon="file_download" />;
@ -147,6 +124,34 @@ const RightControls = (props: IDockviewHeaderActionsProps) => {
return groupControlsComponents[props.activePanel.id];
}, [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 (
<div
className="group-control"
@ -160,7 +165,7 @@ const RightControls = (props: IDockviewHeaderActionsProps) => {
>
{props.isGroupActive && <Icon icon="star" />}
{Component && <Component />}
<Button />
<Icon icon={icon} onClick={onClick} />
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,7 +39,7 @@ For example if n=4 then our split view controls has 4 views. This generic approa
</ul>
</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.
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