From 307780d15ae6c585d4f4796a95d3f5528ae5c5e4 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sat, 8 Jul 2023 14:44:01 +0100 Subject: [PATCH] feat: add additional methods and docs for floating groups --- .../__tests__/api/dockviewPanelApi.spec.ts | 18 +- .../__tests__/dnd/groupDragHandler.spec.ts | 6 +- .../src/__tests__/dnd/overlay.spec.ts | 42 +- .../components/titlebar/tabsContainer.spec.ts | 18 +- .../dockview/dockviewComponent.spec.ts | 216 +++++------ .../dockview-core/src/__tests__/dom.spec.ts | 21 + .../dockview-core/src/api/component.api.ts | 9 +- .../src/api/dockviewGroupPanelApi.ts | 54 +++ .../dockview-core/src/api/dockviewPanelApi.ts | 31 +- .../dockview-core/src/dnd/groupDragHandler.ts | 12 +- packages/dockview-core/src/dnd/overlay.ts | 359 ++++++++++-------- .../components/titlebar/tabsContainer.ts | 26 +- .../src/dockview/deserializer.ts | 4 +- .../src/dockview/dockviewComponent.scss | 1 + .../src/dockview/dockviewComponent.ts | 76 ++-- .../dockview/dockviewFloatingGroupPanel.ts | 37 ++ .../src/dockview/dockviewGroupPanel.ts | 31 +- .../src/dockview/dockviewGroupPanelModel.ts | 4 + .../src/dockview/dockviewPanel.ts | 6 +- .../dockview-core/src/dockview/options.ts | 6 +- .../src/gridview/gridviewPanel.ts | 15 +- packages/dockview-core/src/index.ts | 6 +- .../src/splitview/splitview.scss | 37 +- packages/docs/docs/components/dockview.mdx | 17 +- .../docs/sandboxes/demo-dockview/src/app.tsx | 31 +- .../floatinggroup-dockview/src/app.tsx | 53 ++- .../floatinggroup-dockview/src/utils.tsx | 30 ++ .../sandboxes/simple-dockview/src/app.tsx | 4 +- packages/docs/src/components/ui/container.tsx | 30 +- 29 files changed, 772 insertions(+), 428 deletions(-) create mode 100644 packages/dockview-core/src/__tests__/dom.spec.ts create mode 100644 packages/dockview-core/src/api/dockviewGroupPanelApi.ts create mode 100644 packages/dockview-core/src/dockview/dockviewFloatingGroupPanel.ts create mode 100644 packages/docs/sandboxes/floatinggroup-dockview/src/utils.tsx diff --git a/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts b/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts index 1226b6cb2..475146b5f 100644 --- a/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts +++ b/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts @@ -5,6 +5,12 @@ import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; describe('groupPanelApi', () => { test('title', () => { + const accessor: Partial = { + onDidAddPanel: jest.fn(), + onDidRemovePanel: jest.fn(), + options: {}, + }; + const panelMock = jest.fn(() => { return { update: jest.fn(), @@ -18,7 +24,11 @@ describe('groupPanelApi', () => { const panel = new panelMock(); const group = new groupMock(); - const cut = new DockviewPanelApiImpl(panel, group); + const cut = new DockviewPanelApiImpl( + panel, + group, + accessor + ); cut.setTitle('test_title'); expect(panel.setTitle).toBeCalledTimes(1); @@ -44,7 +54,8 @@ describe('groupPanelApi', () => { const cut = new DockviewPanelApiImpl( groupPanel, - groupViewPanel + groupViewPanel, + accessor ); cut.updateParameters({ keyA: 'valueA' }); @@ -73,7 +84,8 @@ describe('groupPanelApi', () => { const cut = new DockviewPanelApiImpl( groupPanel, - groupViewPanel + groupViewPanel, + accessor ); let events = 0; diff --git a/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts b/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts index 36497e5de..b597f2fa5 100644 --- a/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts +++ b/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts @@ -10,7 +10,7 @@ describe('groupDragHandler', () => { const groupMock = jest.fn(() => { const partial: Partial = { id: 'test_group_id', - isFloating: false, + api: { isFloating: false } as any, }; return partial as DockviewGroupPanel; }); @@ -48,7 +48,7 @@ describe('groupDragHandler', () => { const groupMock = jest.fn(() => { const partial: Partial = { - isFloating: true, + api: { isFloating: true } as any, }; return partial as DockviewGroupPanel; }); @@ -76,7 +76,7 @@ describe('groupDragHandler', () => { const groupMock = jest.fn(() => { const partial: Partial = { - isFloating: false, + api: { isFloating: false } as any, }; return partial as DockviewGroupPanel; }); diff --git a/packages/dockview-core/src/__tests__/dnd/overlay.spec.ts b/packages/dockview-core/src/__tests__/dnd/overlay.spec.ts index 1d65d90ad..edc661a12 100644 --- a/packages/dockview-core/src/__tests__/dnd/overlay.spec.ts +++ b/packages/dockview-core/src/__tests__/dnd/overlay.spec.ts @@ -39,7 +39,7 @@ describe('overlay', () => { }); }); - test('#1', () => { + test('that out-of-bounds dimensions are fixed', () => { const container = document.createElement('div'); const content = document.createElement('div'); @@ -77,6 +77,46 @@ describe('overlay', () => { }); }); + test('setBounds', () => { + const container = document.createElement('div'); + const content = document.createElement('div'); + + document.body.appendChild(container); + container.appendChild(content); + + const cut = new Overlay({ + height: 1000, + width: 1000, + left: 0, + top: 0, + minimumInViewportWidth: 0, + minimumInViewportHeight: 0, + container, + content, + }); + + const element: HTMLElement = container.querySelector( + '.dv-resize-container' + )!; + expect(element).toBeTruthy(); + + jest.spyOn(element, 'getBoundingClientRect').mockImplementation(() => { + return { left: 300, top: 400, width: 1000, height: 1000 } as any; + }); + jest.spyOn(container, 'getBoundingClientRect').mockImplementation( + () => { + return { left: 0, top: 0, width: 1000, height: 1000 } as any; + } + ); + + cut.setBounds({ height: 100, width: 200, left: 300, top: 400 }); + + expect(element.style.height).toBe('100px'); + expect(element.style.width).toBe('200px'); + expect(element.style.left).toBe('300px'); + expect(element.style.top).toBe('400px'); + }); + test('that the resize handles are added', () => { const container = document.createElement('div'); const content = document.createElement('div'); 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 7384dc100..5514500b3 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 (>{ - isFloating: false, + api: { isFloating: false } as any, }) as DockviewGroupPanel; }); @@ -506,10 +506,14 @@ describe('tabsContainer', () => { const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault'); fireEvent(container, event); - expect(accessor.addFloatingGroup).toBeCalledWith(groupPanel, { - x: 100, - y: 60, - }); + expect(accessor.addFloatingGroup).toBeCalledWith( + groupPanel, + { + x: 100, + y: 60, + }, + { inDragMode: true } + ); expect(accessor.addFloatingGroup).toBeCalledTimes(1); expect(eventPreventDefaultSpy).toBeCalledTimes(1); @@ -534,7 +538,7 @@ describe('tabsContainer', () => { const groupPanelMock = jest.fn(() => { return (>{ - isFloating: true, + api: { isFloating: true } as any, }) as DockviewGroupPanel; }); @@ -587,7 +591,7 @@ describe('tabsContainer', () => { const groupPanelMock = jest.fn(() => { return (>{ - isFloating: true, + api: { isFloating: true } 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 626bb66bd..778df49d6 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -2795,8 +2795,8 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); @@ -2807,8 +2807,8 @@ describe('dockviewComponent', () => { 'right' ); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeFalsy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); }); @@ -2840,8 +2840,8 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); @@ -2852,8 +2852,8 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeFalsy(); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); }); @@ -2891,9 +2891,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeTruthy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); @@ -2904,9 +2904,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeTruthy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -2944,9 +2944,9 @@ describe('dockviewComponent', () => { position: { referencePanel: panel2 }, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeTruthy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -2957,9 +2957,9 @@ describe('dockviewComponent', () => { 'right' ); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeFalsy(); - expect(panel3.group.model.isFloating).toBeFalsy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeFalsy(); + expect(panel3.group.api.isFloating).toBeFalsy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -2997,9 +2997,9 @@ describe('dockviewComponent', () => { position: { referencePanel: panel2 }, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeTruthy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -3010,9 +3010,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeFalsy(); - expect(panel3.group.model.isFloating).toBeFalsy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeFalsy(); + expect(panel3.group.api.isFloating).toBeFalsy(); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(3); }); @@ -3056,10 +3056,10 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); - expect(panel3.group.model.isFloating).toBeTruthy(); - expect(panel4.group.model.isFloating).toBeTruthy(); + 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(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(4); @@ -3070,10 +3070,10 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); - expect(panel3.group.model.isFloating).toBeTruthy(); - expect(panel4.group.model.isFloating).toBeTruthy(); + 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(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(4); }); @@ -3105,8 +3105,8 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); @@ -3117,8 +3117,8 @@ describe('dockviewComponent', () => { 'right' ); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeFalsy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); }); @@ -3150,8 +3150,8 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); @@ -3162,8 +3162,8 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeFalsy(); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); }); @@ -3201,9 +3201,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeTruthy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); @@ -3214,9 +3214,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeTruthy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -3254,9 +3254,9 @@ describe('dockviewComponent', () => { position: { referencePanel: panel2 }, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeTruthy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -3267,9 +3267,9 @@ describe('dockviewComponent', () => { 'right' ); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeFalsy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeFalsy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); }); @@ -3307,9 +3307,9 @@ describe('dockviewComponent', () => { position: { referencePanel: panel2 }, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeTruthy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -3320,9 +3320,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeFalsy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeFalsy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -3366,10 +3366,10 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); - expect(panel3.group.model.isFloating).toBeTruthy(); - expect(panel4.group.model.isFloating).toBeTruthy(); + 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(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(4); @@ -3380,10 +3380,10 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); - expect(panel3.group.model.isFloating).toBeTruthy(); - expect(panel4.group.model.isFloating).toBeTruthy(); + 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(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(4); }); @@ -3421,9 +3421,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeFalsy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeFalsy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); @@ -3434,9 +3434,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.model.isFloating).toBeTruthy(); - expect(panel2.group.model.isFloating).toBeFalsy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeTruthy(); + expect(panel2.group.api.isFloating).toBeFalsy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -3473,9 +3473,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeFalsy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeFalsy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -3486,9 +3486,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.model.isFloating).toBeTruthy(); - expect(panel2.group.model.isFloating).toBeFalsy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeTruthy(); + expect(panel2.group.api.isFloating).toBeFalsy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -3526,9 +3526,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeFalsy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeFalsy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); @@ -3539,9 +3539,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.model.isFloating).toBeTruthy(); - expect(panel2.group.model.isFloating).toBeFalsy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeTruthy(); + expect(panel2.group.api.isFloating).toBeFalsy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -3578,9 +3578,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeFalsy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeFalsy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -3591,9 +3591,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.model.isFloating).toBeTruthy(); - expect(panel2.group.model.isFloating).toBeTruthy(); - expect(panel3.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeTruthy(); + expect(panel2.group.api.isFloating).toBeTruthy(); + expect(panel3.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(3); }); @@ -3625,15 +3625,15 @@ describe('dockviewComponent', () => { position: { direction: 'right' }, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeFalsy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); dockview.addFloatingGroup(panel2); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); }); @@ -3664,15 +3664,15 @@ describe('dockviewComponent', () => { component: 'default', }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeFalsy(); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); dockview.addFloatingGroup(panel2); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); }); @@ -3704,15 +3704,15 @@ describe('dockviewComponent', () => { position: { direction: 'right' }, }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeFalsy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); dockview.addFloatingGroup(panel2.group); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); }); @@ -3743,15 +3743,15 @@ describe('dockviewComponent', () => { component: 'default', }); - expect(panel1.group.model.isFloating).toBeFalsy(); - expect(panel2.group.model.isFloating).toBeFalsy(); + expect(panel1.group.api.isFloating).toBeFalsy(); + expect(panel2.group.api.isFloating).toBeFalsy(); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); dockview.addFloatingGroup(panel2.group); - expect(panel1.group.model.isFloating).toBeTruthy(); - expect(panel2.group.model.isFloating).toBeTruthy(); + expect(panel1.group.api.isFloating).toBeTruthy(); + expect(panel2.group.api.isFloating).toBeTruthy(); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); }); diff --git a/packages/dockview-core/src/__tests__/dom.spec.ts b/packages/dockview-core/src/__tests__/dom.spec.ts new file mode 100644 index 000000000..f628d5904 --- /dev/null +++ b/packages/dockview-core/src/__tests__/dom.spec.ts @@ -0,0 +1,21 @@ +import { quasiDefaultPrevented, quasiPreventDefault } from '../dom'; + +describe('dom', () => { + test('quasiPreventDefault', () => { + const event = new Event('myevent'); + expect((event as any)['dv-quasiPreventDefault']).toBeUndefined(); + quasiPreventDefault(event); + expect((event as any)['dv-quasiPreventDefault']).toBe(true); + }); + + test('quasiDefaultPrevented', () => { + const event = new Event('myevent'); + expect(quasiDefaultPrevented(event)).toBeFalsy(); + + (event as any)['dv-quasiPreventDefault'] = false; + expect(quasiDefaultPrevented(event)).toBeFalsy(); + + (event as any)['dv-quasiPreventDefault'] = true; + expect(quasiDefaultPrevented(event)).toBeTruthy(); + }); +}); diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index d527ef6af..e67fc1b62 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -435,7 +435,7 @@ export class DockviewApi implements CommonApi { return this.component.addPanel(options); } - addGroup(options?: AddGroupOptions): IDockviewGroupPanel { + addGroup(options?: AddGroupOptions): DockviewGroupPanel { return this.component.addGroup(options); } @@ -459,6 +459,13 @@ export class DockviewApi implements CommonApi { return this.component.getPanel(id); } + addFloatingGroup( + item: IDockviewPanel | DockviewGroupPanel, + coord?: { x: number; y: number } + ): void { + return this.component.addFloatingGroup(item, coord); + } + fromJSON(data: SerializedDockview): void { this.component.fromJSON(data); } diff --git a/packages/dockview-core/src/api/dockviewGroupPanelApi.ts b/packages/dockview-core/src/api/dockviewGroupPanelApi.ts new file mode 100644 index 000000000..65154b961 --- /dev/null +++ b/packages/dockview-core/src/api/dockviewGroupPanelApi.ts @@ -0,0 +1,54 @@ +import { Position } from '../dnd/droptarget'; +import { DockviewComponent } from '../dockview/dockviewComponent'; +import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel'; +import { Emitter, Event } from '../events'; +import { GridviewPanelApi, GridviewPanelApiImpl } from './gridviewPanelApi'; + +export interface DockviewGroupPanelApi extends GridviewPanelApi { + readonly onDidFloatingStateChange: Event; + readonly isFloating: boolean; + moveTo(options: { group: DockviewGroupPanel; position?: Position }): void; +} + +export interface DockviewGroupPanelFloatingChangeEvent { + readonly isFloating: boolean; +} + +export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl { + private _group: DockviewGroupPanel | undefined; + + readonly _onDidFloatingStateChange = + new Emitter(); + readonly onDidFloatingStateChange: Event = + this._onDidFloatingStateChange.event; + + get isFloating() { + if (!this._group) { + throw new Error(`DockviewGroupPanelApiImpl not initialized`); + } + return this._group.model.isFloating; + } + + constructor(id: string, private readonly accessor: DockviewComponent) { + super(id); + + this.addDisposables(this._onDidFloatingStateChange); + } + + moveTo(options: { group: DockviewGroupPanel; position?: Position }): void { + if (!this._group) { + throw new Error(`DockviewGroupPanelApiImpl not initialized`); + } + + this.accessor.moveGroupOrPanel( + options.group, + this._group.id, + undefined, + options.position ?? 'center' + ); + } + + initialize(group: DockviewGroupPanel): void { + this._group = group; + } +} diff --git a/packages/dockview-core/src/api/dockviewPanelApi.ts b/packages/dockview-core/src/api/dockviewPanelApi.ts index 5c6222071..e724c251d 100644 --- a/packages/dockview-core/src/api/dockviewPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewPanelApi.ts @@ -3,6 +3,8 @@ import { GridviewPanelApiImpl, GridviewPanelApi } from './gridviewPanelApi'; import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel'; import { MutableDisposable } from '../lifecycle'; import { IDockviewPanel } from '../dockview/dockviewPanel'; +import { DockviewComponent } from '../dockview/dockviewComponent'; +import { Position } from '../dnd/droptarget'; export interface TitleEvent { readonly title: string; @@ -24,6 +26,11 @@ export interface DockviewPanelApi readonly onDidGroupChange: Event; close(): void; setTitle(title: string): void; + moveTo(options: { + group: DockviewGroupPanel; + position?: Position; + index?: number; + }): void; } export class DockviewPanelApiImpl @@ -73,7 +80,11 @@ export class DockviewPanelApiImpl return this._group; } - constructor(private panel: IDockviewPanel, group: DockviewGroupPanel) { + constructor( + private panel: IDockviewPanel, + group: DockviewGroupPanel, + private readonly accessor: DockviewComponent + ) { super(panel.id); this.initialize(panel); @@ -88,11 +99,25 @@ export class DockviewPanelApiImpl ); } - public setTitle(title: string): void { + moveTo(options: { + group: DockviewGroupPanel; + position?: Position; + index?: number; + }): void { + this.accessor.moveGroupOrPanel( + options.group, + this._group.id, + this.panel.id, + options.position ?? 'center', + options.index + ); + } + + setTitle(title: string): void { this.panel.setTitle(title); } - public close(): void { + close(): void { this.group.model.closePanel(this.panel); } } diff --git a/packages/dockview-core/src/dnd/groupDragHandler.ts b/packages/dockview-core/src/dnd/groupDragHandler.ts index 794118498..0a6f55008 100644 --- a/packages/dockview-core/src/dnd/groupDragHandler.ts +++ b/packages/dockview-core/src/dnd/groupDragHandler.ts @@ -23,11 +23,11 @@ export class GroupDragHandler extends DragHandler { 'mousedown', (e) => { if (e.shiftKey) { - /** - * You cannot call e.preventDefault() because that will prevent drag events from firing - * but we also need to stop any group overlay drag events from occuring - * Use a custom event marker that can be checked by the overlay drag events - */ + /** + * You cannot call e.preventDefault() because that will prevent drag events from firing + * but we also need to stop any group overlay drag events from occuring + * Use a custom event marker that can be checked by the overlay drag events + */ quasiPreventDefault(e); } }, @@ -37,7 +37,7 @@ export class GroupDragHandler extends DragHandler { } override isCancelled(_event: DragEvent): boolean { - if (this.group.isFloating && !_event.shiftKey) { + if (this.group.api.isFloating && !_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 16ac21558..6d818725a 100644 --- a/packages/dockview-core/src/dnd/overlay.ts +++ b/packages/dockview-core/src/dnd/overlay.ts @@ -7,7 +7,6 @@ import { } from '../events'; import { CompositeDisposable, MutableDisposable } from '../lifecycle'; import { clamp } from '../math'; -import { getPaneData, getPanelData } from './dataTransfer'; const bringElementToFront = (() => { let previous: HTMLElement | null = null; @@ -66,6 +65,30 @@ export class Overlay extends CompositeDisposable { this.renderWithinBoundaryConditions(); } + setBounds( + bounds: Partial<{ + height: number; + width: number; + top: number; + left: number; + }> + ): void { + if (typeof bounds.height === 'number') { + this._element.style.height = `${bounds.height}px`; + } + if (typeof bounds.width === 'number') { + this._element.style.width = `${bounds.width}px`; + } + if (typeof bounds.top === 'number') { + this._element.style.top = `${bounds.top}px`; + } + if (typeof bounds.left === 'number') { + this._element.style.left = `${bounds.left}px`; + } + + this.renderWithinBoundaryConditions(); + } + toJSON(): { top: number; left: number; height: number; width: number } { const container = this.options.container.getBoundingClientRect(); const element = this._element.getBoundingClientRect(); @@ -78,6 +101,173 @@ export class Overlay extends CompositeDisposable { }; } + renderWithinBoundaryConditions(): void { + const containerRect = this.options.container.getBoundingClientRect(); + const overlayRect = this._element.getBoundingClientRect(); + + // a minimum width of minimumViewportWidth must be inside the viewport + const xOffset = Math.max( + 0, + overlayRect.width - this.options.minimumInViewportWidth + ); + + // a minimum height of minimumViewportHeight must be inside the viewport + const yOffset = Math.max( + 0, + overlayRect.height - this.options.minimumInViewportHeight + ); + + const left = clamp( + overlayRect.left - containerRect.left, + -xOffset, + Math.max(0, containerRect.width - overlayRect.width + xOffset) + ); + + const top = clamp( + overlayRect.top - containerRect.top, + -yOffset, + Math.max(0, containerRect.height - overlayRect.height + yOffset) + ); + + this._element.style.left = `${left}px`; + this._element.style.top = `${top}px`; + } + + setupDrag( + dragTarget: HTMLElement, + options: { inDragMode: boolean } = { inDragMode: false } + ): void { + const move = new MutableDisposable(); + + const track = () => { + let offset: { x: number; y: number } | null = null; + + move.value = new CompositeDisposable( + addDisposableWindowListener(window, 'mousemove', (e) => { + const containerRect = + this.options.container.getBoundingClientRect(); + const x = e.clientX - containerRect.left; + const y = e.clientY - containerRect.top; + + toggleClass( + this._element, + 'dv-resize-container-dragging', + true + ); + + const overlayRect = this._element.getBoundingClientRect(); + if (offset === null) { + offset = { + x: e.clientX - overlayRect.left, + y: e.clientY - overlayRect.top, + }; + } + + const xOffset = Math.max( + 0, + overlayRect.width - this.options.minimumInViewportWidth + ); + const yOffset = Math.max( + 0, + overlayRect.height - + this.options.minimumInViewportHeight + ); + + const left = clamp( + x - offset.x, + -xOffset, + Math.max( + 0, + containerRect.width - overlayRect.width + xOffset + ) + ); + + const top = clamp( + y - offset.y, + -yOffset, + Math.max( + 0, + containerRect.height - overlayRect.height + yOffset + ) + ); + + this._element.style.left = `${left}px`; + this._element.style.top = `${top}px`; + }), + addDisposableWindowListener(window, 'mouseup', () => { + toggleClass( + this._element, + 'dv-resize-container-dragging', + false + ); + + move.dispose(); + this._onDidChange.fire(); + }) + ); + }; + + this.addDisposables( + move, + addDisposableListener(dragTarget, 'mousedown', (event) => { + if (event.defaultPrevented) { + event.preventDefault(); + return; + } + + // if somebody has marked this event then treat as a defaultPrevented + // without actually calling event.preventDefault() + if (quasiDefaultPrevented(event)) { + return; + } + + track(); + }), + addDisposableListener( + this.options.content, + 'mousedown', + (event) => { + if (event.defaultPrevented) { + return; + } + + // if somebody has marked this event then treat as a defaultPrevented + // without actually calling event.preventDefault() + if (quasiDefaultPrevented(event)) { + return; + } + + if (event.shiftKey) { + track(); + } + } + ), + addDisposableListener( + this.options.content, + 'mousedown', + () => { + bringElementToFront(this._element); + }, + true + ) + ); + + bringElementToFront(this._element); + + if (options.inDragMode) { + track(); + } + } + + private setupOverlay(): void { + this._element.style.height = `${this.options.height}px`; + this._element.style.width = `${this.options.width}px`; + this._element.style.left = `${this.options.left}px`; + this._element.style.top = `${this.options.top}px`; + + this._element.className = 'dv-resize-container'; + } + private setupResize( direction: | 'top' @@ -252,173 +442,6 @@ export class Overlay extends CompositeDisposable { ); } - private setupOverlay(): void { - this._element.style.height = `${this.options.height}px`; - this._element.style.width = `${this.options.width}px`; - this._element.style.left = `${this.options.left}px`; - this._element.style.top = `${this.options.top}px`; - - this._element.className = 'dv-resize-container'; - } - - setupDrag( - dragTarget: HTMLElement, - options: { inDragMode: boolean } = { inDragMode: false } - ): void { - const move = new MutableDisposable(); - - const track = () => { - let offset: { x: number; y: number } | null = null; - - move.value = new CompositeDisposable( - addDisposableWindowListener(window, 'mousemove', (e) => { - const containerRect = - this.options.container.getBoundingClientRect(); - const x = e.clientX - containerRect.left; - const y = e.clientY - containerRect.top; - - toggleClass( - this._element, - 'dv-resize-container-dragging', - true - ); - - const overlayRect = this._element.getBoundingClientRect(); - if (offset === null) { - offset = { - x: e.clientX - overlayRect.left, - y: e.clientY - overlayRect.top, - }; - } - - const xOffset = Math.max( - 0, - overlayRect.width - this.options.minimumInViewportWidth - ); - const yOffset = Math.max( - 0, - overlayRect.height - - this.options.minimumInViewportHeight - ); - - const left = clamp( - x - offset.x, - -xOffset, - Math.max( - 0, - containerRect.width - overlayRect.width + xOffset - ) - ); - - const top = clamp( - y - offset.y, - -yOffset, - Math.max( - 0, - containerRect.height - overlayRect.height + yOffset - ) - ); - - this._element.style.left = `${left}px`; - this._element.style.top = `${top}px`; - }), - addDisposableWindowListener(window, 'mouseup', () => { - toggleClass( - this._element, - 'dv-resize-container-dragging', - false - ); - - move.dispose(); - this._onDidChange.fire(); - }) - ); - }; - - this.addDisposables( - move, - addDisposableListener(dragTarget, 'mousedown', (event) => { - if (event.defaultPrevented) { - event.preventDefault(); - return; - } - - // if somebody has marked this event then treat as a defaultPrevented - // without actually calling event.preventDefault() - if (quasiDefaultPrevented(event)) { - return; - } - - track(); - }), - addDisposableListener( - this.options.content, - 'mousedown', - (event) => { - if (event.defaultPrevented) { - return; - } - - // if somebody has marked this event then treat as a defaultPrevented - // without actually calling event.preventDefault() - if (quasiDefaultPrevented(event)) { - return; - } - - if (event.shiftKey) { - track(); - } - } - ), - addDisposableListener( - this.options.content, - 'mousedown', - () => { - bringElementToFront(this._element); - }, - true - ) - ); - - bringElementToFront(this._element); - - if (options.inDragMode) { - track(); - } - } - - renderWithinBoundaryConditions(): void { - const containerRect = this.options.container.getBoundingClientRect(); - const overlayRect = this._element.getBoundingClientRect(); - - // a minimum width of minimumViewportWidth must be inside the viewport - const xOffset = Math.max( - 0, - overlayRect.width - this.options.minimumInViewportWidth - ); - - // a minimum height of minimumViewportHeight must be inside the viewport - const yOffset = Math.max( - 0, - overlayRect.height - this.options.minimumInViewportHeight - ); - - const left = clamp( - this.options.left, - -xOffset, - Math.max(0, containerRect.width - overlayRect.width + xOffset) - ); - - const top = clamp( - this.options.top, - -yOffset, - Math.max(0, containerRect.height - overlayRect.height + yOffset) - ); - - this._element.style.left = `${left}px`; - this._element.style.top = `${top}px`; - } - override dispose(): void { this._element.remove(); super.dispose(); diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index 840be83cd..b857dc00e 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -197,7 +197,7 @@ export class TabsContainer if ( isFloatingGroupsEnabled && event.shiftKey && - !this.group.isFloating + !this.group.api.isFloating ) { event.preventDefault(); @@ -206,10 +206,14 @@ export class TabsContainer const { top: rootTop, left: rootLeft } = this.accessor.element.getBoundingClientRect(); - this.accessor.addFloatingGroup(this.group, { - x: left - rootLeft + 20, - y: top - rootTop + 20, - }); + this.accessor.addFloatingGroup( + this.group, + { + x: left - rootLeft + 20, + y: top - rootTop + 20, + }, + { inDragMode: true } + ); } } ), @@ -302,10 +306,14 @@ export class TabsContainer const { top: rootTop, left: rootLeft } = this.accessor.element.getBoundingClientRect(); - this.accessor.addFloatingGroup(panel as DockviewPanel, { - x: left - rootLeft, - y: top - rootTop, - }); + this.accessor.addFloatingGroup( + panel as DockviewPanel, + { + x: left - rootLeft, + y: top - rootTop, + }, + { inDragMode: true } + ); return; } diff --git a/packages/dockview-core/src/dockview/deserializer.ts b/packages/dockview-core/src/dockview/deserializer.ts index 82daa7381..3f3490b14 100644 --- a/packages/dockview-core/src/dockview/deserializer.ts +++ b/packages/dockview-core/src/dockview/deserializer.ts @@ -1,7 +1,7 @@ import { GroupviewPanelState } from './types'; import { DockviewGroupPanel } from './dockviewGroupPanel'; import { DockviewPanel, IDockviewPanel } from './dockviewPanel'; -import { IDockviewComponent } from './dockviewComponent'; +import { DockviewComponent } from './dockviewComponent'; import { DockviewPanelModel } from './dockviewPanelModel'; import { DockviewApi } from '../api/component.api'; @@ -21,7 +21,7 @@ interface LegacyState extends GroupviewPanelState { } export class DefaultDockviewDeserialzier implements IPanelDeserializer { - constructor(private readonly layout: IDockviewComponent) {} + constructor(private readonly layout: DockviewComponent) {} public fromJSON( panelData: GroupviewPanelState, diff --git a/packages/dockview-core/src/dockview/dockviewComponent.scss b/packages/dockview-core/src/dockview/dockviewComponent.scss index 8231e327f..1aa878539 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.scss +++ b/packages/dockview-core/src/dockview/dockviewComponent.scss @@ -8,6 +8,7 @@ left: 0px; height: 100%; width: 100%; + z-index: 1; } } diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 2006dfd12..2ef2cdf5a 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, IDisposable } from '../lifecycle'; +import { CompositeDisposable } from '../lifecycle'; import { Event, Emitter } from '../events'; import { Watermark } from './components/watermark/watermark'; import { @@ -41,11 +41,15 @@ import { GroupPanelViewState, GroupviewDropEvent, } from './dockviewGroupPanelModel'; -import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel'; +import { DockviewGroupPanel } from './dockviewGroupPanel'; import { DockviewPanelModel } from './dockviewPanelModel'; import { getPanelData } from '../dnd/dataTransfer'; import { Overlay } from '../dnd/overlay'; import { toggleClass } from '../dom'; +import { + DockviewFloatingGroupPanel, + IDockviewFloatingGroupPanel, +} from './dockviewFloatingGroupPanel'; export interface PanelReference { update: (event: { params: { [key: string]: any } }) => void; @@ -92,6 +96,7 @@ 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; @@ -110,7 +115,7 @@ export interface IDockviewComponent extends IBaseGrid { getGroupPanel: (id: string) => IDockviewPanel | undefined; createWatermarkComponent(): IWatermarkRenderer; // lifecycle - addGroup(options?: AddGroupOptions): IDockviewGroupPanel; + addGroup(options?: AddGroupOptions): DockviewGroupPanel; closeAllGroups(): void; // events moveToNext(options?: MovementOptions): void; @@ -159,11 +164,7 @@ export class DockviewComponent readonly onDidActivePanelChange: Event = this._onDidActivePanelChange.event; - private readonly floatingGroups: { - instance: DockviewGroupPanel; - disposable: IDisposable; - overlay: Overlay; - }[] = []; + readonly floatingGroups: DockviewFloatingGroupPanel[] = []; get orientation(): Orientation { return this.gridview.orientation; @@ -306,7 +307,7 @@ export class DockviewComponent addFloatingGroup( item: DockviewPanel | DockviewGroupPanel, coord?: { x?: number; y?: number; height?: number; width?: number }, - options?: { skipRemoveGroup: boolean; inDragMode: boolean } + options?: { skipRemoveGroup?: boolean; inDragMode: boolean } ): void { let group: DockviewGroupPanel; @@ -356,29 +357,30 @@ export class DockviewComponent inDragMode: typeof options?.inDragMode === 'boolean' ? options.inDragMode - : true, + : false, }); } - const instance = { - instance: group, + const floatingGroupPanel = new DockviewFloatingGroupPanel( + group, + overlay + ); - overlay, - disposable: new CompositeDisposable( - overlay, - overlay.onDidChange(() => { - this._bufferOnDidLayoutChange.fire(); - }), - { - dispose: () => { - group.model.isFloating = false; - remove(this.floatingGroups, instance); - }, - } - ), - }; + floatingGroupPanel.addDisposables( + overlay.onDidChange(() => { + this._bufferOnDidLayoutChange.fire(); + }), + { + dispose: () => { + group.model.isFloating = false; + remove(this.floatingGroups, floatingGroupPanel); + this.updateWatermark(); + }, + } + ); - this.floatingGroups.push(instance); + this.floatingGroups.push(floatingGroupPanel); + this.updateWatermark(); } private orthogonalize(position: Position): DockviewGroupPanel { @@ -519,7 +521,7 @@ export class DockviewComponent const floats: SerializedFloatingGroup[] = this.floatingGroups.map( (floatingGroup) => { return { - data: floatingGroup.instance.toJSON() as GroupPanelViewState, + data: floatingGroup.group.toJSON() as GroupPanelViewState, position: floatingGroup.overlay.toJSON(), }; } @@ -726,7 +728,7 @@ export class DockviewComponent inDragMode: false, skipRemoveGroup: true, }); - } else if (referenceGroup.model.isFloating || target === 'center') { + } else if (referenceGroup.api.isFloating || target === 'center') { panel = this.createPanel(options, referenceGroup); referenceGroup.model.openPanel(panel); } else { @@ -807,7 +809,7 @@ export class DockviewComponent } private updateWatermark(): void { - if (this.groups.filter((x) => !x.model.isFloating).length === 0) { + if (this.groups.filter((x) => !x.api.isFloating).length === 0) { if (!this.watermark) { this.watermark = this.createWatermarkComponent(); @@ -920,17 +922,17 @@ export class DockviewComponent | undefined ): DockviewGroupPanel { const floatingGroup = this.floatingGroups.find( - (_) => _.instance === group + (_) => _.group === group ); if (floatingGroup) { if (!options?.skipDispose) { - floatingGroup.instance.dispose(); + floatingGroup.group.dispose(); this._groups.delete(group.id); } - floatingGroup.disposable.dispose(); + floatingGroup.dispose(); - return floatingGroup.instance; + return floatingGroup.group; } return super.doRemoveGroup(group, options); @@ -986,7 +988,7 @@ export class DockviewComponent const [targetParentLocation, to] = tail(targetLocation); const isFloating = this.floatingGroups.find( - (x) => x.instance === sourceGroup + (x) => x.group === sourceGroup ); if (!isFloating) { @@ -1066,11 +1068,11 @@ export class DockviewComponent } } else { const floatingGroup = this.floatingGroups.find( - (x) => x.instance === sourceGroup + (x) => x.group === sourceGroup ); if (floatingGroup) { - floatingGroup.disposable.dispose(); + floatingGroup.dispose(); } else { this.gridview.removeView( getGridLocation(sourceGroup.element) diff --git a/packages/dockview-core/src/dockview/dockviewFloatingGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewFloatingGroupPanel.ts new file mode 100644 index 000000000..65d768c94 --- /dev/null +++ b/packages/dockview-core/src/dockview/dockviewFloatingGroupPanel.ts @@ -0,0 +1,37 @@ +import { Overlay } from '../dnd/overlay'; +import { CompositeDisposable } from '../lifecycle'; +import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel'; + +export interface IDockviewFloatingGroupPanel { + readonly group: IDockviewGroupPanel; + position( + bounds: Partial<{ + top: number; + left: number; + height: number; + width: number; + }> + ): void; +} + +export class DockviewFloatingGroupPanel + extends CompositeDisposable + implements IDockviewFloatingGroupPanel +{ + constructor(readonly group: DockviewGroupPanel, readonly overlay: Overlay) { + super(); + + this.addDisposables(overlay); + } + + position( + bounds: Partial<{ + top: number; + left: number; + height: number; + width: number; + }> + ): void { + this.overlay.setBounds(bounds); + } +} diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanel.ts index 11bce86fa..0a1ccd52f 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanel.ts @@ -1,6 +1,5 @@ import { IFrameworkPart } from '../panel/types'; import { DockviewComponent } from '../dockview/dockviewComponent'; -import { GridviewPanelApi } from '../api/gridviewPanelApi'; import { DockviewGroupPanelModel, GroupOptions, @@ -9,8 +8,13 @@ import { } from './dockviewGroupPanelModel'; import { GridviewPanel, IGridviewPanel } from '../gridview/gridviewPanel'; import { IDockviewPanel } from '../dockview/dockviewPanel'; +import { + DockviewGroupPanelApi, + DockviewGroupPanelApiImpl, +} from '../api/dockviewGroupPanelApi'; -export interface IDockviewGroupPanel extends IGridviewPanel { +export interface IDockviewGroupPanel + extends IGridviewPanel { model: IDockviewGroupPanelModel; locked: boolean; readonly size: number; @@ -20,10 +24,8 @@ export interface IDockviewGroupPanel extends IGridviewPanel { export type IDockviewGroupPanelPublic = IDockviewGroupPanel; -export type DockviewGroupPanelApi = GridviewPanelApi; - export class DockviewGroupPanel - extends GridviewPanel + extends GridviewPanel implements IDockviewGroupPanel { private readonly _model: DockviewGroupPanelModel; @@ -52,10 +54,6 @@ export class DockviewGroupPanel this._model.locked = value; } - get isFloating(): boolean { - return this._model.isFloating; - } - get header(): IHeader { return this._model.header; } @@ -65,10 +63,17 @@ export class DockviewGroupPanel id: string, options: GroupOptions ) { - super(id, 'groupview_default', { - minimumHeight: 100, - minimumWidth: 100, - }); + super( + id, + 'groupview_default', + { + minimumHeight: 100, + minimumWidth: 100, + }, + new DockviewGroupPanelApiImpl(id, accessor) + ); + + this.api.initialize(this); // cannot use 'this' after after 'super' call this._model = new DockviewGroupPanelModel( this.element, diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 05adc5dfd..fb15a8895 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -237,6 +237,10 @@ export class DockviewGroupPanelModel ); toggleClass(this.container, 'dv-groupview-floating', value); + + this.groupPanel.api._onDidFloatingStateChange.fire({ + isFloating: this.isFloating, + }); } constructor( diff --git a/packages/dockview-core/src/dockview/dockviewPanel.ts b/packages/dockview-core/src/dockview/dockviewPanel.ts index 4f67933fc..c2d1b08dd 100644 --- a/packages/dockview-core/src/dockview/dockviewPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewPanel.ts @@ -8,7 +8,7 @@ import { DockviewGroupPanel } from './dockviewGroupPanel'; import { CompositeDisposable, IDisposable } from '../lifecycle'; import { IPanel, PanelUpdateEvent, Parameters } from '../panel/types'; import { IDockviewPanelModel } from './dockviewPanelModel'; -import { IDockviewComponent } from './dockviewComponent'; +import { DockviewComponent } from './dockviewComponent'; export interface IDockviewPanel extends IDisposable, IPanel { readonly view: IDockviewPanelModel; @@ -47,7 +47,7 @@ export class DockviewPanel constructor( public readonly id: string, - accessor: IDockviewComponent, + accessor: DockviewComponent, private readonly containerApi: DockviewApi, group: DockviewGroupPanel, readonly view: IDockviewPanelModel @@ -55,7 +55,7 @@ export class DockviewPanel super(); this._group = group; - this.api = new DockviewPanelApiImpl(this, this._group); + this.api = new DockviewPanelApiImpl(this, this._group, accessor); this.addDisposables( this.api.onActiveChange(() => { diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 04314fcc3..e7249d6c9 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -8,16 +8,14 @@ import { IWatermarkRenderer, DockviewDropTargets, } from './types'; -import { - DockviewGroupPanel, - DockviewGroupPanelApi, -} from './dockviewGroupPanel'; +import { DockviewGroupPanel } from './dockviewGroupPanel'; import { ISplitviewStyles, Orientation } from '../splitview/splitview'; import { PanelTransfer } from '../dnd/dataTransfer'; import { IDisposable } from '../lifecycle'; import { Position } from '../dnd/droptarget'; import { IDockviewPanel } from './dockviewPanel'; import { FrameworkFactory } from '../panel/componentFactory'; +import { DockviewGroupPanelApi } from '../api/dockviewGroupPanelApi'; export interface IHeaderActionsRenderer extends IDisposable { readonly element: HTMLElement; diff --git a/packages/dockview-core/src/gridview/gridviewPanel.ts b/packages/dockview-core/src/gridview/gridviewPanel.ts index 9562bb92b..b35758287 100644 --- a/packages/dockview-core/src/gridview/gridviewPanel.ts +++ b/packages/dockview-core/src/gridview/gridviewPanel.ts @@ -28,8 +28,8 @@ export interface GridviewInitParameters extends PanelInitParameters { isVisible?: boolean; } -export interface IGridviewPanel - extends BasePanelViewExported { +export interface IGridviewPanel + extends BasePanelViewExported { readonly minimumWidth: number; readonly maximumWidth: number; readonly minimumHeight: number; @@ -38,8 +38,10 @@ export interface IGridviewPanel readonly snap: boolean; } -export abstract class GridviewPanel - extends BasePanelView +export abstract class GridviewPanel< + T extends GridviewPanelApiImpl = GridviewPanelApiImpl + > + extends BasePanelView implements IGridPanelComponentView, IGridviewPanel { private _evaluatedMinimumWidth = 0; @@ -134,9 +136,10 @@ export abstract class GridviewPanel maximumWidth?: number; minimumHeight?: number; maximumHeight?: number; - } + }, + api?: T ) { - super(id, component, new GridviewPanelApiImpl(id)); + super(id, component, api ?? new GridviewPanelApiImpl(id)); if (typeof options?.minimumWidth === 'number') { this._minimumWidth = options.minimumWidth; diff --git a/packages/dockview-core/src/index.ts b/packages/dockview-core/src/index.ts index 35aec243e..be2f51877 100644 --- a/packages/dockview-core/src/index.ts +++ b/packages/dockview-core/src/index.ts @@ -1,7 +1,5 @@ export * from './dnd/dataTransfer'; -export { watchElementResize } from './dom'; - /** * Events, Emitters and Disposables are very common concepts that most codebases will contain. * We export them with a 'Dockview' prefix here to prevent accidental use by others. @@ -71,6 +69,10 @@ export { SplitviewPanelApi, } from './api/splitviewPanelApi'; export { ExpansionEvent, PaneviewPanelApi } from './api/paneviewPanelApi'; +export { + DockviewGroupPanelApi, + DockviewGroupPanelFloatingChangeEvent, +} from './api/dockviewGroupPanelApi'; export { CommonApi, SplitviewApi, diff --git a/packages/dockview-core/src/splitview/splitview.scss b/packages/dockview-core/src/splitview/splitview.scss index 1f0df7ca9..df3380a89 100644 --- a/packages/dockview-core/src/splitview/splitview.scss +++ b/packages/dockview-core/src/splitview/splitview.scss @@ -1,3 +1,24 @@ +.dv-debug { + .split-view-container { + .sash-container { + .sash { + &.enabled { + background-color: black; + } + &.disabled { + background-color: orange; + } + &.maximum { + background-color: green; + } + &.minimum { + background-color: red; + } + } + } + } +} + .split-view-container { position: relative; overflow: hidden; @@ -12,22 +33,6 @@ } } - // debug - // .sash { - // &.enabled { - // background-color: black; - // } - // &.disabled { - // background-color: orange; - // } - // &.maximum { - // background-color: green; - // } - // &.minimum { - // background-color: red; - // } - // } - &.horizontal { height: 100%; diff --git a/packages/docs/docs/components/dockview.mdx b/packages/docs/docs/components/dockview.mdx index cc8d652bb..3f2f0f51c 100644 --- a/packages/docs/docs/components/dockview.mdx +++ b/packages/docs/docs/components/dockview.mdx @@ -383,7 +383,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.isFloating` property. See examples for full code. +a group is floating via the `group.api.isFloating` property. See examples for full code. @@ -499,6 +499,21 @@ panel.api.updateParameters({ }); ``` +### Move panel + +You can programatically move a panel using the panel `api`. + +```ts +panel.api.moveTo({ group, position, index }); +``` + +An equivalent method for moving groups is avaliable on the group `api`. + +```ts +const group = panel.api.group; +group.api.moveTo({ group, position }); +``` + ### Panel Rendering By default `DockviewReact` only adds to the DOM those panels that are visible, diff --git a/packages/docs/sandboxes/demo-dockview/src/app.tsx b/packages/docs/sandboxes/demo-dockview/src/app.tsx index 790ef29be..7461fe1a1 100644 --- a/packages/docs/sandboxes/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/demo-dockview/src/app.tsx @@ -11,28 +11,6 @@ import * as ReactDOM from 'react-dom'; import { v4 } from 'uuid'; import './app.scss'; -function useLocalStorageItem(key: string, defaultValue: string): string { - const [item, setItem] = React.useState( - localStorage.getItem(key) - ); - - React.useEffect(() => { - const listener = (event: StorageEvent) => { - setItem(localStorage.getItem(key)); - }; - - window.addEventListener('storage', listener); - - setItem(localStorage.getItem(key)); - - return () => { - window.removeEventListener('storage', listener); - }; - }, [key]); - - return item === null ? defaultValue : item; -} - const components = { default: (props: IDockviewPanelProps<{ title: string }>) => { return
{props.params.title}
; @@ -213,7 +191,7 @@ const LeftControls = (props: IDockviewHeaderActionsProps) => { ); }; -const DockviewDemo = () => { +const DockviewDemo = (props: { theme?: string }) => { const onReady = (event: DockviewReadyEvent) => { event.api.addPanel({ id: 'panel_1', @@ -264,11 +242,6 @@ const DockviewDemo = () => { event.api.getPanel('panel_1')!.api.setActive(); }; - const theme = useLocalStorageItem( - 'dv-theme-class-name', - 'dockview-theme-abyss' - ); - return ( { rightHeaderActionsComponent={RightControls} leftHeaderActionsComponent={LeftControls} onReady={onReady} - className={theme} + className={props.theme || 'dockview-theme-abyss'} /> ); }; diff --git a/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx b/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx index 4192555f3..6dc1c553a 100644 --- a/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx +++ b/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx @@ -1,11 +1,14 @@ 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 }>) => { @@ -68,12 +71,11 @@ function loadDefaultLayout(api: DockviewApi) { let panelCount = 0; -function addFloatingPanel(api: DockviewApi) { +function addPanel(api: DockviewApi) { api.addPanel({ id: (++panelCount).toString(), title: `Tab ${panelCount}`, component: 'default', - floating: true, }); } @@ -203,6 +205,8 @@ export const DockviewPersistance = () => { onReady={onReady} components={components} watermarkComponent={Watermark} + leftHeaderActionsComponent={LeftComponent} + rightHeaderActionsComponent={RightComponent} className="dockview-theme-abyss" /> @@ -210,6 +214,51 @@ export const DockviewPersistance = () => { ); }; +const LeftComponent = (props: IDockviewHeaderActionsProps) => { + const onClick = () => { + addPanel(props.containerApi); + }; + return ( +
+ +
+ ); +}; + +const RightComponent = (props: IDockviewHeaderActionsProps) => { + const [floating, setFloating] = React.useState( + props.api.isFloating + ); + + React.useEffect(() => { + const disposable = props.group.api.onDidFloatingStateChange((event) => [ + setFloating(event.isFloating), + ]); + + return () => { + disposable.dispose(); + }; + }, [props.group.api]); + + const onClick = () => { + if (floating) { + const group = props.containerApi.addGroup(); + props.group.api.moveTo({ group }); + } else { + props.containerApi.addFloatingGroup(props.group); + } + }; + + return ( +
+ +
+ ); +}; + export default DockviewPersistance; const Watermark = () => { diff --git a/packages/docs/sandboxes/floatinggroup-dockview/src/utils.tsx b/packages/docs/sandboxes/floatinggroup-dockview/src/utils.tsx new file mode 100644 index 000000000..457128d67 --- /dev/null +++ b/packages/docs/sandboxes/floatinggroup-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/simple-dockview/src/app.tsx b/packages/docs/sandboxes/simple-dockview/src/app.tsx index 4d1f066da..258e0a114 100644 --- a/packages/docs/sandboxes/simple-dockview/src/app.tsx +++ b/packages/docs/sandboxes/simple-dockview/src/app.tsx @@ -15,7 +15,7 @@ const components = { }, }; -export const App: React.FC = () => { +export const App: React.FC = (props: { theme?: string }) => { const onReady = (event: DockviewReadyEvent) => { const panel = event.api.addPanel({ id: 'panel_1', @@ -88,7 +88,7 @@ export const App: React.FC = () => { ); }; diff --git a/packages/docs/src/components/ui/container.tsx b/packages/docs/src/components/ui/container.tsx index 4011fb732..b5321c570 100644 --- a/packages/docs/src/components/ui/container.tsx +++ b/packages/docs/src/components/ui/container.tsx @@ -77,6 +77,28 @@ const themes = [ 'dockview-theme-replit', ]; +function useLocalStorageItem(key: string, defaultValue: string): string { + const [item, setItem] = React.useState( + localStorage.getItem(key) + ); + + React.useEffect(() => { + const listener = (event: StorageEvent) => { + setItem(localStorage.getItem(key)); + }; + + window.addEventListener('storage', listener); + + setItem(localStorage.getItem(key)); + + return () => { + window.removeEventListener('storage', listener); + }; + }, [key]); + + return item === null ? defaultValue : item; +} + export const ThemePicker = () => { const [theme, setTheme] = React.useState( localStorage.getItem('dv-theme-class-name') || themes[0] @@ -124,6 +146,11 @@ export const MultiFrameworkContainer = (props: { const [animation, setAnimation] = React.useState(false); + const theme = useLocalStorageItem( + 'dv-theme-class-name', + 'dockview-theme-abyss' + ); + React.useEffect(() => { setAnimation(true); @@ -182,7 +209,7 @@ export const MultiFrameworkContainer = (props: { )} - {framework === 'React' && } + {framework === 'React' && }
-