From 5799549901cc54197c2d28ef6b4fe96505ee638d Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sun, 29 Oct 2023 13:54:32 +0000 Subject: [PATCH 1/3] feat: popout windows --- .../__tests__/dnd/groupDragHandler.spec.ts | 8 +- .../components/titlebar/tabsContainer.spec.ts | 12 +- .../dockview/dockviewComponent.spec.ts | 216 ++++++------- .../dockview-core/src/api/component.api.ts | 11 + .../src/api/dockviewGroupPanelApi.ts | 19 +- .../dockview-core/src/dnd/groupDragHandler.ts | 2 +- packages/dockview-core/src/dnd/overlay.ts | 18 +- .../dockview/components/tab/defaultTab.scss | 18 ++ .../components/titlebar/tabsContainer.ts | 4 +- .../src/dockview/dockviewComponent.ts | 301 +++++++++++++++--- .../src/dockview/dockviewGroupPanelModel.ts | 48 ++- .../dockview-core/src/dockview/options.ts | 1 + packages/dockview-core/src/dom.ts | 32 ++ packages/dockview-core/src/popoutWindow.ts | 121 +++++++ packages/dockview-core/src/types.ts | 7 + packages/docs/docs/components/dockview.mdx | 8 +- .../docs/sandboxes/demo-dockview/src/app.tsx | 5 + .../floatinggroup-dockview/src/app.tsx | 10 +- .../popoutgroup-dockview/package.json | 32 ++ .../popoutgroup-dockview/public/index.html | 44 +++ .../popoutgroup-dockview/src/app.tsx | 253 +++++++++++++++ .../popoutgroup-dockview/src/index.tsx | 20 ++ .../popoutgroup-dockview/src/styles.css | 16 + .../popoutgroup-dockview/src/utils.tsx | 30 ++ .../popoutgroup-dockview/tsconfig.json | 18 ++ packages/docs/src/pages/popout.tsx | 5 + 26 files changed, 1048 insertions(+), 211 deletions(-) create mode 100644 packages/dockview-core/src/popoutWindow.ts create mode 100644 packages/docs/sandboxes/popoutgroup-dockview/package.json create mode 100644 packages/docs/sandboxes/popoutgroup-dockview/public/index.html create mode 100644 packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx create mode 100644 packages/docs/sandboxes/popoutgroup-dockview/src/index.tsx create mode 100644 packages/docs/sandboxes/popoutgroup-dockview/src/styles.css create mode 100644 packages/docs/sandboxes/popoutgroup-dockview/src/utils.tsx create mode 100644 packages/docs/sandboxes/popoutgroup-dockview/tsconfig.json create mode 100644 packages/docs/src/pages/popout.tsx diff --git a/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts b/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts index 7ca528766..b2cd3d44d 100644 --- a/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts +++ b/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts @@ -11,7 +11,7 @@ describe('groupDragHandler', () => { const groupMock = jest.fn(() => { const partial: Partial = { 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(() => { const partial: Partial = { - api: { isFloating: true } as any, + api: { location: 'floating' } as any, }; return partial as DockviewGroupPanel; }); @@ -85,7 +85,7 @@ describe('groupDragHandler', () => { const groupMock = jest.fn(() => { const partial: Partial = { - api: { isFloating: false } as any, + api: { location: 'grid' } as any, }; return partial as DockviewGroupPanel; }); diff --git a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts index 711d2ee0b..58a007393 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts @@ -478,7 +478,7 @@ describe('tabsContainer', () => { const groupPanelMock = jest.fn(() => { return (>{ - api: { isFloating: false } as any, + api: { location: 'grid' } as any, }) as DockviewGroupPanel; }); @@ -538,7 +538,7 @@ describe('tabsContainer', () => { const groupPanelMock = jest.fn(() => { return (>{ - api: { isFloating: true } as any, + api: { location: 'floating' } as any, }) as DockviewGroupPanel; }); @@ -591,7 +591,7 @@ describe('tabsContainer', () => { const groupPanelMock = jest.fn(() => { return (>{ - 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(() => { return (>{ - 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(() => { return (>{ - 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(() => { return (>{ - api: { isFloating: true } as any, + api: { location: 'grid' } as any, model: {} as any, }) as DockviewGroupPanel; }); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index c3d858315..450800fb5 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -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); }); diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index c46a2aa56..fc8d36334 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -42,6 +42,7 @@ import { GroupDragEvent, TabDragEvent, } from '../dockview/components/titlebar/tabsContainer'; +import { Box } from '../types'; export interface CommonApi { readonly height: number; @@ -804,4 +805,14 @@ export class DockviewApi implements CommonApi { moveToPrevious(options?: MovementOptions): void { this.component.moveToPrevious(options); } + + addPopoutGroup( + item: IDockviewPanel | DockviewGroupPanel, + options?: { + skipRemoveGroup?: boolean; + position?: Box; + } + ): void { + this.component.addPopoutGroup(item, options); + } } diff --git a/packages/dockview-core/src/api/dockviewGroupPanelApi.ts b/packages/dockview-core/src/api/dockviewGroupPanelApi.ts index 65154b961..c277d8680 100644 --- a/packages/dockview-core/src/api/dockviewGroupPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewGroupPanelApi.ts @@ -1,38 +1,39 @@ import { Position } from '../dnd/droptarget'; import { DockviewComponent } from '../dockview/dockviewComponent'; import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel'; +import { DockviewGroupLocation } from '../dockview/dockviewGroupPanelModel'; import { Emitter, Event } from '../events'; import { GridviewPanelApi, GridviewPanelApiImpl } from './gridviewPanelApi'; export interface DockviewGroupPanelApi extends GridviewPanelApi { - readonly onDidFloatingStateChange: Event; - readonly isFloating: boolean; + readonly onDidRenderPositionChange: Event; + readonly location: DockviewGroupLocation; moveTo(options: { group: DockviewGroupPanel; position?: Position }): void; } export interface DockviewGroupPanelFloatingChangeEvent { - readonly isFloating: boolean; + readonly location: DockviewGroupLocation; } export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl { private _group: DockviewGroupPanel | undefined; - readonly _onDidFloatingStateChange = + readonly _onDidRenderPositionChange = new Emitter(); - readonly onDidFloatingStateChange: Event = - this._onDidFloatingStateChange.event; + readonly onDidRenderPositionChange: Event = + this._onDidRenderPositionChange.event; - get isFloating() { + get location(): DockviewGroupLocation { if (!this._group) { throw new Error(`DockviewGroupPanelApiImpl not initialized`); } - return this._group.model.isFloating; + return this._group.model.location; } constructor(id: string, private readonly accessor: DockviewComponent) { super(id); - this.addDisposables(this._onDidFloatingStateChange); + this.addDisposables(this._onDidRenderPositionChange); } moveTo(options: { group: DockviewGroupPanel; position?: Position }): void { diff --git a/packages/dockview-core/src/dnd/groupDragHandler.ts b/packages/dockview-core/src/dnd/groupDragHandler.ts index fd6d42f77..de4a3ef04 100644 --- a/packages/dockview-core/src/dnd/groupDragHandler.ts +++ b/packages/dockview-core/src/dnd/groupDragHandler.ts @@ -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; diff --git a/packages/dockview-core/src/dnd/overlay.ts b/packages/dockview-core/src/dnd/overlay.ts index 05e1afb18..7780bb785 100644 --- a/packages/dockview-core/src/dnd/overlay.ts +++ b/packages/dockview-core/src/dnd/overlay.ts @@ -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 = {}): 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(); diff --git a/packages/dockview-core/src/dockview/components/tab/defaultTab.scss b/packages/dockview-core/src/dockview/components/tab/defaultTab.scss index d820534b8..2a1e58734 100644 --- a/packages/dockview-core/src/dockview/components/tab/defaultTab.scss +++ b/packages/dockview-core/src/dockview/components/tab/defaultTab.scss @@ -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); diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index 77a3f7182..123608573 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -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 && diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index e7ab959c2..4621e267a 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -7,7 +7,7 @@ import { import { directionToPosition, Droptarget, Position } from '../dnd/droptarget'; import { tail, sequenceEquals, remove } from '../array'; import { DockviewPanel, IDockviewPanel } from './dockviewPanel'; -import { CompositeDisposable } from '../lifecycle'; +import { CompositeDisposable, IDisposable } from '../lifecycle'; import { Event, Emitter } from '../events'; import { Watermark } from './components/watermark/watermark'; import { @@ -47,14 +47,40 @@ import { getPanelData } from '../dnd/dataTransfer'; import { Parameters } from '../panel/types'; import { Overlay } from '../dnd/overlay'; import { toggleClass, watchElementResize } from '../dom'; -import { - DockviewFloatingGroupPanel, - IDockviewFloatingGroupPanel, -} from './dockviewFloatingGroupPanel'; +import { DockviewFloatingGroupPanel } from './dockviewFloatingGroupPanel'; import { GroupDragEvent, TabDragEvent, } from './components/titlebar/tabsContainer'; +import { PopoutWindow } from '../popoutWindow'; +import { Box } from '../types'; + +function getTheme(element: HTMLElement): string | undefined { + function toClassList(element: HTMLElement) { + const list: string[] = []; + + for (let i = 0; i < element.classList.length; i++) { + list.push(element.classList.item(i)!); + } + + return list; + } + + let theme: string | undefined = undefined; + let parent: HTMLElement | null = element; + + while (parent !== null) { + theme = toClassList(parent).find((cls) => + cls.startsWith('dockview-theme-') + ); + if (typeof theme === 'string') { + break; + } + parent = parent.parentElement; + } + + return theme; +} const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100; @@ -65,7 +91,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 { @@ -78,6 +109,7 @@ export interface SerializedDockview { panels: Record; activeGroup?: string; floatingGroups?: SerializedFloatingGroup[]; + popoutGroups?: SerializedPopoutGroup[]; } function typeValidate3(data: GroupPanelViewState, path: string): void { @@ -188,11 +220,16 @@ export interface DockviewDropEvent extends GroupviewDropEvent { group: DockviewGroupPanel | null; } +export interface DockviewPopoutGroupPanel { + window: PopoutWindow; + disposable: IDisposable; + group: DockviewGroupPanel; +} + export interface IDockviewComponent extends IBaseGrid { readonly activePanel: IDockviewPanel | undefined; readonly totalPanels: number; readonly panels: IDockviewPanel[]; - readonly floatingGroups: IDockviewFloatingGroupPanel[]; readonly onDidDrop: Event; readonly orientation: Orientation; updateOptions(options: DockviewComponentUpdateOptions): void; @@ -233,6 +270,13 @@ export interface IDockviewComponent extends IBaseGrid { item: IDockviewPanel | DockviewGroupPanel, coord?: { x: number; y: number } ): void; + addPopoutGroup( + item: IDockviewPanel | DockviewGroupPanel, + options?: { + skipRemoveGroup?: boolean; + position?: Box; + } + ): void; } export class DockviewComponent @@ -271,7 +315,8 @@ export class DockviewComponent readonly onDidActivePanelChange: Event = this._onDidActivePanelChange.event; - readonly floatingGroups: DockviewFloatingGroupPanel[] = []; + private readonly _floatingGroups: DockviewFloatingGroupPanel[] = []; + private readonly _popoutGroups: DockviewPopoutGroupPanel[] = []; get orientation(): Orientation { return this.gridview.orientation; @@ -427,6 +472,85 @@ export class DockviewComponent this.updateWatermark(); } + addPopoutGroup( + item: DockviewPanel | DockviewGroupPanel, + options?: { + skipRemoveGroup?: boolean; + position?: Box; + } + ): void { + let group: DockviewGroupPanel; + const theme = getTheme(this.gridview.element); + + 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 { top: boundingTop, left: boundingLeft } = + // this.element.getBoundingClientRect(); + + const window = new PopoutWindow('test', theme ?? '', { + url: this.options.popoutUrl ?? 'popout.html', + left: box.left, + top: box.top, + width: box.width, + height: box.height, + }); + + const disposable = new CompositeDisposable(); + const wrappedWindow = { window, disposable, group }; + + disposable.addDisposables( + window.onDidClose(() => { + group.model.location = 'grid'; + + remove(this._popoutGroups, wrappedWindow); + + this.doAddGroup(group, [0]); + }), + { + dispose: () => { + group.model.location = 'grid'; + remove(this._popoutGroups, wrappedWindow); + }, + }, + window + ); + + group.model.location = 'popout'; + + this._popoutGroups.push(wrappedWindow); + + window.open(group.element); + } + addFloatingGroup( item: DockviewPanel | DockviewGroupPanel, coord?: { x?: number; y?: number; height?: number; width?: number }, @@ -455,7 +579,7 @@ export class DockviewComponent } } - group.model.isFloating = true; + group.model.location = 'floating'; const overlayLeft = typeof coord?.x === 'number' ? Math.max(coord.x, 0) : 100; @@ -526,14 +650,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(); } @@ -587,7 +711,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; @@ -620,8 +744,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(); } @@ -699,11 +823,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(), }; } ); @@ -718,6 +851,10 @@ export class DockviewComponent result.floatingGroups = floats; } + if (popoutGroups.length > 0) { + result.popoutGroups = popoutGroups; + } + return result; } @@ -823,7 +960,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(); } @@ -857,7 +1007,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(); } @@ -981,7 +1131,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 { @@ -1064,7 +1217,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(); @@ -1182,27 +1335,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.disposable.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); @@ -1257,11 +1444,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); @@ -1337,16 +1520,34 @@ export class DockviewComponent }); } } else { - const floatingGroup = this.floatingGroups.find( + 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 floatingGroup = this._floatingGroups.find( + (x) => x.group === sourceGroup + ); + if (!floatingGroup) { + throw new Error('failed to find floating group'); + } + floatingGroup.dispose(); + break; + case 'popout': + const selectedGroup = this._popoutGroups.find( + (x) => x.group === sourceGroup + ); + if (!selectedGroup) { + throw new Error('failed to find popout group'); + } + selectedGroup.disposable.dispose(); + selectedGroup.window.dispose(); } const referenceLocation = getGridLocation( diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 43ee03b01..34932ff45 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -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(); @@ -241,21 +244,42 @@ 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.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.dropTarget.setTargetZones([ + 'top', + 'bottom', + 'left', + 'right', + 'center', + ]); + break; + case 'floating': + this.dropTarget.setTargetZones(['center']); - this.groupPanel.api._onDidFloatingStateChange.fire({ - isFloating: this.isFloating, + toggleClass(this.container, 'dv-groupview-floating', true); + + break; + case 'popout': + this.dropTarget.setTargetZones(['center']); + + toggleClass(this.container, 'dv-groupview-popout', true); + + break; + } + + this.groupPanel.api._onDidRenderPositionChange.fire({ + location: this.location, }); } @@ -286,7 +310,7 @@ export class DockviewGroupPanelModel const data = getPanelData(); - if (!data && event.shiftKey && !this.isFloating) { + if (!data && event.shiftKey && this.location !== 'floating') { return false; } diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 8bd23025a..d5b5565ee 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -96,6 +96,7 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions { minimumHeightWithinViewport?: number; minimumWidthWithinViewport?: number; }; + popoutUrl?: string; } export interface PanelOptions

{ diff --git a/packages/dockview-core/src/dom.ts b/packages/dockview-core/src/dom.ts index a12b50742..38a3813f1 100644 --- a/packages/dockview-core/src/dom.ts +++ b/packages/dockview-core/src/dom.ts @@ -185,3 +185,35 @@ export function quasiPreventDefault(event: Event): void { 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); + } + } +} diff --git a/packages/dockview-core/src/popoutWindow.ts b/packages/dockview-core/src/popoutWindow.ts new file mode 100644 index 000000000..2fb14fa7d --- /dev/null +++ b/packages/dockview-core/src/popoutWindow.ts @@ -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(); + 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(); + }); + }); + } +} diff --git a/packages/dockview-core/src/types.ts b/packages/dockview-core/src/types.ts index 935d54b21..ed669f694 100644 --- a/packages/dockview-core/src/types.ts +++ b/packages/dockview-core/src/types.ts @@ -1,3 +1,10 @@ export type FunctionOrValue = (() => T) | T; export type Optional = Pick, K> & Omit; + +export interface Box { + left: number; + top: number; + height: number; + width: number; +} diff --git a/packages/docs/docs/components/dockview.mdx b/packages/docs/docs/components/dockview.mdx index 6c9528f88..0469a799d 100644 --- a/packages/docs/docs/components/dockview.mdx +++ b/packages/docs/docs/components/dockview.mdx @@ -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 { DocRef, Markdown } from '@site/src/components/ui/reference/docRef'; @@ -355,7 +356,7 @@ Floating groups can be interacted with whilst holding the `shift` key activating 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: @@ -369,6 +370,11 @@ You can control the bounding box of floating groups through the optional `floati react={DockviewFloating} /> +## Popout Window + + + + ## Panels ### Add Panel diff --git a/packages/docs/sandboxes/demo-dockview/src/app.tsx b/packages/docs/sandboxes/demo-dockview/src/app.tsx index 43a1c779f..4384bfe28 100644 --- a/packages/docs/sandboxes/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/demo-dockview/src/app.tsx @@ -194,6 +194,11 @@ const LeftControls = (props: IDockviewHeaderActionsProps) => { const PrefixHeaderControls = (props: IDockviewHeaderActionsProps) => { return (

{ + if (props.activePanel) { + props.containerApi.addPopoutGroup(props.activePanel.group); + } + }} className="group-control" style={{ display: 'flex', diff --git a/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx b/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx index 778fe290e..d90e8c5ff 100644 --- a/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx +++ b/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx @@ -255,13 +255,15 @@ const LeftComponent = (props: IDockviewHeaderActionsProps) => { const RightComponent = (props: IDockviewHeaderActionsProps) => { const [floating, setFloating] = React.useState( - 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(); diff --git a/packages/docs/sandboxes/popoutgroup-dockview/package.json b/packages/docs/sandboxes/popoutgroup-dockview/package.json new file mode 100644 index 000000000..9a533bff7 --- /dev/null +++ b/packages/docs/sandboxes/popoutgroup-dockview/package.json @@ -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" + ] +} diff --git a/packages/docs/sandboxes/popoutgroup-dockview/public/index.html b/packages/docs/sandboxes/popoutgroup-dockview/public/index.html new file mode 100644 index 000000000..1f8a52426 --- /dev/null +++ b/packages/docs/sandboxes/popoutgroup-dockview/public/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + React App + + + + +
+ + + + diff --git a/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx b/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx new file mode 100644 index 000000000..4deedf4f6 --- /dev/null +++ b/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx @@ -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 ( +
+ {props.params.title} +
+ ); + }, +}; + +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', + }); + + const panel4 = api.addPanel({ + id: 'panel_4', + component: 'default', + }); + + api.addPanel({ + id: 'panel_5', + component: 'default', + position: { referencePanel: panel4 }, + }); + + api.addPanel({ + id: 'panel_6', + component: 'default', + }); +} + +let panelCount = 0; + +function safeParse(value: any): T | null { + try { + return JSON.parse(value) as T; + } catch (err) { + return null; + } +} + +const useLocalStorage = ( + key: string +): [T | null, (setter: T | null) => void] => { + const [state, setState] = React.useState( + 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(); + const [layout, setLayout] = + useLocalStorage('floating.layout'); + + const [disableFloatingGroups, setDisableFloatingGroups] = + React.useState(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 ( +
+
+ + + +
+
+ +
+
+ ); +}; + +const LeftComponent = (props: IDockviewHeaderActionsProps) => { + const onClick = () => { + props.containerApi.addPanel({ + id: (++panelCount).toString(), + title: `Tab ${panelCount}`, + component: 'default', + position: { referenceGroup: props.group }, + }); + }; + return ( +
+ +
+ ); +}; + +const RightComponent = (props: IDockviewHeaderActionsProps) => { + const [floating, setFloating] = React.useState( + props.api.position === 'popout' + ); + + React.useEffect(() => { + const disposable = props.group.api.onDidRenderPositionChange( + (event) => [setFloating(event.position === 'popout')] + ); + + return () => { + disposable.dispose(); + }; + }, [props.group.api]); + + const onClick = () => { + if (floating) { + const group = props.containerApi.addGroup(); + props.group.api.moveTo({ group }); + } else { + props.containerApi.addPopoutGroup(props.group); + } + }; + + return ( +
+ +
+ ); +}; + +export default DockviewPersistance; + +const Watermark = () => { + return
watermark
; +}; diff --git a/packages/docs/sandboxes/popoutgroup-dockview/src/index.tsx b/packages/docs/sandboxes/popoutgroup-dockview/src/index.tsx new file mode 100644 index 000000000..2fe1be232 --- /dev/null +++ b/packages/docs/sandboxes/popoutgroup-dockview/src/index.tsx @@ -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( + +
+ +
+
+ ); +} diff --git a/packages/docs/sandboxes/popoutgroup-dockview/src/styles.css b/packages/docs/sandboxes/popoutgroup-dockview/src/styles.css new file mode 100644 index 000000000..92b6a1b36 --- /dev/null +++ b/packages/docs/sandboxes/popoutgroup-dockview/src/styles.css @@ -0,0 +1,16 @@ +body { + margin: 0px; + color: white; + font-family: sans-serif; + text-align: center; +} + +#root { + height: 100vh; + width: 100vw; +} + +.app { + height: 100%; + +} diff --git a/packages/docs/sandboxes/popoutgroup-dockview/src/utils.tsx b/packages/docs/sandboxes/popoutgroup-dockview/src/utils.tsx new file mode 100644 index 000000000..457128d67 --- /dev/null +++ b/packages/docs/sandboxes/popoutgroup-dockview/src/utils.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; + +export const Icon = (props: { + icon: string; + title?: string; + onClick?: (event: React.MouseEvent) => void; +}) => { + return ( +
+ + {props.icon} + +
+ ); +}; diff --git a/packages/docs/sandboxes/popoutgroup-dockview/tsconfig.json b/packages/docs/sandboxes/popoutgroup-dockview/tsconfig.json new file mode 100644 index 000000000..cdc4fb5f5 --- /dev/null +++ b/packages/docs/sandboxes/popoutgroup-dockview/tsconfig.json @@ -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 + } +} diff --git a/packages/docs/src/pages/popout.tsx b/packages/docs/src/pages/popout.tsx new file mode 100644 index 000000000..d203a94ea --- /dev/null +++ b/packages/docs/src/pages/popout.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function Popout() { + return
; +} From ff6d40a5459e322b5a66c3cf95196e328880b1ad Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Mon, 1 Jan 2024 21:40:53 +0000 Subject: [PATCH 2/3] feat: indicate focused tab with css style --- .../dockview/components/tab/defaultTab.scss | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/dockview-core/src/dockview/components/tab/defaultTab.scss b/packages/dockview-core/src/dockview/components/tab/defaultTab.scss index d820534b8..2a1e58734 100644 --- a/packages/dockview-core/src/dockview/components/tab/defaultTab.scss +++ b/packages/dockview-core/src/dockview/components/tab/defaultTab.scss @@ -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); From 406af8a87f5afce127dab18891cf2c7b69956bf6 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Tue, 26 Dec 2023 20:36:49 +0000 Subject: [PATCH 3/3] feat: popout windows --- .../dockview-core/src/api/component.api.ts | 5 +- .../dockview/components/tab/defaultTab.scss | 18 ----- .../src/dockview/dockviewComponent.ts | 74 +++++++------------ .../src/dockview/dockviewPopoutGroupPanel.ts | 43 +++++++++++ packages/dockview-core/src/popoutWindow.ts | 2 +- packages/docs/docs/components/dockview.mdx | 18 ++++- .../docs/sandboxes/demo-dockview/src/app.tsx | 5 -- .../popoutgroup-dockview/src/app.tsx | 14 ++-- packages/docs/src/generated/api.output.json | 30 ++++++++ 9 files changed, 128 insertions(+), 81 deletions(-) create mode 100644 packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index fc8d36334..2067824e4 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -806,11 +806,14 @@ export class DockviewApi implements CommonApi { this.component.moveToPrevious(options); } + /** + * Add a popout group in a new Window + */ addPopoutGroup( item: IDockviewPanel | DockviewGroupPanel, options?: { - skipRemoveGroup?: boolean; position?: Box; + popoutUrl?: string; } ): void { this.component.addPopoutGroup(item, options); diff --git a/packages/dockview-core/src/dockview/components/tab/defaultTab.scss b/packages/dockview-core/src/dockview/components/tab/defaultTab.scss index 2a1e58734..d820534b8 100644 --- a/packages/dockview-core/src/dockview/components/tab/defaultTab.scss +++ b/packages/dockview-core/src/dockview/components/tab/defaultTab.scss @@ -9,24 +9,6 @@ .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); diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index d9db6d830..6460244b4 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -58,6 +58,7 @@ import { GreadyRenderContainer, DockviewPanelRenderer, } from './components/greadyRenderContainer'; +import { DockviewPopoutGroupPanel } from './dockviewPopoutGroupPanel'; function getTheme(element: HTMLElement): string | undefined { function toClassList(element: HTMLElement) { @@ -224,12 +225,6 @@ export interface DockviewDropEvent extends GroupviewDropEvent { group: DockviewGroupPanel | null; } -export interface DockviewPopoutGroupPanel { - window: PopoutWindow; - disposable: IDisposable; - group: DockviewGroupPanel; -} - export interface IDockviewComponent extends IBaseGrid { readonly activePanel: IDockviewPanel | undefined; readonly totalPanels: number; @@ -277,8 +272,8 @@ export interface IDockviewComponent extends IBaseGrid { addPopoutGroup( item: IDockviewPanel | DockviewGroupPanel, options?: { - skipRemoveGroup?: boolean; position?: Box; + popoutUrl?: string; } ): void; } @@ -495,11 +490,10 @@ export class DockviewComponent options?: { skipRemoveGroup?: boolean; position?: Box; + popoutUrl?: string; } ): void { let group: DockviewGroupPanel; - const theme = getTheme(this.gridview.element); - let box: Box | undefined = options?.position; if (item instanceof DockviewPanel) { @@ -531,42 +525,31 @@ export class DockviewComponent } } - // const { top: boundingTop, left: boundingLeft } = - // this.element.getBoundingClientRect(); + const theme = getTheme(this.gridview.element); - const window = new PopoutWindow('test', theme ?? '', { - url: this.options.popoutUrl ?? 'popout.html', - left: box.left, - top: box.top, - width: box.width, - height: box.height, + 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, + }, }); - const disposable = new CompositeDisposable(); - const wrappedWindow = { window, disposable, group }; - - disposable.addDisposables( - window.onDidClose(() => { - group.model.location = 'grid'; - - remove(this._popoutGroups, wrappedWindow); - - this.doAddGroup(group, [0]); - }), + popoutWindow.addDisposables( { dispose: () => { - group.model.location = 'grid'; - remove(this._popoutGroups, wrappedWindow); + remove(this._popoutGroups, popoutWindow); }, }, - window + popoutWindow.window.onDidClose(() => { + this.doAddGroup(group, [0]); + }) ); - group.model.location = 'popout'; - - this._popoutGroups.push(wrappedWindow); - - window.open(group.element); + this._popoutGroups.push(popoutWindow); } addFloatingGroup( @@ -1395,7 +1378,7 @@ export class DockviewComponent this._onDidRemoveGroup.fire(group); } - selectedGroup.disposable.dispose(); + selectedGroup.dispose(); if (!options?.skipActive && this._activeGroup === group) { const groups = Array.from(this._groups.values()); @@ -1539,10 +1522,6 @@ export class DockviewComponent }); } } else { - const floatingGroup = this._floatingGroups.find( - (x) => x.group === sourceGroup - ); - switch (sourceGroup.api.location) { case 'grid': this.gridview.removeView( @@ -1550,23 +1529,22 @@ export class DockviewComponent ); break; case 'floating': - const floatingGroup = this._floatingGroups.find( + const selectedFloatingGroup = this._floatingGroups.find( (x) => x.group === sourceGroup ); - if (!floatingGroup) { + if (!selectedFloatingGroup) { throw new Error('failed to find floating group'); } - floatingGroup.dispose(); + selectedFloatingGroup.dispose(); break; case 'popout': - const selectedGroup = this._popoutGroups.find( + const selectedPopoutGroup = this._popoutGroups.find( (x) => x.group === sourceGroup ); - if (!selectedGroup) { + if (!selectedPopoutGroup) { throw new Error('failed to find popout group'); } - selectedGroup.disposable.dispose(); - selectedGroup.window.dispose(); + selectedPopoutGroup.dispose(); } const referenceLocation = getGridLocation( diff --git a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts new file mode 100644 index 000000000..3052cb6ff --- /dev/null +++ b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts @@ -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); + } +} diff --git a/packages/dockview-core/src/popoutWindow.ts b/packages/dockview-core/src/popoutWindow.ts index 2fb14fa7d..2994e44ec 100644 --- a/packages/dockview-core/src/popoutWindow.ts +++ b/packages/dockview-core/src/popoutWindow.ts @@ -84,7 +84,7 @@ export class PopoutWindow extends CompositeDisposable { }; // prevent any default content from loading - externalWindow.document.body.replaceWith(document.createElement('div')); + // externalWindow.document.body.replaceWith(document.createElement('div')); disposable.addDisposables( addDisposableWindowListener(window, 'beforeunload', () => { diff --git a/packages/docs/docs/components/dockview.mdx b/packages/docs/docs/components/dockview.mdx index db5f14a23..d58ceb062 100644 --- a/packages/docs/docs/components/dockview.mdx +++ b/packages/docs/docs/components/dockview.mdx @@ -371,7 +371,23 @@ You can control the bounding box of floating groups through the optional `floati react={DockviewFloating} /> -## Popout Window +## 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); +``` diff --git a/packages/docs/sandboxes/demo-dockview/src/app.tsx b/packages/docs/sandboxes/demo-dockview/src/app.tsx index e420a0ddc..6ea9ac80c 100644 --- a/packages/docs/sandboxes/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/demo-dockview/src/app.tsx @@ -250,11 +250,6 @@ const LeftControls = (props: IDockviewHeaderActionsProps) => { const PrefixHeaderControls = (props: IDockviewHeaderActionsProps) => { return (
{ - if (props.activePanel) { - props.containerApi.addPopoutGroup(props.activePanel.group); - } - }} className="group-control" style={{ display: 'flex', diff --git a/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx b/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx index 4deedf4f6..1d16e0029 100644 --- a/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx +++ b/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx @@ -50,7 +50,7 @@ function loadDefaultLayout(api: DockviewApi) { component: 'default', }); - const panel4 = api.addPanel({ + api.addPanel({ id: 'panel_4', component: 'default', }); @@ -58,7 +58,7 @@ function loadDefaultLayout(api: DockviewApi) { api.addPanel({ id: 'panel_5', component: 'default', - position: { referencePanel: panel4 }, + position: { direction: 'right' }, }); api.addPanel({ @@ -213,13 +213,13 @@ const LeftComponent = (props: IDockviewHeaderActionsProps) => { }; const RightComponent = (props: IDockviewHeaderActionsProps) => { - const [floating, setFloating] = React.useState( - props.api.position === 'popout' + const [popout, setPopout] = React.useState( + props.api.location === 'popout' ); React.useEffect(() => { const disposable = props.group.api.onDidRenderPositionChange( - (event) => [setFloating(event.position === 'popout')] + (event) => [setPopout(event.location === 'popout')] ); return () => { @@ -228,7 +228,7 @@ const RightComponent = (props: IDockviewHeaderActionsProps) => { }, [props.group.api]); const onClick = () => { - if (floating) { + if (popout) { const group = props.containerApi.addGroup(); props.group.api.moveTo({ group }); } else { @@ -240,7 +240,7 @@ const RightComponent = (props: IDockviewHeaderActionsProps) => {
); diff --git a/packages/docs/src/generated/api.output.json b/packages/docs/src/generated/api.output.json index 4c3fdca77..d5d9a667f 100644 --- a/packages/docs/src/generated/api.output.json +++ b/packages/docs/src/generated/api.output.json @@ -359,6 +359,11 @@ "signature": "(options: AddPanelOptions): IDockviewPanel", "type": "method" }, + { + "name": "addPopoutGroup", + "signature": "(item: IDockviewPanel | DockviewGroupPanel, options?: { position: Box, skipRemoveGroup: boolean }): void", + "type": "method" + }, { "name": "clear", "comment": { @@ -1525,11 +1530,21 @@ "signature": "Event", "type": "property" }, + { + "name": "onDidRendererChange", + "signature": "Event", + "type": "property" + }, { "name": "onDidVisibilityChange", "signature": "Event", "type": "property" }, + { + "name": "renderer", + "signature": "DockviewPanelRenderer", + "type": "property" + }, { "name": "title", "signature": "string | undefined", @@ -1563,6 +1578,11 @@ "signature": "(): void", "type": "method" }, + { + "name": "setRenderer", + "signature": "(renderer: DockviewPanelRenderer): void", + "type": "method" + }, { "name": "setSize", "signature": "(event: SizeEvent): void", @@ -2005,6 +2025,16 @@ "signature": "PanelCollection>", "type": "property" }, + { + "name": "debug", + "signature": "boolean", + "type": "property" + }, + { + "name": "defaultRenderer", + "signature": "DockviewPanelRenderer", + "type": "property" + }, { "name": "defaultTabComponent", "signature": "FunctionComponent>",