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 3b5c8586e..d4a8aae02 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; @@ -820,4 +821,17 @@ export class DockviewApi implements CommonApi { get onDidMaxmizedGroupChange(): Event { return this.component.onDidMaxmizedGroupChange; } + + /** + * Add a popout group in a new Window + */ + addPopoutGroup( + item: IDockviewPanel | DockviewGroupPanel, + options?: { + position?: Box; + popoutUrl?: string; + } + ): void { + this.component.addPopoutGroup(item, options); + } } 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/greadyRenderContainer.ts b/packages/dockview-core/src/dockview/components/greadyRenderContainer.ts index 387e6af75..e4c02cb9a 100644 --- a/packages/dockview-core/src/dockview/components/greadyRenderContainer.ts +++ b/packages/dockview-core/src/dockview/components/greadyRenderContainer.ts @@ -85,7 +85,7 @@ export class GreadyRenderContainer extends CompositeDisposable { toggleClass( focusContainer, 'dv-render-overlay-float', - panel.group.api.isFloating + panel.group.api.location === 'floating' ); }; diff --git a/packages/dockview-core/src/dockview/components/panel/content.ts b/packages/dockview-core/src/dockview/components/panel/content.ts index 19f01d1b5..3a0dc707f 100644 --- a/packages/dockview-core/src/dockview/components/panel/content.ts +++ b/packages/dockview-core/src/dockview/components/panel/content.ts @@ -76,7 +76,11 @@ export class ContentContainer const data = getPanelData(); - if (!data && event.shiftKey && !this.group.isFloating) { + if ( + !data && + event.shiftKey && + this.group.location !== 'floating' + ) { return false; } 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 60efcb876..6460244b4 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -47,18 +47,45 @@ import { getPanelData } from '../dnd/dataTransfer'; import { Parameters } from '../panel/types'; import { Overlay } from '../dnd/overlay'; import { toggleClass, watchElementResize } from '../dom'; -import { - DockviewFloatingGroupPanel, - IDockviewFloatingGroupPanel, -} from './dockviewFloatingGroupPanel'; +import { DockviewFloatingGroupPanel } from './dockviewFloatingGroupPanel'; import { GroupDragEvent, TabDragEvent, } from './components/titlebar/tabsContainer'; +import { PopoutWindow } from '../popoutWindow'; +import { Box } from '../types'; import { GreadyRenderContainer, DockviewPanelRenderer, } from './components/greadyRenderContainer'; +import { DockviewPopoutGroupPanel } from './dockviewPopoutGroupPanel'; + +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; @@ -69,7 +96,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 { @@ -82,6 +114,7 @@ export interface SerializedDockview { panels: Record; activeGroup?: string; floatingGroups?: SerializedFloatingGroup[]; + popoutGroups?: SerializedPopoutGroup[]; } function typeValidate3(data: GroupPanelViewState, path: string): void { @@ -196,7 +229,6 @@ 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; @@ -237,6 +269,13 @@ export interface IDockviewComponent extends IBaseGrid { item: IDockviewPanel | DockviewGroupPanel, coord?: { x: number; y: number } ): void; + addPopoutGroup( + item: IDockviewPanel | DockviewGroupPanel, + options?: { + position?: Box; + popoutUrl?: string; + } + ): void; } export class DockviewComponent @@ -277,7 +316,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; @@ -445,6 +485,73 @@ export class DockviewComponent this.updateWatermark(); } + addPopoutGroup( + item: DockviewPanel | DockviewGroupPanel, + options?: { + skipRemoveGroup?: boolean; + position?: Box; + popoutUrl?: string; + } + ): void { + let group: DockviewGroupPanel; + let box: Box | undefined = options?.position; + + if (item instanceof DockviewPanel) { + group = this.createGroup(); + + this.removePanel(item, { + removeEmptyGroup: true, + skipDispose: true, + }); + + group.model.openPanel(item); + + if (!box) { + box = this.element.getBoundingClientRect(); + } + } else { + group = item; + + if (!box) { + box = group.element.getBoundingClientRect(); + } + + const skip = + typeof options?.skipRemoveGroup === 'boolean' && + options.skipRemoveGroup; + + if (!skip) { + this.doRemoveGroup(item, { skipDispose: true }); + } + } + + const theme = getTheme(this.gridview.element); + + const popoutWindow = new DockviewPopoutGroupPanel(group, { + className: theme ?? '', + popoutUrl: options?.popoutUrl ?? '/popout.html', + box: { + left: box.left, + top: box.top, + width: box.width, + height: box.height, + }, + }); + + popoutWindow.addDisposables( + { + dispose: () => { + remove(this._popoutGroups, popoutWindow); + }, + }, + popoutWindow.window.onDidClose(() => { + this.doAddGroup(group, [0]); + }) + ); + + this._popoutGroups.push(popoutWindow); + } + addFloatingGroup( item: DockviewPanel | DockviewGroupPanel, coord?: { x?: number; y?: number; height?: number; width?: number }, @@ -473,7 +580,7 @@ export class DockviewComponent } } - group.model.isFloating = true; + group.model.location = 'floating'; const overlayLeft = typeof coord?.x === 'number' ? Math.max(coord.x, 0) : 100; @@ -544,14 +651,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(); } @@ -605,7 +712,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; @@ -638,8 +745,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(); } @@ -717,11 +824,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(), }; } ); @@ -736,6 +852,10 @@ export class DockviewComponent result.floatingGroups = floats; } + if (popoutGroups.length > 0) { + result.popoutGroups = popoutGroups; + } + return result; } @@ -841,7 +961,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(); } @@ -875,7 +1008,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(); } @@ -999,7 +1132,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 { @@ -1083,7 +1219,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(); @@ -1201,27 +1337,61 @@ export class DockviewComponent } | undefined ): DockviewGroupPanel { - const floatingGroup = this.floatingGroups.find( - (_) => _.group === group - ); - if (floatingGroup) { - if (!options?.skipDispose) { - floatingGroup.group.dispose(); - this._groups.delete(group.id); - this._onDidRemoveGroup.fire(group); + if (group.api.location === 'floating') { + const floatingGroup = this._floatingGroups.find( + (_) => _.group === group + ); + + if (floatingGroup) { + if (!options?.skipDispose) { + floatingGroup.group.dispose(); + this._groups.delete(group.id); + this._onDidRemoveGroup.fire(group); + } + + remove(this._floatingGroups, floatingGroup); + floatingGroup.dispose(); + + if (!options?.skipActive && this._activeGroup === group) { + const groups = Array.from(this._groups.values()); + + this.doSetGroupActive( + groups.length > 0 ? groups[0].value : undefined + ); + } + + return floatingGroup.group; } - floatingGroup.dispose(); + throw new Error('failed to find floating group'); + } - if (!options?.skipActive && this._activeGroup === group) { - const groups = Array.from(this._groups.values()); + if (group.api.location === 'popout') { + const selectedGroup = this._popoutGroups.find( + (_) => _.group === group + ); - this.doSetGroupActive( - groups.length > 0 ? groups[0].value : undefined - ); + if (selectedGroup) { + if (!options?.skipDispose) { + selectedGroup.group.dispose(); + this._groups.delete(group.id); + this._onDidRemoveGroup.fire(group); + } + + selectedGroup.dispose(); + + if (!options?.skipActive && this._activeGroup === group) { + const groups = Array.from(this._groups.values()); + + this.doSetGroupActive( + groups.length > 0 ? groups[0].value : undefined + ); + } + + return selectedGroup.group; } - return floatingGroup.group; + throw new Error('failed to find popout group'); } return super.doRemoveGroup(group, options); @@ -1276,11 +1446,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); @@ -1356,16 +1522,29 @@ export class DockviewComponent }); } } else { - const floatingGroup = this.floatingGroups.find( - (x) => x.group === sourceGroup - ); - - if (floatingGroup) { - floatingGroup.dispose(); - } else { - this.gridview.removeView( - getGridLocation(sourceGroup.element) - ); + switch (sourceGroup.api.location) { + case 'grid': + this.gridview.removeView( + getGridLocation(sourceGroup.element) + ); + break; + case 'floating': + const selectedFloatingGroup = this._floatingGroups.find( + (x) => x.group === sourceGroup + ); + if (!selectedFloatingGroup) { + throw new Error('failed to find floating group'); + } + selectedFloatingGroup.dispose(); + break; + case 'popout': + const selectedPopoutGroup = this._popoutGroups.find( + (x) => x.group === sourceGroup + ); + if (!selectedPopoutGroup) { + throw new Error('failed to find popout group'); + } + selectedPopoutGroup.dispose(); } const referenceLocation = getGridLocation( diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 03007cc1e..c6d3feb0d 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -130,7 +130,7 @@ export interface IDockviewGroupPanelModel extends IPanel { ): boolean; } -export type DockviewGroupMode = 'grid' | 'floating' | 'fullscreen' | 'popout'; +export type DockviewGroupLocation = 'grid' | 'floating' | 'popout'; export class DockviewGroupPanelModel extends CompositeDisposable @@ -143,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(); @@ -243,21 +244,45 @@ 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; + toggleClass(this.container, 'dv-groupview-floating', false); + toggleClass(this.container, 'dv-groupview-popout', false); + + switch (value) { + case 'grid': + this.contentContainer.dropTarget.setTargetZones([ + 'top', + 'bottom', + 'left', + 'right', + 'center', + ]); + break; + case 'floating': + this.contentContainer.dropTarget.setTargetZones(['center']); this.contentContainer.dropTarget.setTargetZones( value ? ['center'] : ['top', 'bottom', 'left', 'right', 'center'] ); - toggleClass(this.container, 'dv-groupview-floating', value); + toggleClass(this.container, 'dv-groupview-floating', true); - this.groupPanel.api._onDidFloatingStateChange.fire({ - isFloating: this.isFloating, + break; + case 'popout': + this.contentContainer.dropTarget.setTargetZones(['center']); + + toggleClass(this.container, 'dv-groupview-popout', true); + + break; + } + + this.groupPanel.api._onDidRenderPositionChange.fire({ + location: this.location, }); } 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/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 97587f1a6..da19572bf 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -97,6 +97,7 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions { minimumHeightWithinViewport?: number; minimumWidthWithinViewport?: number; }; + popoutUrl?: string; defaultRenderer?: DockviewPanelRenderer; debug?: boolean; } diff --git a/packages/dockview-core/src/dom.ts b/packages/dockview-core/src/dom.ts index 12cac06e8..71eebb8ed 100644 --- a/packages/dockview-core/src/dom.ts +++ b/packages/dockview-core/src/dom.ts @@ -186,6 +186,38 @@ export function quasiDefaultPrevented(event: Event): boolean { return (event as any)[QUASI_PREVENT_DEFAULT_KEY]; } +export function addStyles(document: Document, styleSheetList: StyleSheetList) { + const styleSheets = Array.from(styleSheetList); + + for (const styleSheet of styleSheets) { + if (styleSheet.href) { + const link = document.createElement('link'); + link.href = styleSheet.href; + link.type = styleSheet.type; + link.rel = 'stylesheet'; + document.head.appendChild(link); + } + + let cssTexts: string[] = []; + + try { + if (styleSheet.cssRules) { + cssTexts = Array.from(styleSheet.cssRules).map( + (rule) => rule.cssText + ); + } + } catch (err) { + // security errors (lack of permissions), ignore + } + + for (const rule of cssTexts) { + const style = document.createElement('style'); + style.appendChild(document.createTextNode(rule)); + document.head.appendChild(style); + } + } +} + export function getDomNodePagePosition(domNode: Element): { left: number; top: number; diff --git a/packages/dockview-core/src/popoutWindow.ts b/packages/dockview-core/src/popoutWindow.ts new file mode 100644 index 000000000..2994e44ec --- /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 b6e599341..d58ceb062 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 DockviewRenderMode from '@site/sandboxes/rendermode-dockview/src/app'; import { DocRef } from '@site/src/components/ui/reference/docRef'; @@ -356,7 +357,7 @@ Floating groups can be interacted with whilst holding the `shift` key activating Floating groups can be programatically added through the dockview `api` method `api.addFloatingGroup(...)` and you can check whether -a group is floating via the `group.api.isFloating` property. See examples for full code. +a group is floating via the `group.api.location` property. See examples for full code. You can control the bounding box of floating groups through the optional `floatingGroupBounds` options: @@ -370,6 +371,27 @@ You can control the bounding box of floating groups through the optional `floati react={DockviewFloating} /> +## Popout Groups + +Dockview has built-in support for opening groups in new Windows. +Each popout window can contain a single group with many panels and you can have as many popout +windows as needed. You cannot dock multiple groups together in the same window. + +To open an existing group in a new window + +```tsx +api.addPopoutGroup(group); +``` + +From within a panel you may say + +```tsx +props.containerApi.addPopoutGroup(props.api.group); +``` + + + + ## Panels ### Add Panel 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..1d16e0029 --- /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', + }); + + api.addPanel({ + id: 'panel_4', + component: 'default', + }); + + api.addPanel({ + id: 'panel_5', + component: 'default', + position: { direction: 'right' }, + }); + + api.addPanel({ + id: 'panel_6', + component: 'default', + }); +} + +let panelCount = 0; + +function safeParse(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 [popout, setPopout] = React.useState( + props.api.location === 'popout' + ); + + React.useEffect(() => { + const disposable = props.group.api.onDidRenderPositionChange( + (event) => [setPopout(event.location === 'popout')] + ); + + return () => { + disposable.dispose(); + }; + }, [props.group.api]); + + const onClick = () => { + if (popout) { + const group = props.containerApi.addGroup(); + props.group.api.moveTo({ group }); + } else { + props.containerApi.addPopoutGroup(props.group); + } + }; + + return ( +
+ +
+ ); +}; + +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/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>", 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
; +}