From 23b1edb0033941fb9bdd57b1104ae97993a22e1c Mon Sep 17 00:00:00 2001 From: sachnk <5503945+sachnk@users.noreply.github.com> Date: Mon, 22 Jan 2024 19:02:53 +0000 Subject: [PATCH 1/6] add window-lifecycle callbacks --- packages/dockview-core/src/api/component.api.ts | 2 ++ .../dockview-core/src/dockview/dockviewComponent.ts | 6 ++++++ .../src/dockview/dockviewPopoutGroupPanel.ts | 4 ++++ packages/dockview-core/src/popoutWindow.ts | 10 ++++++++++ 4 files changed, 22 insertions(+) diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index d4a8aae02..02f4292a7 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -830,6 +830,8 @@ export class DockviewApi implements CommonApi { options?: { position?: Box; popoutUrl?: string; + onOpened?: (id: string, window: Window) => void; + onClosing?: (id: string, window: Window) => void; } ): void { this.component.addPopoutGroup(item, options); diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 82e5283a4..0080c93a3 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -286,6 +286,8 @@ export interface IDockviewComponent extends IBaseGrid { options?: { position?: Box; popoutUrl?: string; + onOpened?: (id: string, window: Window) => void; + onClosing?: (id: string, window: Window) => void; } ): void; } @@ -513,6 +515,8 @@ export class DockviewComponent skipRemoveGroup?: boolean; position?: Box; popoutUrl?: string; + onOpened?: (id: string, window: Window) => void; + onClosing?: (id: string, window: Window) => void; } ): void { let group: DockviewGroupPanel; @@ -561,6 +565,8 @@ export class DockviewComponent width: box.width, height: box.height, }, + onOpened: options?.onOpened, + onClosing: options?.onClosing } ); diff --git a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts index 803fa5411..6d8e7d1b0 100644 --- a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts @@ -13,6 +13,8 @@ export class DockviewPopoutGroupPanel extends CompositeDisposable { className: string; popoutUrl: string; box: Box; + onOpened?: (id: string, window: Window) => void; + onClosing?: (id: string, window: Window) => void; } ) { super(); @@ -23,6 +25,8 @@ export class DockviewPopoutGroupPanel extends CompositeDisposable { top: this.options.box.top, width: this.options.box.width, height: this.options.box.height, + onOpened: this.options.onOpened, + onClosing: this.options.onClosing, }); group.model.location = 'popout'; diff --git a/packages/dockview-core/src/popoutWindow.ts b/packages/dockview-core/src/popoutWindow.ts index c73334549..1e26e4257 100644 --- a/packages/dockview-core/src/popoutWindow.ts +++ b/packages/dockview-core/src/popoutWindow.ts @@ -5,6 +5,8 @@ import { Box } from './types'; export type PopoutWindowOptions = { url: string; + onOpened?: (id: string, window: Window) => void; + onClosing?: (id: string, window: Window) => void; } & Box; export class PopoutWindow extends CompositeDisposable { @@ -42,6 +44,10 @@ export class PopoutWindow extends CompositeDisposable { close(): void { if (this._window) { + if (this.options.onClosing) { + this.options.onClosing(this.id, this._window.value); + } + this._window.disposable.dispose(); this._window.value.close(); this._window = null; @@ -114,5 +120,9 @@ export class PopoutWindow extends CompositeDisposable { cleanUp(); }); }); + + if (this.options.onOpened) { + this.options.onOpened(this.id, externalWindow); + } } } From a24dd21ca2b22d2eb87f230975ef1bfc3ef766f9 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:11:47 +0000 Subject: [PATCH 2/6] feat: provide means to obtain popoutWindow document --- .../__tests__/dnd/groupDragHandler.spec.ts | 6 +- .../components/titlebar/tabsContainer.spec.ts | 40 ++- .../dockview/dockviewComponent.spec.ts | 244 +++++++++--------- .../__tests__/overlayRenderContainer.spec.ts | 4 +- .../src/api/dockviewGroupPanelApi.ts | 15 +- .../dockview-core/src/api/dockviewPanelApi.ts | 48 +++- .../dockview-core/src/dnd/groupDragHandler.ts | 2 +- .../src/dockview/components/panel/content.ts | 2 +- .../components/titlebar/tabsContainer.ts | 4 +- .../src/dockview/dockviewComponent.ts | 20 +- .../src/dockview/dockviewGroupPanelModel.ts | 9 +- .../src/dockview/dockviewPopoutGroupPanel.ts | 7 +- packages/dockview-core/src/index.ts | 2 + .../src/overlayRenderContainer.ts | 2 +- packages/dockview-core/src/popoutWindow.ts | 74 ++++-- yarn.lock | 9 +- 16 files changed, 288 insertions(+), 200 deletions(-) diff --git a/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts b/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts index b2cd3d44d..0ea0f7f5f 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: { location: 'grid' } as any, + api: { location: { type: 'grid' } } as any, }; return partial as DockviewGroupPanel; }); @@ -53,7 +53,7 @@ describe('groupDragHandler', () => { const groupMock = jest.fn(() => { const partial: Partial = { - api: { location: 'floating' } as any, + api: { location: { type: 'floating' } } as any, }; return partial as DockviewGroupPanel; }); @@ -85,7 +85,7 @@ describe('groupDragHandler', () => { const groupMock = jest.fn(() => { const partial: Partial = { - api: { location: 'grid' } as any, + api: { location: { type: '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 58a007393..266781742 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 @@ -9,6 +9,7 @@ import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanel import { fireEvent } from '@testing-library/dom'; import { TestPanel } from '../../dockviewGroupPanelModel.spec'; import { IDockviewPanel } from '../../../../dockview/dockviewPanel'; +import { fromPartial } from '@total-typescript/shoehorn'; describe('tabsContainer', () => { test('that an external event does not render a drop target and calls through to the group mode', () => { @@ -478,7 +479,7 @@ describe('tabsContainer', () => { const groupPanelMock = jest.fn(() => { return (>{ - api: { location: 'grid' } as any, + api: { location: { type: 'grid' } } as any, }) as DockviewGroupPanel; }); @@ -538,7 +539,7 @@ describe('tabsContainer', () => { const groupPanelMock = jest.fn(() => { return (>{ - api: { location: 'floating' } as any, + api: { location: { type: 'floating' } } as any, }) as DockviewGroupPanel; }); @@ -591,7 +592,7 @@ describe('tabsContainer', () => { const groupPanelMock = jest.fn(() => { return (>{ - api: { location: 'floating' } as any, + api: { location: { type: 'floating' } } as any, model: {} as any, }) as DockviewGroupPanel; }); @@ -601,23 +602,20 @@ describe('tabsContainer', () => { const cut = new TabsContainer(accessor, groupPanel); - const panelMock = jest.fn((id: string) => { - const partial: Partial = { + const createPanel = (id: string) => + fromPartial({ id, - view: { tab: { element: document.createElement('div'), - } as any, + }, content: { element: document.createElement('div'), - } as any, - } as any, - }; - return partial as IDockviewPanel; - }); + }, + }, + }); - const panel = new panelMock('test_id'); + const panel = createPanel('test_id'); cut.openPanel(panel); const el = cut.element.querySelector('.tab')!; @@ -628,15 +626,15 @@ describe('tabsContainer', () => { fireEvent(el, event); // a floating group with a single tab shouldn't be eligible - expect(preventDefaultSpy).toBeCalledTimes(0); - expect(accessor.addFloatingGroup).toBeCalledTimes(0); + expect(preventDefaultSpy).toHaveBeenCalledTimes(0); + expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(0); - const panel2 = new panelMock('test_id_2'); + const panel2 = createPanel('test_id_2'); cut.openPanel(panel2); fireEvent(el, event); - expect(preventDefaultSpy).toBeCalledTimes(1); - expect(accessor.addFloatingGroup).toBeCalledTimes(1); + expect(preventDefaultSpy).toHaveBeenCalledTimes(1); + expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(1); }); test('pre header actions', () => { @@ -653,7 +651,7 @@ describe('tabsContainer', () => { const groupPanelMock = jest.fn(() => { return (>{ - api: { location: 'grid' } as any, + api: { location: { type: 'grid' } } as any, model: {} as any, }) as DockviewGroupPanel; }); @@ -723,7 +721,7 @@ describe('tabsContainer', () => { const groupPanelMock = jest.fn(() => { return (>{ - api: { location: 'grid' } as any, + api: { location: { type: 'grid' } } as any, model: {} as any, }) as DockviewGroupPanel; }); @@ -793,7 +791,7 @@ describe('tabsContainer', () => { const groupPanelMock = jest.fn(() => { return (>{ - api: { location: 'grid' } as any, + api: { location: { type: '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 9dd25066d..15e38dd61 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -3452,8 +3452,8 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); @@ -3464,8 +3464,8 @@ describe('dockviewComponent', () => { 'right' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); }); @@ -3497,8 +3497,8 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); @@ -3509,8 +3509,8 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); }); @@ -3548,9 +3548,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); @@ -3561,9 +3561,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -3601,9 +3601,9 @@ describe('dockviewComponent', () => { position: { referencePanel: panel2 }, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -3614,9 +3614,9 @@ describe('dockviewComponent', () => { 'right' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -3654,9 +3654,9 @@ describe('dockviewComponent', () => { position: { referencePanel: panel2 }, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -3667,9 +3667,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(3); }); @@ -3713,10 +3713,10 @@ describe('dockviewComponent', () => { floating: true, }); - 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(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); + expect(panel4.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(4); @@ -3727,10 +3727,10 @@ describe('dockviewComponent', () => { 'center' ); - 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(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); + expect(panel4.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(4); }); @@ -3762,8 +3762,8 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); @@ -3774,8 +3774,8 @@ describe('dockviewComponent', () => { 'right' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); }); @@ -3807,8 +3807,8 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); @@ -3819,8 +3819,8 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); }); @@ -3858,9 +3858,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); @@ -3871,9 +3871,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -3911,9 +3911,9 @@ describe('dockviewComponent', () => { position: { referencePanel: panel2 }, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -3924,9 +3924,9 @@ describe('dockviewComponent', () => { 'right' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); }); @@ -3964,9 +3964,9 @@ describe('dockviewComponent', () => { position: { referencePanel: panel2 }, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -3977,9 +3977,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -4023,10 +4023,10 @@ describe('dockviewComponent', () => { floating: true, }); - 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(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); + expect(panel4.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(4); @@ -4037,10 +4037,10 @@ describe('dockviewComponent', () => { 'center' ); - 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(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); + expect(panel4.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(4); }); @@ -4078,9 +4078,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); @@ -4091,9 +4091,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('floating'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('floating'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -4130,9 +4130,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -4143,9 +4143,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('floating'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('floating'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -4183,9 +4183,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); @@ -4196,9 +4196,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('floating'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('floating'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -4235,9 +4235,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -4248,9 +4248,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('floating'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('floating'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(3); }); @@ -4282,15 +4282,15 @@ describe('dockviewComponent', () => { position: { direction: 'right' }, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); dockview.addFloatingGroup(panel2); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); }); @@ -4321,15 +4321,15 @@ describe('dockviewComponent', () => { component: 'default', }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); dockview.addFloatingGroup(panel2); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); }); @@ -4361,15 +4361,15 @@ describe('dockviewComponent', () => { position: { direction: 'right' }, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); dockview.addFloatingGroup(panel2.group); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); }); @@ -4400,15 +4400,15 @@ describe('dockviewComponent', () => { component: 'default', }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); dockview.addFloatingGroup(panel2.group); - expect(panel1.group.api.location).toBe('floating'); - expect(panel2.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('floating'); + expect(panel2.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); }); @@ -4440,7 +4440,7 @@ describe('dockviewComponent', () => { expect(dockview.panels.length).toBe(1); expect(dockview.groups.length).toBe(1); - expect(panel1.api.group.api.location).toBe('popout'); + expect(panel1.api.group.api.location.type).toBe('popout'); dockview.removePanel(panel1); @@ -4474,15 +4474,15 @@ describe('dockviewComponent', () => { component: 'default', }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); dockview.addPopoutGroup(panel2.group); - expect(panel1.group.api.location).toBe('popout'); - expect(panel2.group.api.location).toBe('popout'); + expect(panel1.group.api.location.type).toBe('popout'); + expect(panel2.group.api.location.type).toBe('popout'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); }); @@ -4521,17 +4521,17 @@ describe('dockviewComponent', () => { }, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); dockview.addPopoutGroup(panel2.group); - expect(panel1.group.api.location).toBe('popout'); - expect(panel2.group.api.location).toBe('popout'); - expect(panel3.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('popout'); + expect(panel2.group.api.location.type).toBe('popout'); + expect(panel3.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -4542,9 +4542,9 @@ describe('dockviewComponent', () => { 'right' ); - expect(panel1.group.api.location).toBe('popout'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('popout'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); }); diff --git a/packages/dockview-core/src/__tests__/overlayRenderContainer.spec.ts b/packages/dockview-core/src/__tests__/overlayRenderContainer.spec.ts index db61d319a..81310060b 100644 --- a/packages/dockview-core/src/__tests__/overlayRenderContainer.spec.ts +++ b/packages/dockview-core/src/__tests__/overlayRenderContainer.spec.ts @@ -41,7 +41,7 @@ describe('overlayRenderContainer', () => { }, group: { api: { - location: 'grid', + location: { type: 'grid' }, }, }, }); @@ -77,7 +77,7 @@ describe('overlayRenderContainer', () => { }, group: { api: { - location: 'grid', + location: { type: 'grid' }, }, }, }); diff --git a/packages/dockview-core/src/api/dockviewGroupPanelApi.ts b/packages/dockview-core/src/api/dockviewGroupPanelApi.ts index c4b349bdf..1a999301d 100644 --- a/packages/dockview-core/src/api/dockviewGroupPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewGroupPanelApi.ts @@ -8,6 +8,13 @@ import { GridviewPanelApi, GridviewPanelApiImpl } from './gridviewPanelApi'; export interface DockviewGroupPanelApi extends GridviewPanelApi { readonly onDidLocationChange: Event; readonly location: DockviewGroupLocation; + /** + * + * If you require the documents Window object you can call `document.defaultView`. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/defaultView + */ + getDocument(): Document; moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void; maximize(): void; isMaximized(): boolean; @@ -42,6 +49,12 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl { this.addDisposables(this._onDidLocationChange); } + getDocument(): Document { + return this.location.type === 'popout' + ? this.location.getWindow().document + : window.document; + } + moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void { if (!this._group) { throw new Error(NOT_INITIALIZED_MESSAGE); @@ -66,7 +79,7 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl { throw new Error(NOT_INITIALIZED_MESSAGE); } - if (this.location !== 'grid') { + if (this.location.type !== 'grid') { // only grid groups can be maximized return; } diff --git a/packages/dockview-core/src/api/dockviewPanelApi.ts b/packages/dockview-core/src/api/dockviewPanelApi.ts index 6ef8d824a..dffca9297 100644 --- a/packages/dockview-core/src/api/dockviewPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewPanelApi.ts @@ -1,11 +1,13 @@ import { Emitter, Event } from '../events'; import { GridviewPanelApiImpl, GridviewPanelApi } from './gridviewPanelApi'; import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel'; -import { MutableDisposable } from '../lifecycle'; +import { CompositeDisposable, MutableDisposable } from '../lifecycle'; import { DockviewPanel } from '../dockview/dockviewPanel'; import { DockviewComponent } from '../dockview/dockviewComponent'; import { Position } from '../dnd/droptarget'; import { DockviewPanelRenderer } from '../overlayRenderContainer'; +import { DockviewGroupPanelFloatingChangeEvent } from './dockviewGroupPanelApi'; +import { DockviewGroupLocation } from '../dockview/dockviewGroupPanelModel'; export interface TitleEvent { readonly title: string; @@ -28,6 +30,8 @@ export interface DockviewPanelApi readonly onDidActiveGroupChange: Event; readonly onDidGroupChange: Event; readonly onDidRendererChange: Event; + readonly location: DockviewGroupLocation; + readonly onDidLocationChange: Event; close(): void; setTitle(title: string): void; setRenderer(renderer: DockviewPanelRenderer): void; @@ -39,6 +43,13 @@ export interface DockviewPanelApi maximize(): void; isMaximized(): boolean; exitMaximized(): void; + /** + * + * If you require the documents Window object you can call `document.defaultView`. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/defaultView + */ + getDocument(): Document; } export class DockviewPanelApiImpl @@ -59,7 +70,16 @@ export class DockviewPanelApiImpl readonly _onDidRendererChange = new Emitter(); readonly onDidRendererChange = this._onDidRendererChange.event; - private readonly disposable = new MutableDisposable(); + private readonly _onDidLocationChange = + new Emitter(); + readonly onDidLocationChange: Event = + this._onDidLocationChange.event; + + private readonly groupEventsDisposable = new MutableDisposable(); + + get location(): DockviewGroupLocation { + return this.group.api.location; + } get title(): string | undefined { return this.panel.title; @@ -81,13 +101,22 @@ export class DockviewPanelApiImpl this._onDidGroupChange.fire(); if (this._group) { - this.disposable.value = this._group.api.onDidActiveChange(() => { - this._onDidActiveGroupChange.fire(); - }); + this.groupEventsDisposable.value = new CompositeDisposable( + this.group.api.onDidLocationChange((event) => { + this._onDidLocationChange.fire(event); + }), + this.group.api.onDidActiveChange(() => { + this._onDidActiveGroupChange.fire(); + }) + ); if (this.isGroupActive !== isOldGroupActive) { this._onDidActiveGroupChange.fire(); } + + this._onDidLocationChange.fire({ + location: this.group.api.location, + }); } } @@ -107,14 +136,19 @@ export class DockviewPanelApiImpl this._group = group; this.addDisposables( - this.disposable, + this.groupEventsDisposable, this._onDidRendererChange, this._onDidTitleChange, this._onDidGroupChange, - this._onDidActiveGroupChange + this._onDidActiveGroupChange, + this._onDidLocationChange ); } + getDocument(): Document { + return this.group.api.getDocument(); + } + moveTo(options: { group: DockviewGroupPanel; position?: Position; diff --git a/packages/dockview-core/src/dnd/groupDragHandler.ts b/packages/dockview-core/src/dnd/groupDragHandler.ts index de4a3ef04..7138437bf 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.location === 'floating' && !_event.shiftKey) { + if (this.group.api.location.type === 'floating' && !_event.shiftKey) { return true; } return false; diff --git a/packages/dockview-core/src/dockview/components/panel/content.ts b/packages/dockview-core/src/dockview/components/panel/content.ts index b98b12289..dd762873a 100644 --- a/packages/dockview-core/src/dockview/components/panel/content.ts +++ b/packages/dockview-core/src/dockview/components/panel/content.ts @@ -71,7 +71,7 @@ export class ContentContainer if ( !data && event.shiftKey && - this.group.location !== 'floating' + this.group.location.type !== 'floating' ) { return false; } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index 123608573..6a7caac29 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.location !== 'floating' + this.group.api.location.type !== 'floating' ) { event.preventDefault(); @@ -350,7 +350,7 @@ export class TabsContainer !this.accessor.options.disableFloatingGroups; const isFloatingWithOnePanel = - this.group.api.location === 'floating' && this.size === 1; + this.group.api.location.type === '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 7595d723a..a46177a95 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -67,6 +67,7 @@ import { DockviewPanelRenderer, OverlayRenderContainer, } from '../overlayRenderContainer'; +import { PopoutWindow } from '../popoutWindow'; const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = { activationSize: { type: 'pixels', value: 10 }, @@ -608,7 +609,7 @@ export class DockviewComponent } } - group.model.location = 'floating'; + group.model.location = { type: 'floating' }; const overlayLeft = typeof coord?.x === 'number' @@ -683,7 +684,7 @@ export class DockviewComponent dispose: () => { disposable.dispose(); - group.model.location = 'grid'; + group.model.location = { type: 'grid' }; remove(this._floatingGroups, floatingGroupPanel); this.updateWatermark(); }, @@ -1173,7 +1174,7 @@ export class DockviewComponent group.model.openPanel(panel); this.doSetGroupAndPanelActive(group); } else if ( - referenceGroup.api.location === 'floating' || + referenceGroup.api.location.type === 'floating' || target === 'center' ) { panel = this.createPanel(options, referenceGroup); @@ -1259,7 +1260,10 @@ export class DockviewComponent } private updateWatermark(): void { - if (this.groups.filter((x) => x.api.location === 'grid').length === 0) { + if ( + this.groups.filter((x) => x.api.location.type === 'grid').length === + 0 + ) { if (!this.watermark) { this.watermark = this.createWatermarkComponent(); @@ -1377,7 +1381,7 @@ export class DockviewComponent } | undefined ): DockviewGroupPanel { - if (group.api.location === 'floating') { + if (group.api.location.type === 'floating') { const floatingGroup = this._floatingGroups.find( (_) => _.group === group ); @@ -1406,7 +1410,7 @@ export class DockviewComponent throw new Error('failed to find floating group'); } - if (group.api.location === 'popout') { + if (group.api.location.type === 'popout') { const selectedGroup = this._popoutGroups.find( (_) => _.group === group ); @@ -1486,7 +1490,7 @@ export class DockviewComponent if (sourceGroup && sourceGroup.size < 2) { const [targetParentLocation, to] = tail(targetLocation); - if (sourceGroup.api.location === 'grid') { + if (sourceGroup.api.location.type === 'grid') { const sourceLocation = getGridLocation(sourceGroup.element); const [sourceParentLocation, from] = tail(sourceLocation); @@ -1562,7 +1566,7 @@ export class DockviewComponent }); } } else { - switch (sourceGroup.api.location) { + switch (sourceGroup.api.location.type) { case 'grid': this.gridview.removeView( getGridLocation(sourceGroup.element) diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 4462add04..a80a7baa0 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -130,7 +130,10 @@ export interface IDockviewGroupPanelModel extends IPanel { ): boolean; } -export type DockviewGroupLocation = 'grid' | 'floating' | 'popout'; +export type DockviewGroupLocation = + | { type: 'grid' } + | { type: 'floating' } + | { type: 'popout'; getWindow: () => Window }; export class DockviewGroupPanelModel extends CompositeDisposable @@ -146,7 +149,7 @@ export class DockviewGroupPanelModel private _leftHeaderActions: IHeaderActionsRenderer | undefined; private _prefixHeaderActions: IHeaderActionsRenderer | undefined; - private _location: DockviewGroupLocation = 'grid'; + private _location: DockviewGroupLocation = { type: 'grid' }; private mostRecentlyUsed: IDockviewPanel[] = []; @@ -253,7 +256,7 @@ export class DockviewGroupPanelModel toggleClass(this.container, 'dv-groupview-floating', false); toggleClass(this.container, 'dv-groupview-popout', false); - switch (value) { + switch (value.type) { case 'grid': this.contentContainer.dropTarget.setTargetZones([ 'top', diff --git a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts index 803fa5411..c95fdce1f 100644 --- a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts @@ -25,13 +25,16 @@ export class DockviewPopoutGroupPanel extends CompositeDisposable { height: this.options.box.height, }); - group.model.location = 'popout'; + group.model.location = { + type: 'popout', + getWindow: () => this.window.window!, + }; this.addDisposables( this.window, { dispose: () => { - group.model.location = 'grid'; + group.model.location = { type: 'grid' }; }, }, this.window.onDidClose(() => { diff --git a/packages/dockview-core/src/index.ts b/packages/dockview-core/src/index.ts index 62d415174..3f5c8bf70 100644 --- a/packages/dockview-core/src/index.ts +++ b/packages/dockview-core/src/index.ts @@ -11,6 +11,8 @@ export { CompositeDisposable as DockviewCompositeDisposable, } from './lifecycle'; +export { PopoutWindow } from './popoutWindow'; + export * from './panel/types'; export * from './panel/componentFactory'; diff --git a/packages/dockview-core/src/overlayRenderContainer.ts b/packages/dockview-core/src/overlayRenderContainer.ts index 5fea8cee0..62095e595 100644 --- a/packages/dockview-core/src/overlayRenderContainer.ts +++ b/packages/dockview-core/src/overlayRenderContainer.ts @@ -93,7 +93,7 @@ export class OverlayRenderContainer extends CompositeDisposable { toggleClass( focusContainer, 'dv-render-overlay-float', - panel.group.api.location === 'floating' + panel.group.api.location.type === 'floating' ); }; diff --git a/packages/dockview-core/src/popoutWindow.ts b/packages/dockview-core/src/popoutWindow.ts index c73334549..672a6d4e5 100644 --- a/packages/dockview-core/src/popoutWindow.ts +++ b/packages/dockview-core/src/popoutWindow.ts @@ -8,19 +8,26 @@ export type PopoutWindowOptions = { } & Box; export class PopoutWindow extends CompositeDisposable { + private readonly _onWillClose = new Emitter(); + readonly onWillClose = this._onWillClose.event; + private readonly _onDidClose = new Emitter(); readonly onDidClose = this._onDidClose.event; private _window: { value: Window; disposable: IDisposable } | null = null; + get window(): Window | null { + return this._window?.value ?? null; + } + constructor( - private readonly id: string, + private readonly target: string, private readonly className: string, private readonly options: PopoutWindowOptions ) { super(); - this.addDisposables(this._onDidClose, { + this.addDisposables(this._onWillClose, this._onDidClose, { dispose: () => { this.close(); }, @@ -42,9 +49,13 @@ export class PopoutWindow extends CompositeDisposable { close(): void { if (this._window) { + this._onWillClose.fire(); + this._window.disposable.dispose(); this._window.value.close(); this._window = null; + + this._onDidClose.fire(); } } @@ -64,8 +75,10 @@ export class PopoutWindow extends CompositeDisposable { .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); + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/open + */ + const externalWindow = window.open(url, this.target, features); if (!externalWindow) { return; @@ -75,44 +88,55 @@ export class PopoutWindow extends CompositeDisposable { this._window = { value: externalWindow, disposable }; - const cleanUp = () => { - 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(); + /** + * before the main window closes we should close this popup too + * to be good citizens + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event + */ this.close(); }) ); externalWindow.addEventListener('load', () => { + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event + */ + 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); + const container = this.createPopoutWindowContainer(); + container.classList.add(this.className); + container.appendChild(content); - externalDocument.body.replaceChildren(div); + // externalDocument.body.replaceChildren(container); + externalDocument.body.appendChild(container); externalDocument.body.classList.add(this.className); addStyles(externalDocument, window.document.styleSheets); externalWindow.addEventListener('beforeunload', () => { - // TODO: indicate external window is closing - cleanUp(); + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event + */ + this.close(); }); }); } + + private createPopoutWindowContainer(): HTMLElement { + const el = document.createElement('div'); + el.classList.add('dv-popout-window'); + el.id = 'dv-popout-window'; + el.style.position = 'absolute'; + el.style.width = '100%'; + el.style.height = '100%'; + el.style.top = '0px'; + el.style.left = '0px'; + + return el; + } } diff --git a/yarn.lock b/yarn.lock index 7844d2be6..448c87069 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13882,6 +13882,13 @@ react-json-view-lite@^1.2.0: resolved "https://registry.yarnpkg.com/react-json-view-lite/-/react-json-view-lite-1.2.1.tgz#c59a0bea4ede394db331d482ee02e293d38f8218" integrity sha512-Itc0g86fytOmKZoIoJyGgvNqohWSbh3NXIKNgH6W6FT9PC1ck4xas1tT3Rr/b3UlFXyA9Jjaw9QSXdZy2JwGMQ== +react-laag@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/react-laag/-/react-laag-2.0.5.tgz#549f1035b761b9ba09ac98fd128ccad63464c877" + integrity sha512-RCvublJhdcgGRHU1wMYJ8kRtnYsKUgYusLvVhMuftg65POnnOB4+fwXvnETm6adc0cMnc1spujlrK6bGIz6aug== + dependencies: + tiny-warning "^1.0.3" + react-loadable-ssr-addon-v5-slorber@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz#2cdc91e8a744ffdf9e3556caabeb6e4278689883" @@ -15678,7 +15685,7 @@ tiny-invariant@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== -tiny-warning@^1.0.0: +tiny-warning@^1.0.0, tiny-warning@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== From 0fd3a669c7ea27dffef622e125be73208c9a1353 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sat, 27 Jan 2024 19:21:09 +0000 Subject: [PATCH 3/6] test --- .../docs/sandboxes/demo-dockview/src/app.tsx | 4 +- .../floatinggroup-dockview/src/app.tsx | 4 +- .../popoutgroup-dockview/package.json | 5 +- .../popoutgroup-dockview/src/app.tsx | 99 ++++++++++++++----- .../popoutgroup-dockview/src/popover.tsx | 61 ++++++++++++ 5 files changed, 144 insertions(+), 29 deletions(-) create mode 100644 packages/docs/sandboxes/popoutgroup-dockview/src/popover.tsx diff --git a/packages/docs/sandboxes/demo-dockview/src/app.tsx b/packages/docs/sandboxes/demo-dockview/src/app.tsx index 79885e0bf..975f3f6be 100644 --- a/packages/docs/sandboxes/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/demo-dockview/src/app.tsx @@ -87,7 +87,7 @@ const RightControls = (props: IDockviewHeaderActionsProps) => { ); const [isPopout, setIsPopout] = React.useState( - props.api.location === 'popout' + props.api.location.type === 'popout' ); React.useEffect(() => { @@ -96,7 +96,7 @@ const RightControls = (props: IDockviewHeaderActionsProps) => { }); const disposable2 = props.api.onDidLocationChange(() => { - setIsPopout(props.api.location === 'popout'); + setIsPopout(props.api.location.type === 'popout'); }); return () => { diff --git a/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx b/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx index e3c066bbd..c90e32cec 100644 --- a/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx +++ b/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx @@ -255,13 +255,13 @@ const LeftComponent = (props: IDockviewHeaderActionsProps) => { const RightComponent = (props: IDockviewHeaderActionsProps) => { const [floating, setFloating] = React.useState( - props.api.location === 'floating' + props.api.location.type === 'floating' ); React.useEffect(() => { const disposable = props.group.api.onDidLocationChange( (event) => { - setFloating(event.location === 'floating'); + setFloating(event.location.type === 'floating'); } ); diff --git a/packages/docs/sandboxes/popoutgroup-dockview/package.json b/packages/docs/sandboxes/popoutgroup-dockview/package.json index 9a533bff7..9b8601416 100644 --- a/packages/docs/sandboxes/popoutgroup-dockview/package.json +++ b/packages/docs/sandboxes/popoutgroup-dockview/package.json @@ -9,7 +9,8 @@ "dependencies": { "dockview": "*", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-laag": "^2.0.5" }, "devDependencies": { "@types/react": "^18.0.28", @@ -29,4 +30,4 @@ "not ie <= 11", "not op_mini all" ] -} +} \ No newline at end of file diff --git a/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx b/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx index df732b7c0..57fe7e8be 100644 --- a/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx +++ b/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx @@ -5,12 +5,53 @@ import { IDockviewHeaderActionsProps, IDockviewPanelProps, SerializedDockview, + DockviewPanelApi, + DockviewGroupLocation, } from 'dockview'; import * as React from 'react'; +import * as ReactDOM from 'react-dom'; import { Icon } from './utils'; +import { PopoverMenu } from './popover'; + +function usePopoutWindowContext(api: DockviewPanelApi): Window { + const [location, setLocation] = React.useState( + api.location + ); + + React.useEffect(() => { + const disposable = api.onDidLocationChange((event) => { + setLocation(event.location); + }); + + return () => { + disposable.dispose(); + }; + }); + + const windowContext = React.useMemo(() => { + if (location.type === 'popout') { + return location.getWindow(); + } + return window; + }, [location]); + + return windowContext; +} const components = { default: (props: IDockviewPanelProps<{ title: string }>) => { + const windowContext = usePopoutWindowContext(props.api); + + React.useEffect(() => { + setTimeout(() => { + const a = windowContext.document.createElement('div'); + a.className = 'aaa'; + windowContext.document.body.appendChild(a); + }, 5000); + }, [windowContext]); + + const [reset, setReset] = React.useState(false); + return (
- {props.params.title} + + {!reset && } + {props.api.title}
); }, @@ -31,31 +84,31 @@ function loadDefaultLayout(api: DockviewApi) { component: 'default', }); - api.addPanel({ - id: 'panel_2', - component: 'default', - }); + // api.addPanel({ + // id: 'panel_2', + // component: 'default', + // }); - api.addPanel({ - id: 'panel_3', - component: 'default', - }); + // api.addPanel({ + // id: 'panel_3', + // component: 'default', + // }); - api.addPanel({ - id: 'panel_4', - component: 'default', - }); + // api.addPanel({ + // id: 'panel_4', + // component: 'default', + // }); - api.addPanel({ - id: 'panel_5', - component: 'default', - position: { direction: 'right' }, - }); + // api.addPanel({ + // id: 'panel_5', + // component: 'default', + // position: { direction: 'right' }, + // }); - api.addPanel({ - id: 'panel_6', - component: 'default', - }); + // api.addPanel({ + // id: 'panel_6', + // component: 'default', + // }); } let panelCount = 0; @@ -223,7 +276,7 @@ const RightComponent = (props: IDockviewHeaderActionsProps) => { const group = props.containerApi.addGroup(); props.group.api.moveTo({ group }); } else { - props.containerApi.addPopoutGroup(props.group, { + const window = props.containerApi.addPopoutGroup(props.group, { popoutUrl: '/popout/index.html', }); } diff --git a/packages/docs/sandboxes/popoutgroup-dockview/src/popover.tsx b/packages/docs/sandboxes/popoutgroup-dockview/src/popover.tsx new file mode 100644 index 000000000..3b0b8d1f4 --- /dev/null +++ b/packages/docs/sandboxes/popoutgroup-dockview/src/popover.tsx @@ -0,0 +1,61 @@ +import { useLayer, Arrow } from 'react-laag'; +import { motion, AnimatePresence } from 'framer-motion'; +import * as React from 'react'; +import { DockviewPanelApi } from 'dockview'; + +export function PopoverMenu(props: { api: DockviewPanelApi }) { + const [isOpen, setOpen] = React.useState(false); + + // helper function to close the menu + function close() { + setOpen(false); + } + + const _window = + props.api.location.type === 'popout' + ? props.api.location.getWindow() + : undefined; + + const { renderLayer, triggerProps, layerProps, arrowProps } = useLayer({ + isOpen, + onOutsideClick: close, // close the menu when the user clicks outside + onDisappear: close, // close the menu when the menu gets scrolled out of sight + overflowContainer: false, // keep the menu positioned inside the container + auto: true, // automatically find the best placement + placement: 'top-end', // we prefer to place the menu "top-end" + triggerOffset: 12, // keep some distance to the trigger + containerOffset: 16, // give the menu some room to breath relative to the container + arrowOffset: 16, // let the arrow have some room to breath also, + environment: _window, + container: _window + ? () => { + const el = _window.document.body; + Object.setPrototypeOf(el, HTMLElement.prototype); + return el; + } + : undefined, + // container: props.window.document.body + }); + + // Again, we're using framer-motion for the transition effect + return ( + <> + + {renderLayer( + + {isOpen && ( + +
  • Item 1
  • +
  • Item 2
  • +
  • Item 3
  • +
  • Item 4
  • + +
    + )} +
    + )} + + ); +} From 6274708acb8981dce02da39c16f4545676348c6d Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sun, 28 Jan 2024 13:43:09 +0000 Subject: [PATCH 4/6] feat: align event names --- packages/dockview-core/src/api/component.api.ts | 4 ++-- .../src/dockview/dockviewComponent.ts | 12 ++++++------ .../src/dockview/dockviewPopoutGroupPanel.ts | 8 ++++---- packages/dockview-core/src/popoutWindow.ts | 15 +++++++-------- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index 02f4292a7..f57fa9fa4 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -830,8 +830,8 @@ export class DockviewApi implements CommonApi { options?: { position?: Box; popoutUrl?: string; - onOpened?: (id: string, window: Window) => void; - onClosing?: (id: string, window: Window) => void; + onDidOpen?: (event: { id: string; window: Window }) => void; + onWillClose?: (event: { id: string; window: Window }) => void; } ): void { this.component.addPopoutGroup(item, options); diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index b28dde011..e808bdb79 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -287,8 +287,8 @@ export interface IDockviewComponent extends IBaseGrid { options?: { position?: Box; popoutUrl?: string; - onOpened?: (id: string, window: Window) => void; - onClosing?: (id: string, window: Window) => void; + onDidOpen?: (event: { id: string; window: Window }) => void; + onWillClose?: (event: { id: string; window: Window }) => void; } ): void; } @@ -516,8 +516,8 @@ export class DockviewComponent skipRemoveGroup?: boolean; position?: Box; popoutUrl?: string; - onOpened?: (id: string, window: Window) => void; - onClosing?: (id: string, window: Window) => void; + onDidOpen?: (event: { id: string; window: Window }) => void; + onWillClose?: (event: { id: string; window: Window }) => void; } ): void { let group: DockviewGroupPanel; @@ -566,8 +566,8 @@ export class DockviewComponent width: box.width, height: box.height, }, - onOpened: options?.onOpened, - onClosing: options?.onClosing + onDidOpen: options?.onDidOpen, + onWillClose: options?.onWillClose, } ); diff --git a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts index 371fda793..858178280 100644 --- a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts @@ -13,8 +13,8 @@ export class DockviewPopoutGroupPanel extends CompositeDisposable { className: string; popoutUrl: string; box: Box; - onOpened?: (id: string, window: Window) => void; - onClosing?: (id: string, window: Window) => void; + onDidOpen?: (event: { id: string; window: Window }) => void; + onWillClose?: (event: { id: string; window: Window }) => void; } ) { super(); @@ -25,8 +25,8 @@ export class DockviewPopoutGroupPanel extends CompositeDisposable { top: this.options.box.top, width: this.options.box.width, height: this.options.box.height, - onOpened: this.options.onOpened, - onClosing: this.options.onClosing, + onDidOpen: this.options.onDidOpen, + onWillClose: this.options.onWillClose, }); group.model.location = { diff --git a/packages/dockview-core/src/popoutWindow.ts b/packages/dockview-core/src/popoutWindow.ts index 68dfc6736..3f1531cac 100644 --- a/packages/dockview-core/src/popoutWindow.ts +++ b/packages/dockview-core/src/popoutWindow.ts @@ -5,8 +5,8 @@ import { Box } from './types'; export type PopoutWindowOptions = { url: string; - onOpened?: (id: string, window: Window) => void; - onClosing?: (id: string, window: Window) => void; + onDidOpen?: (event: { id: string; window: Window }) => void; + onWillClose?: (event: { id: string; window: Window }) => void; } & Box; export class PopoutWindow extends CompositeDisposable { @@ -53,9 +53,10 @@ export class PopoutWindow extends CompositeDisposable { if (this._window) { this._onWillClose.fire(); - if (this.options.onClosing) { - this.options.onClosing(this.target, this._window.value); - } + this.options.onWillClose?.({ + id: this.target, + window: this._window.value, + }); this._window.disposable.dispose(); this._window.value.close(); @@ -132,9 +133,7 @@ export class PopoutWindow extends CompositeDisposable { }); }); - if (this.options.onOpened) { - this.options.onOpened(this.target, externalWindow); - } + this.options.onDidOpen?.({ id: this.target, window: externalWindow }); } private createPopoutWindowContainer(): HTMLElement { From 8f9d225c61b1d325ae555e285d4c4f76e553d2cf Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sun, 28 Jan 2024 14:23:22 +0000 Subject: [PATCH 5/6] feat: window popout enhancements --- .../dockview/dockviewComponent.spec.ts | 16 ++ .../dockview-core/src/api/component.api.ts | 4 +- .../src/api/dockviewGroupPanelApi.ts | 13 +- .../dockview-core/src/api/dockviewPanelApi.ts | 11 +- .../src/dockview/dockviewComponent.ts | 167 +++++++++++------- .../src/dockview/dockviewPopoutGroupPanel.ts | 18 +- packages/dockview-core/src/popoutWindow.ts | 61 ++++--- packages/docs/package.json | 1 + .../popoutgroup-dockview/src/app.tsx | 39 ++-- .../popoutgroup-dockview/src/popover.tsx | 14 +- 10 files changed, 190 insertions(+), 154 deletions(-) diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 15e38dd61..af7b18617 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -111,6 +111,13 @@ describe('dockviewComponent', () => { }); describe('memory leakage', () => { + beforeEach(() => { + window.open = () => fromPartial({ + addEventListener: jest.fn(), + close: jest.fn(), + }); + }); + test('event leakage', () => { Emitter.setLeakageMonitorEnabled(true); @@ -4415,6 +4422,15 @@ describe('dockviewComponent', () => { }); describe('popout group', () => { + beforeEach(() => { + jest.spyOn(window, 'open').mockReturnValue( + fromPartial({ + addEventListener: jest.fn(), + close: jest.fn(), + }) + ); + }); + test('that can remove a popout group', () => { const container = document.createElement('div'); diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index f57fa9fa4..49d82f98f 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -833,7 +833,7 @@ export class DockviewApi implements CommonApi { onDidOpen?: (event: { id: string; window: Window }) => void; onWillClose?: (event: { id: string; window: Window }) => void; } - ): void { - this.component.addPopoutGroup(item, options); + ): Promise { + return this.component.addPopoutGroup(item, options); } } diff --git a/packages/dockview-core/src/api/dockviewGroupPanelApi.ts b/packages/dockview-core/src/api/dockviewGroupPanelApi.ts index 1a999301d..a5a8bb371 100644 --- a/packages/dockview-core/src/api/dockviewGroupPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewGroupPanelApi.ts @@ -9,12 +9,9 @@ export interface DockviewGroupPanelApi extends GridviewPanelApi { readonly onDidLocationChange: Event; readonly location: DockviewGroupLocation; /** - * - * If you require the documents Window object you can call `document.defaultView`. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/defaultView + * If you require the Window object */ - getDocument(): Document; + getWindow(): Window; moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void; maximize(): void; isMaximized(): boolean; @@ -49,10 +46,10 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl { this.addDisposables(this._onDidLocationChange); } - getDocument(): Document { + getWindow(): Window { return this.location.type === 'popout' - ? this.location.getWindow().document - : window.document; + ? this.location.getWindow() + : window; } moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void { diff --git a/packages/dockview-core/src/api/dockviewPanelApi.ts b/packages/dockview-core/src/api/dockviewPanelApi.ts index dffca9297..66772bd1d 100644 --- a/packages/dockview-core/src/api/dockviewPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewPanelApi.ts @@ -44,12 +44,9 @@ export interface DockviewPanelApi isMaximized(): boolean; exitMaximized(): void; /** - * - * If you require the documents Window object you can call `document.defaultView`. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/defaultView + * If you require the Window object */ - getDocument(): Document; + getWindow(): Window; } export class DockviewPanelApiImpl @@ -145,8 +142,8 @@ export class DockviewPanelApiImpl ); } - getDocument(): Document { - return this.group.api.getDocument(); + getWindow(): Window { + return this.group.api.getWindow(); } moveTo(options: { diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index e808bdb79..0f6ef22c0 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -12,7 +12,7 @@ import { } from '../dnd/droptarget'; import { tail, sequenceEquals, remove } from '../array'; import { DockviewPanel, IDockviewPanel } from './dockviewPanel'; -import { CompositeDisposable, Disposable } from '../lifecycle'; +import { CompositeDisposable, Disposable, IDisposable } from '../lifecycle'; import { Event, Emitter } from '../events'; import { Watermark } from './components/watermark/watermark'; import { @@ -74,7 +74,7 @@ const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = { size: { type: 'pixels', value: 20 }, }; -function getTheme(element: HTMLElement): string | undefined { +function getDockviewTheme(element: HTMLElement): string | undefined { function toClassList(element: HTMLElement) { const list: string[] = []; @@ -290,7 +290,7 @@ export interface IDockviewComponent extends IBaseGrid { onDidOpen?: (event: { id: string; window: Window }) => void; onWillClose?: (event: { id: string; window: Window }) => void; } - ): void; + ): Promise; } export class DockviewComponent @@ -332,7 +332,11 @@ export class DockviewComponent this._onDidActivePanelChange.event; private readonly _floatingGroups: DockviewFloatingGroupPanel[] = []; - private readonly _popoutGroups: DockviewPopoutGroupPanel[] = []; + private readonly _popoutGroups: { + window: PopoutWindow; + group: DockviewGroupPanel; + disposable: IDisposable; + }[] = []; private readonly _rootDropTarget: Droptarget; get orientation(): Orientation { @@ -413,7 +417,7 @@ export class DockviewComponent // iterate over a copy of the array since .dispose() mutates the original array for (const group of [...this._popoutGroups]) { - group.dispose(); + group.disposable.dispose(); } }) ); @@ -510,7 +514,7 @@ export class DockviewComponent this.updateWatermark(); } - addPopoutGroup( + async addPopoutGroup( item: DockviewPanel | DockviewGroupPanel, options?: { skipRemoveGroup?: boolean; @@ -519,72 +523,108 @@ export class DockviewComponent onDidOpen?: (event: { id: string; window: Window }) => void; onWillClose?: (event: { id: string; window: Window }) => void; } - ): void { - let group: DockviewGroupPanel; - let box: Box | undefined = options?.position; + ): Promise { + const theme = getDockviewTheme(this.gridview.element); - 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 getBox: () => Box = () => { + if (options?.position) { + return options.position; } - const skip = - typeof options?.skipRemoveGroup === 'boolean' && - options.skipRemoveGroup; - - if (!skip) { - this.doRemoveGroup(item, { skipDispose: true }); + if (item instanceof DockviewGroupPanel) { + return item.element.getBoundingClientRect(); } - } - const theme = getTheme(this.gridview.element); + if (item.group) { + return item.group.element.getBoundingClientRect(); + } + return this.element.getBoundingClientRect(); + }; - const popoutWindow = new DockviewPopoutGroupPanel( - `${this.id}-${group.id}`, // globally unique within dockview - group, + const box: Box = getBox(); + + const groupId = + item instanceof DockviewGroupPanel + ? item.id + : this.getNextGroupId(); + + const _window = new PopoutWindow( + `${this.id}-${groupId}`, // globally unique within dockview + theme ?? '', { - className: theme ?? '', - popoutUrl: options?.popoutUrl ?? '/popout.html', - box: { - left: window.screenX + box.left, - top: window.screenY + box.top, - width: box.width, - height: box.height, - }, + url: options?.popoutUrl ?? '/popout.html', + left: window.screenX + box.left, + top: window.screenY + box.top, + width: box.width, + height: box.height, onDidOpen: options?.onDidOpen, onWillClose: options?.onWillClose, } ); - popoutWindow.addDisposables( - { - dispose: () => { - remove(this._popoutGroups, popoutWindow); - this.updateWatermark(); - }, - }, - popoutWindow.window.onDidClose(() => { - this.doAddGroup(group, [0]); + const disposables = new CompositeDisposable( + _window, + _window.onDidClose(() => { + disposables.dispose(); }) ); - this._popoutGroups.push(popoutWindow); - this.updateWatermark(); + const popoutContainer = await _window.open(); + + if (popoutContainer) { + let group: DockviewGroupPanel; + + if (item instanceof DockviewPanel) { + group = this.createGroup({ id: groupId }); + + this.removePanel(item, { + removeEmptyGroup: true, + skipDispose: true, + }); + + group.model.openPanel(item); + } else { + group = item; + + const skip = + typeof options?.skipRemoveGroup === 'boolean' && + options.skipRemoveGroup; + + if (!skip) { + this.doRemoveGroup(item, { skipDispose: true }); + } + } + + popoutContainer.appendChild(group.element); + + group.model.location = { + type: 'popout', + getWindow: () => _window.window!, + }; + + const value = { window: _window, group, disposable: disposables }; + + disposables.addDisposables( + { + dispose: () => { + group.model.location = { type: 'grid' }; + + remove(this._popoutGroups, value); + this.updateWatermark(); + }, + }, + _window.onDidClose(() => { + this.doAddGroup(group, [0]); + }) + ); + + this._popoutGroups.push(value); + this.updateWatermark(); + return true; + } else { + disposables.dispose(); + return false; + } } addFloatingGroup( @@ -1428,7 +1468,7 @@ export class DockviewComponent this._onDidRemoveGroup.fire(group); } - selectedGroup.dispose(); + selectedGroup.disposable.dispose(); if (!options?.skipActive && this._activeGroup === group) { const groups = Array.from(this._groups.values()); @@ -1595,7 +1635,7 @@ export class DockviewComponent if (!selectedPopoutGroup) { throw new Error('failed to find popout group'); } - selectedPopoutGroup.dispose(); + selectedPopoutGroup.disposable.dispose(); } } @@ -1629,6 +1669,15 @@ export class DockviewComponent } } + private getNextGroupId(): string { + let id = this.nextGroupId.next(); + while (this._groups.has(id)) { + id = this.nextGroupId.next(); + } + + return id; + } + createGroup(options?: GroupOptions): DockviewGroupPanel { if (!options) { options = {}; diff --git a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts index 858178280..3116b56d9 100644 --- a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts @@ -1,14 +1,12 @@ 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 id: string, - readonly group: DockviewGroupPanel, private readonly options: { className: string; popoutUrl: string; @@ -29,23 +27,17 @@ export class DockviewPopoutGroupPanel extends CompositeDisposable { onWillClose: this.options.onWillClose, }); - group.model.location = { - type: 'popout', - getWindow: () => this.window.window!, - }; - this.addDisposables( this.window, - { - dispose: () => { - group.model.location = { type: 'grid' }; - }, - }, this.window.onDidClose(() => { this.dispose(); }) ); + } - this.window.open(group.element); + open(): Promise { + const didOpen = this.window.open(); + + return didOpen; } } diff --git a/packages/dockview-core/src/popoutWindow.ts b/packages/dockview-core/src/popoutWindow.ts index 3f1531cac..33d3a1434 100644 --- a/packages/dockview-core/src/popoutWindow.ts +++ b/packages/dockview-core/src/popoutWindow.ts @@ -66,7 +66,7 @@ export class PopoutWindow extends CompositeDisposable { } } - open(content: HTMLElement): void { + async open(): Promise { if (this._window) { throw new Error('instance of popout window is already open'); } @@ -88,9 +88,13 @@ export class PopoutWindow extends CompositeDisposable { const externalWindow = window.open(url, this.target, features); if (!externalWindow) { - return; + /** + * Popup blocked + */ + return null; } + const disposable = new CompositeDisposable(); this._window = { value: externalWindow, disposable }; @@ -104,36 +108,41 @@ export class PopoutWindow extends CompositeDisposable { * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event */ this.close(); - }) - ); - - externalWindow.addEventListener('load', () => { - /** - * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event - */ - - const externalDocument = externalWindow.document; - externalDocument.title = document.title; - - const container = this.createPopoutWindowContainer(); - container.classList.add(this.className); - container.appendChild(content); - - // externalDocument.body.replaceChildren(container); - externalDocument.body.appendChild(container); - externalDocument.body.classList.add(this.className); - - addStyles(externalDocument, window.document.styleSheets); - - externalWindow.addEventListener('beforeunload', () => { + }), + addDisposableWindowListener(externalWindow, 'beforeunload', () => { /** * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event */ this.close(); - }); + }) + ); + + const container = this.createPopoutWindowContainer(); + container.classList.add(this.className); + + this.options.onDidOpen?.({ + id: this.target, + window: externalWindow, }); - this.options.onDidOpen?.({ id: this.target, window: externalWindow }); + return new Promise((resolve) => { + externalWindow.addEventListener('load', () => { + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event + */ + + const externalDocument = externalWindow.document; + externalDocument.title = document.title; + + // externalDocument.body.replaceChildren(container); + externalDocument.body.appendChild(container); + externalDocument.body.classList.add(this.className); + + addStyles(externalDocument, window.document.styleSheets); + + resolve(container); + }); + }); } private createPopoutWindowContainer(): HTMLElement { diff --git a/packages/docs/package.json b/packages/docs/package.json index efc9d4128..6f40f715e 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -34,6 +34,7 @@ "dockview": "^1.9.2", "prism-react-renderer": "^2.3.1", "react-dnd": "^16.0.1", + "react-laag": "^2.0.5", "recoil": "^0.7.7", "source-map-loader": "^4.0.2", "uuid": "^9.0.1" diff --git a/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx b/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx index 57fe7e8be..fbca7560d 100644 --- a/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx +++ b/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx @@ -6,49 +6,30 @@ import { IDockviewPanelProps, SerializedDockview, DockviewPanelApi, - DockviewGroupLocation, } from 'dockview'; import * as React from 'react'; -import * as ReactDOM from 'react-dom'; import { Icon } from './utils'; import { PopoverMenu } from './popover'; -function usePopoutWindowContext(api: DockviewPanelApi): Window { - const [location, setLocation] = React.useState( - api.location - ); +function usePanelWindowObject(api: DockviewPanelApi): Window { + const [document, setDocument] = React.useState(api.getWindow()); React.useEffect(() => { const disposable = api.onDidLocationChange((event) => { - setLocation(event.location); + setDocument(api.getWindow()); }); return () => { disposable.dispose(); }; - }); + }, [api]); - const windowContext = React.useMemo(() => { - if (location.type === 'popout') { - return location.getWindow(); - } - return window; - }, [location]); - - return windowContext; + return document; } const components = { default: (props: IDockviewPanelProps<{ title: string }>) => { - const windowContext = usePopoutWindowContext(props.api); - - React.useEffect(() => { - setTimeout(() => { - const a = windowContext.document.createElement('div'); - a.className = 'aaa'; - windowContext.document.body.appendChild(a); - }, 5000); - }, [windowContext]); + const _window = usePanelWindowObject(props.api); const [reset, setReset] = React.useState(false); @@ -62,7 +43,7 @@ const components = { > - {!reset && } + {!reset && } {props.api.title} ); @@ -258,12 +239,12 @@ const LeftComponent = (props: IDockviewHeaderActionsProps) => { const RightComponent = (props: IDockviewHeaderActionsProps) => { const [popout, setPopout] = React.useState( - props.api.location === 'popout' + props.api.location.type === 'popout' ); React.useEffect(() => { const disposable = props.group.api.onDidLocationChange((event) => [ - setPopout(event.location === 'popout'), + setPopout(event.location.type === 'popout'), ]); return () => { diff --git a/packages/docs/sandboxes/popoutgroup-dockview/src/popover.tsx b/packages/docs/sandboxes/popoutgroup-dockview/src/popover.tsx index 3b0b8d1f4..9d9663443 100644 --- a/packages/docs/sandboxes/popoutgroup-dockview/src/popover.tsx +++ b/packages/docs/sandboxes/popoutgroup-dockview/src/popover.tsx @@ -3,7 +3,7 @@ import { motion, AnimatePresence } from 'framer-motion'; import * as React from 'react'; import { DockviewPanelApi } from 'dockview'; -export function PopoverMenu(props: { api: DockviewPanelApi }) { +export function PopoverMenu(props: { window: Window }) { const [isOpen, setOpen] = React.useState(false); // helper function to close the menu @@ -11,11 +11,6 @@ export function PopoverMenu(props: { api: DockviewPanelApi }) { setOpen(false); } - const _window = - props.api.location.type === 'popout' - ? props.api.location.getWindow() - : undefined; - const { renderLayer, triggerProps, layerProps, arrowProps } = useLayer({ isOpen, onOutsideClick: close, // close the menu when the user clicks outside @@ -26,15 +21,14 @@ export function PopoverMenu(props: { api: DockviewPanelApi }) { triggerOffset: 12, // keep some distance to the trigger containerOffset: 16, // give the menu some room to breath relative to the container arrowOffset: 16, // let the arrow have some room to breath also, - environment: _window, - container: _window + environment: props.window, + container: props.window ? () => { - const el = _window.document.body; + const el = props.window.document.body; Object.setPrototypeOf(el, HTMLElement.prototype); return el; } : undefined, - // container: props.window.document.body }); // Again, we're using framer-motion for the transition effect From 20c1a66d205855e0b85d2fd457a1047e6d6a77b6 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Mon, 29 Jan 2024 20:57:15 +0000 Subject: [PATCH 6/6] feat: popout group enhancements --- .../dockview/dockviewComponent.spec.ts | 214 ++++++++++-------- .../gridview/gridviewComponent.spec.ts | 2 +- .../dockview-core/src/api/component.api.ts | 2 +- packages/dockview-core/src/api/panelApi.ts | 59 +++-- .../components/titlebar/tabsContainer.ts | 3 +- .../src/dockview/dockviewComponent.ts | 186 +++++++++------ .../src/dockview/dockviewGroupPanelModel.ts | 1 + .../src/dockview/dockviewPopoutGroupPanel.ts | 43 ---- .../dockview-core/src/gridview/gridview.ts | 28 ++- .../src/gridview/gridviewPanel.ts | 13 +- packages/dockview-core/src/index.ts | 2 - packages/dockview-core/src/lifecycle.ts | 10 +- packages/dockview-core/src/popoutWindow.ts | 34 ++- .../src/splitview/splitviewPanel.ts | 6 +- packages/dockview/src/gridview/view.ts | 5 +- 15 files changed, 335 insertions(+), 273 deletions(-) delete mode 100644 packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index af7b18617..bdf032b03 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -110,109 +110,109 @@ describe('dockviewComponent', () => { window.open = jest.fn(); // not implemented by jest }); - describe('memory leakage', () => { - beforeEach(() => { - window.open = () => fromPartial({ - addEventListener: jest.fn(), - close: jest.fn(), - }); - }); + // describe('memory leakage', () => { + // beforeEach(() => { + // window.open = () => fromPartial({ + // addEventListener: jest.fn(), + // close: jest.fn(), + // }); + // }); - test('event leakage', () => { - Emitter.setLeakageMonitorEnabled(true); + // test('event leakage', () => { + // Emitter.setLeakageMonitorEnabled(true); - dockview = new DockviewComponent({ - parentElement: container, - components: { - default: PanelContentPartTest, - }, - }); + // dockview = new DockviewComponent({ + // parentElement: container, + // components: { + // default: PanelContentPartTest, + // }, + // }); - dockview.layout(500, 1000); + // dockview.layout(500, 1000); - const panel1 = dockview.addPanel({ - id: 'panel1', - component: 'default', - }); + // const panel1 = dockview.addPanel({ + // id: 'panel1', + // component: 'default', + // }); - const panel2 = dockview.addPanel({ - id: 'panel2', - component: 'default', - }); + // const panel2 = dockview.addPanel({ + // id: 'panel2', + // component: 'default', + // }); - dockview.removePanel(panel2); + // dockview.removePanel(panel2); - const panel3 = dockview.addPanel({ - id: 'panel3', - component: 'default', - position: { - direction: 'right', - referencePanel: 'panel1', - }, - }); + // const panel3 = dockview.addPanel({ + // id: 'panel3', + // component: 'default', + // position: { + // direction: 'right', + // referencePanel: 'panel1', + // }, + // }); - const panel4 = dockview.addPanel({ - id: 'panel4', - component: 'default', - position: { - direction: 'above', - }, - }); + // const panel4 = dockview.addPanel({ + // id: 'panel4', + // component: 'default', + // position: { + // direction: 'above', + // }, + // }); - dockview.moveGroupOrPanel( - panel4.group, - panel3.group.id, - panel3.id, - 'center' - ); + // dockview.moveGroupOrPanel( + // panel4.group, + // panel3.group.id, + // panel3.id, + // 'center' + // ); - dockview.addPanel({ - id: 'panel5', - component: 'default', - floating: true, - }); + // dockview.addPanel({ + // id: 'panel5', + // component: 'default', + // floating: true, + // }); - const panel6 = dockview.addPanel({ - id: 'panel6', - component: 'default', - position: { - referencePanel: 'panel5', - direction: 'within', - }, - }); + // const panel6 = dockview.addPanel({ + // id: 'panel6', + // component: 'default', + // position: { + // referencePanel: 'panel5', + // direction: 'within', + // }, + // }); - dockview.addFloatingGroup(panel4.api.group); + // dockview.addFloatingGroup(panel4.api.group); - dockview.addPopoutGroup(panel6); + // dockview.addPopoutGroup(panel6); - dockview.moveGroupOrPanel( - panel1.group, - panel6.group.id, - panel6.id, - 'center' - ); + // dockview.moveGroupOrPanel( + // panel1.group, + // panel6.group.id, + // panel6.id, + // 'center' + // ); - dockview.moveGroupOrPanel( - panel4.group, - panel6.group.id, - panel6.id, - 'center' - ); + // dockview.moveGroupOrPanel( + // panel4.group, + // panel6.group.id, + // panel6.id, + // 'center' + // ); - dockview.dispose(); + // dockview.dispose(); - if (Emitter.MEMORY_LEAK_WATCHER.size > 0) { - for (const entry of Array.from( - Emitter.MEMORY_LEAK_WATCHER.events - )) { - console.log('disposal', entry[1]); - } - throw new Error('not all listeners disposed'); - } + // if (Emitter.MEMORY_LEAK_WATCHER.size > 0) { + // for (const entry of Array.from( + // Emitter.MEMORY_LEAK_WATCHER.events + // )) { + // console.log('disposal', entry[1]); + // } + // throw new Error('not all listeners disposed'); + // } - Emitter.setLeakageMonitorEnabled(false); - }); - }); + // Emitter.setLeakageMonitorEnabled(false); + // }); + // }); test('duplicate panel', () => { dockview.layout(500, 1000); @@ -4425,13 +4425,22 @@ describe('dockviewComponent', () => { beforeEach(() => { jest.spyOn(window, 'open').mockReturnValue( fromPartial({ - addEventListener: jest.fn(), + document: fromPartial({ + body: document.createElement('body'), + }), + addEventListener: jest + .fn() + .mockImplementation((name, cb) => { + if (name === 'load') { + cb(); + } + }), close: jest.fn(), }) ); }); - test('that can remove a popout group', () => { + test('that can remove a popout group', async () => { const container = document.createElement('div'); const dockview = new DockviewComponent({ @@ -4452,10 +4461,10 @@ describe('dockviewComponent', () => { component: 'default', }); - dockview.addPopoutGroup(panel1); + await dockview.addPopoutGroup(panel1); expect(dockview.panels.length).toBe(1); - expect(dockview.groups.length).toBe(1); + expect(dockview.groups.length).toBe(2); expect(panel1.api.group.api.location.type).toBe('popout'); dockview.removePanel(panel1); @@ -4464,7 +4473,7 @@ describe('dockviewComponent', () => { expect(dockview.groups.length).toBe(0); }); - test('add a popout group', () => { + test('add a popout group', async () => { const container = document.createElement('div'); const dockview = new DockviewComponent({ @@ -4495,15 +4504,15 @@ describe('dockviewComponent', () => { expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); - dockview.addPopoutGroup(panel2.group); + await dockview.addPopoutGroup(panel2.group); expect(panel1.group.api.location.type).toBe('popout'); expect(panel2.group.api.location.type).toBe('popout'); - expect(dockview.groups.length).toBe(1); + expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); }); - test('move from fixed to popout group and back', () => { + test('move from fixed to popout group and back', async () => { const container = document.createElement('div'); const dockview = new DockviewComponent({ @@ -4543,12 +4552,12 @@ describe('dockviewComponent', () => { expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); - dockview.addPopoutGroup(panel2.group); + await dockview.addPopoutGroup(panel2.group); expect(panel1.group.api.location.type).toBe('popout'); expect(panel2.group.api.location.type).toBe('popout'); expect(panel3.group.api.location.type).toBe('grid'); - expect(dockview.groups.length).toBe(2); + expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); dockview.moveGroupOrPanel( @@ -4561,7 +4570,20 @@ describe('dockviewComponent', () => { expect(panel1.group.api.location.type).toBe('popout'); expect(panel2.group.api.location.type).toBe('grid'); expect(panel3.group.api.location.type).toBe('grid'); - expect(dockview.groups.length).toBe(3); + expect(dockview.groups.length).toBe(4); + expect(dockview.panels.length).toBe(3); + + dockview.moveGroupOrPanel( + panel3.api.group, + panel1.api.group.id, + panel1.api.id, + 'center' + ); + + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('grid'); + expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); }); diff --git a/packages/dockview-core/src/__tests__/gridview/gridviewComponent.spec.ts b/packages/dockview-core/src/__tests__/gridview/gridviewComponent.spec.ts index c54341cc0..d0873eba0 100644 --- a/packages/dockview-core/src/__tests__/gridview/gridviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/gridview/gridviewComponent.spec.ts @@ -268,7 +268,7 @@ describe('gridview', () => { ], }, }, - activePanel: 'panel_1', + activePanel: 'panel_2', }); }); diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index 49d82f98f..e5faa8cd5 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -833,7 +833,7 @@ export class DockviewApi implements CommonApi { onDidOpen?: (event: { id: string; window: Window }) => void; onWillClose?: (event: { id: string; window: Window }) => void; } - ): Promise { + ): Promise { return this.component.addPopoutGroup(item, options); } } diff --git a/packages/dockview-core/src/api/panelApi.ts b/packages/dockview-core/src/api/panelApi.ts index 95d35a3b8..57e80f214 100644 --- a/packages/dockview-core/src/api/panelApi.ts +++ b/packages/dockview-core/src/api/panelApi.ts @@ -14,6 +14,10 @@ export interface VisibilityEvent { readonly isVisible: boolean; } +export interface HiddenEvent { + readonly isHidden: boolean; +} + export interface ActiveEvent { readonly isActive: boolean; } @@ -24,7 +28,7 @@ export interface PanelApi { readonly onDidFocusChange: Event; readonly onDidVisibilityChange: Event; readonly onDidActiveChange: Event; - setVisible(isVisible: boolean): void; + readonly onDidHiddenChange: Event; setActive(): void; updateParameters(parameters: Parameters): void; /** @@ -43,6 +47,10 @@ export interface PanelApi { * Whether the panel is visible */ readonly isVisible: boolean; + /** + * Whether the panel is hidden + */ + readonly isHidden: boolean; /** * The panel width in pixels */ @@ -60,6 +68,7 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi { private _isFocused = false; private _isActive = false; private _isVisible = true; + private _isHidden = false; private _width = 0; private _height = 0; @@ -69,56 +78,59 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi { replay: true, }); readonly onDidDimensionsChange = this._onDidDimensionChange.event; - // + readonly _onDidChangeFocus = new Emitter({ replay: true, }); readonly onDidFocusChange: Event = this._onDidChangeFocus.event; - // + readonly _onFocusEvent = new Emitter(); readonly onFocusEvent: Event = this._onFocusEvent.event; - // + readonly _onDidVisibilityChange = new Emitter({ replay: true, }); readonly onDidVisibilityChange: Event = this._onDidVisibilityChange.event; - // - readonly _onVisibilityChange = new Emitter(); - readonly onVisibilityChange: Event = - this._onVisibilityChange.event; - // + readonly _onDidHiddenChange = new Emitter(); + readonly onDidHiddenChange: Event = + this._onDidHiddenChange.event; + readonly _onDidActiveChange = new Emitter({ replay: true, }); readonly onDidActiveChange: Event = this._onDidActiveChange.event; - // + readonly _onActiveChange = new Emitter(); readonly onActiveChange: Event = this._onActiveChange.event; - // + readonly _onUpdateParameters = new Emitter(); readonly onUpdateParameters: Event = this._onUpdateParameters.event; - // - get isFocused() { + get isFocused(): boolean { return this._isFocused; } - get isActive() { + get isActive(): boolean { return this._isActive; } - get isVisible() { + + get isVisible(): boolean { return this._isVisible; } - get width() { + get isHidden(): boolean { + return this._isHidden; + } + + get width(): number { return this._width; } - get height() { + get height(): number { return this._height; } @@ -135,6 +147,9 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi { this.onDidVisibilityChange((event) => { this._isVisible = event.isVisible; }), + this.onDidHiddenChange((event) => { + this._isHidden = event.isHidden; + }), this.onDidDimensionsChange((event) => { this._width = event.width; this._height = event.height; @@ -146,7 +161,7 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi { this._onDidActiveChange, this._onFocusEvent, this._onActiveChange, - this._onVisibilityChange, + this._onDidHiddenChange, this._onUpdateParameters ); } @@ -161,8 +176,8 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi { ); } - setVisible(isVisible: boolean) { - this._onVisibilityChange.fire({ isVisible }); + setHidden(isHidden: boolean): void { + this._onDidHiddenChange.fire({ isHidden }); } setActive(): void { @@ -172,8 +187,4 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi { updateParameters(parameters: Parameters): void { this._onUpdateParameters.fire(parameters); } - - dispose() { - 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 6a7caac29..60ae6ed23 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -350,7 +350,8 @@ export class TabsContainer !this.accessor.options.disableFloatingGroups; const isFloatingWithOnePanel = - this.group.api.location.type === 'floating' && this.size === 1; + this.group.api.location.type === '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 0f6ef22c0..3366da217 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -58,7 +58,6 @@ import { TabDragEvent, } from './components/titlebar/tabsContainer'; import { Box } from '../types'; -import { DockviewPopoutGroupPanel } from './dockviewPopoutGroupPanel'; import { DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, DEFAULT_FLOATING_GROUP_POSITION, @@ -290,7 +289,7 @@ export interface IDockviewComponent extends IBaseGrid { onDidOpen?: (event: { id: string; window: Window }) => void; onWillClose?: (event: { id: string; window: Window }) => void; } - ): Promise; + ): Promise; } export class DockviewComponent @@ -334,7 +333,8 @@ export class DockviewComponent private readonly _floatingGroups: DockviewFloatingGroupPanel[] = []; private readonly _popoutGroups: { window: PopoutWindow; - group: DockviewGroupPanel; + popoutGroup: DockviewGroupPanel; + referenceGroup: DockviewGroupPanel; disposable: IDisposable; }[] = []; private readonly _rootDropTarget: Droptarget; @@ -514,7 +514,7 @@ export class DockviewComponent this.updateWatermark(); } - async addPopoutGroup( + addPopoutGroup( item: DockviewPanel | DockviewGroupPanel, options?: { skipRemoveGroup?: boolean; @@ -523,10 +523,28 @@ export class DockviewComponent onDidOpen?: (event: { id: string; window: Window }) => void; onWillClose?: (event: { id: string; window: Window }) => void; } - ): Promise { - const theme = getDockviewTheme(this.gridview.element); + ): Promise { + if (item instanceof DockviewPanel && item.group.size === 1) { + return this.addPopoutGroup(item.group); + } - const getBox: () => Box = () => { + const theme = getDockviewTheme(this.gridview.element); + const element = this.element; + + function moveGroupWithoutDestroying(options: { + from: DockviewGroupPanel; + to: DockviewGroupPanel; + }) { + const panels = [...options.from.panels].map((panel) => + options.from.model.removePanel(panel) + ); + + panels.forEach((panel) => { + options.to.model.openPanel(panel); + }); + } + + function getBox(): Box { if (options?.position) { return options.position; } @@ -538,18 +556,17 @@ export class DockviewComponent if (item.group) { return item.group.element.getBoundingClientRect(); } - return this.element.getBoundingClientRect(); - }; + return element.getBoundingClientRect(); + } const box: Box = getBox(); - const groupId = - item instanceof DockviewGroupPanel - ? item.id - : this.getNextGroupId(); + const groupId = this.getNextGroupId(); //item.id; + + item.api.setHidden(true); const _window = new PopoutWindow( - `${this.id}-${groupId}`, // globally unique within dockview + `${this.id}-${groupId}`, // unique id theme ?? '', { url: options?.popoutUrl ?? '/popout.html', @@ -562,69 +579,85 @@ export class DockviewComponent } ); - const disposables = new CompositeDisposable( + const popoutWindowDisposable = new CompositeDisposable( _window, _window.onDidClose(() => { - disposables.dispose(); + popoutWindowDisposable.dispose(); }) ); - const popoutContainer = await _window.open(); - - if (popoutContainer) { - let group: DockviewGroupPanel; - - if (item instanceof DockviewPanel) { - group = this.createGroup({ id: groupId }); - - this.removePanel(item, { - removeEmptyGroup: true, - skipDispose: true, - }); - - group.model.openPanel(item); - } else { - group = item; - - const skip = - typeof options?.skipRemoveGroup === 'boolean' && - options.skipRemoveGroup; - - if (!skip) { - this.doRemoveGroup(item, { skipDispose: true }); + return _window + .open() + .then((popoutContainer) => { + if (_window.isDisposed) { + return; } - } - popoutContainer.appendChild(group.element); + if (popoutContainer === null) { + popoutWindowDisposable.dispose(); + return; + } - group.model.location = { - type: 'popout', - getWindow: () => _window.window!, - }; + const referenceGroup = + item instanceof DockviewPanel ? item.group : item; - const value = { window: _window, group, disposable: disposables }; + const group = this.createGroup({ id: groupId }); - disposables.addDisposables( - { - dispose: () => { - group.model.location = { type: 'grid' }; + if (item instanceof DockviewPanel) { + const panel = referenceGroup.model.removePanel(item); + group.model.openPanel(panel); + } else { + moveGroupWithoutDestroying({ + from: referenceGroup, + to: group, + }); + referenceGroup.api.setHidden(false); + } - remove(this._popoutGroups, value); - this.updateWatermark(); - }, - }, - _window.onDidClose(() => { - this.doAddGroup(group, [0]); - }) - ); + popoutContainer.appendChild(group.element); - this._popoutGroups.push(value); - this.updateWatermark(); - return true; - } else { - disposables.dispose(); - return false; - } + group.model.location = { + type: 'popout', + getWindow: () => _window.window!, + }; + + const value = { + window: _window, + popoutGroup: group, + referenceGroup, + disposable: popoutWindowDisposable, + }; + + popoutWindowDisposable.addDisposables( + Disposable.from(() => { + if (this.getPanel(referenceGroup.id)) { + moveGroupWithoutDestroying({ + from: group, + to: referenceGroup, + }); + + if (referenceGroup.api.isHidden) { + referenceGroup.api.setHidden(false); + } + + this.doRemoveGroup(group); + } else { + const removedGroup = this.doRemoveGroup(group, { + skipDispose: true, + skipActive: true, + }); + removedGroup.model.location = { type: 'grid' }; + this.doAddGroup(removedGroup, [0]); + } + }) + ); + + this._popoutGroups.push(value); + this.updateWatermark(); + }) + .catch((err) => { + console.error(err); + }); } addFloatingGroup( @@ -923,7 +956,7 @@ export class DockviewComponent const popoutGroups: SerializedPopoutGroup[] = this._popoutGroups.map( (group) => { return { - data: group.group.toJSON() as GroupPanelViewState, + data: group.popoutGroup.toJSON() as GroupPanelViewState, position: group.window.dimensions(), }; } @@ -1307,8 +1340,9 @@ export class DockviewComponent private updateWatermark(): void { if ( - this.groups.filter((x) => x.api.location.type === 'grid').length === - 0 + this.groups.filter( + (x) => x.api.location.type === 'grid' && !x.api.isHidden + ).length === 0 ) { if (!this.watermark) { this.watermark = this.createWatermarkComponent(); @@ -1458,12 +1492,14 @@ export class DockviewComponent if (group.api.location.type === 'popout') { const selectedGroup = this._popoutGroups.find( - (_) => _.group === group + (_) => _.popoutGroup === group ); if (selectedGroup) { if (!options?.skipDispose) { - selectedGroup.group.dispose(); + this.doRemoveGroup(selectedGroup.referenceGroup); + + selectedGroup.popoutGroup.dispose(); this._groups.delete(group.id); this._onDidRemoveGroup.fire(group); } @@ -1478,7 +1514,8 @@ export class DockviewComponent ); } - return selectedGroup.group; + this.updateWatermark(); + return selectedGroup.popoutGroup; } throw new Error('failed to find popout group'); @@ -1630,7 +1667,7 @@ export class DockviewComponent } case 'popout': { const selectedPopoutGroup = this._popoutGroups.find( - (x) => x.group === sourceGroup + (x) => x.popoutGroup === sourceGroup ); if (!selectedPopoutGroup) { throw new Error('failed to find popout group'); @@ -1700,7 +1737,7 @@ export class DockviewComponent } const view = new DockviewGroupPanel(this, id, options); - view.init({ params: {}, accessor: null }); // required to initialized .part and allow for correct disposal of group + view.init({ params: {}, accessor: this }); if (!this._groups.has(view.id)) { const disposable = new CompositeDisposable( @@ -1735,8 +1772,7 @@ export class DockviewComponent this._groups.set(view.id, { value: view, disposable }); } - // TODO: must be called after the above listeners have been setup, - // not an ideal pattern + // TODO: must be called after the above listeners have been setup, not an ideal pattern view.initialize(); return view; diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index a80a7baa0..120bc1b87 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -838,6 +838,7 @@ export class DockviewGroupPanelModel this.watermark?.element.remove(); this.watermark?.dispose?.(); + this.watermark = undefined; for (const panel of this.panels) { panel.dispose(); diff --git a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts deleted file mode 100644 index 3116b56d9..000000000 --- a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { CompositeDisposable } from '../lifecycle'; -import { PopoutWindow } from '../popoutWindow'; -import { Box } from '../types'; - -export class DockviewPopoutGroupPanel extends CompositeDisposable { - readonly window: PopoutWindow; - - constructor( - readonly id: string, - private readonly options: { - className: string; - popoutUrl: string; - box: Box; - onDidOpen?: (event: { id: string; window: Window }) => void; - onWillClose?: (event: { id: string; window: Window }) => void; - } - ) { - super(); - - this.window = new PopoutWindow(id, 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, - onDidOpen: this.options.onDidOpen, - onWillClose: this.options.onWillClose, - }); - - this.addDisposables( - this.window, - this.window.onDidClose(() => { - this.dispose(); - }) - ); - } - - open(): Promise { - const didOpen = this.window.open(); - - return didOpen; - } -} diff --git a/packages/dockview-core/src/gridview/gridview.ts b/packages/dockview-core/src/gridview/gridview.ts index f34fe2672..48138d1e8 100644 --- a/packages/dockview-core/src/gridview/gridview.ts +++ b/packages/dockview-core/src/gridview/gridview.ts @@ -273,7 +273,9 @@ export class Gridview implements IDisposable { readonly element: HTMLElement; private _root: BranchNode | undefined; - private _maximizedNode: LeafNode | undefined = undefined; + private _maximizedNode: + | { leaf: LeafNode; hiddenOnMaximize: LeafNode[] } + | undefined = undefined; private readonly disposable: MutableDisposable = new MutableDisposable(); private readonly _onDidChange = new Emitter<{ @@ -329,7 +331,7 @@ export class Gridview implements IDisposable { } maximizedView(): IGridView | undefined { - return this._maximizedNode?.view; + return this._maximizedNode?.leaf.view; } hasMaximizedView(): boolean { @@ -344,7 +346,7 @@ export class Gridview implements IDisposable { return; } - if (this._maximizedNode === node) { + if (this._maximizedNode?.leaf === node) { return; } @@ -352,12 +354,18 @@ export class Gridview implements IDisposable { this.exitMaximizedView(); } + const hiddenOnMaximize: LeafNode[] = []; + function hideAllViewsBut(parent: BranchNode, exclude: LeafNode): void { for (let i = 0; i < parent.children.length; i++) { const child = parent.children[i]; if (child instanceof LeafNode) { if (child !== exclude) { - parent.setChildVisible(i, false); + if (parent.isChildVisible(i)) { + parent.setChildVisible(i, false); + } else { + hiddenOnMaximize.push(child); + } } } else { hideAllViewsBut(child, exclude); @@ -366,7 +374,7 @@ export class Gridview implements IDisposable { } hideAllViewsBut(this.root, node); - this._maximizedNode = node; + this._maximizedNode = { leaf: node, hiddenOnMaximize }; this._onDidMaxmizedNodeChange.fire(); } @@ -375,11 +383,15 @@ export class Gridview implements IDisposable { return; } + const hiddenOnMaximize = this._maximizedNode.hiddenOnMaximize; + function showViewsInReverseOrder(parent: BranchNode): void { for (let index = parent.children.length - 1; index >= 0; index--) { const child = parent.children[index]; if (child instanceof LeafNode) { - parent.setChildVisible(index, true); + if (!hiddenOnMaximize.includes(child)) { + parent.setChildVisible(index, true); + } } else { showViewsInReverseOrder(child); } @@ -395,8 +407,8 @@ export class Gridview implements IDisposable { public serialize(): SerializedGridview { if (this.hasMaximizedView()) { /** - * do not persist maximized view state but we must first exit any maximized views - * before serialization to ensure the correct dimensions are persisted + * do not persist maximized view state + * firstly exit any maximized views to ensure the correct dimensions are persisted */ this.exitMaximizedView(); } diff --git a/packages/dockview-core/src/gridview/gridviewPanel.ts b/packages/dockview-core/src/gridview/gridviewPanel.ts index b35758287..c2573bde8 100644 --- a/packages/dockview-core/src/gridview/gridviewPanel.ts +++ b/packages/dockview-core/src/gridview/gridviewPanel.ts @@ -16,6 +16,7 @@ import { import { LayoutPriority } from '../splitview/splitview'; import { Emitter, Event } from '../events'; import { IViewSize } from './gridview'; +import { BaseGrid, IGridPanelView } from './baseComponentGridview'; export interface GridviewInitParameters extends PanelInitParameters { minimumWidth?: number; @@ -24,7 +25,7 @@ export interface GridviewInitParameters extends PanelInitParameters { maximumHeight?: number; priority?: LayoutPriority; snap?: boolean; - accessor: GridviewComponent; + accessor: BaseGrid; isVisible?: boolean; } @@ -157,14 +158,16 @@ export abstract class GridviewPanel< this.api.initialize(this); // TODO: required to by-pass 'super before this' requirement this.addDisposables( - this.api.onVisibilityChange((event) => { - const { isVisible } = event; + this.api.onDidHiddenChange((event) => { + const { isHidden } = event; const { accessor } = this._params as GridviewInitParameters; - accessor.setVisible(this, isVisible); + + accessor.setVisible(this, !isHidden); }), this.api.onActiveChange(() => { const { accessor } = this._params as GridviewInitParameters; - accessor.setActive(this); + + accessor.doSetGroupActive(this); }), this.api.onDidConstraintsChangeInternal((event) => { if ( diff --git a/packages/dockview-core/src/index.ts b/packages/dockview-core/src/index.ts index 3f5c8bf70..62d415174 100644 --- a/packages/dockview-core/src/index.ts +++ b/packages/dockview-core/src/index.ts @@ -11,8 +11,6 @@ export { CompositeDisposable as DockviewCompositeDisposable, } from './lifecycle'; -export { PopoutWindow } from './popoutWindow'; - export * from './panel/types'; export * from './panel/componentFactory'; diff --git a/packages/dockview-core/src/lifecycle.ts b/packages/dockview-core/src/lifecycle.ts index 69936fff2..439b181c1 100644 --- a/packages/dockview-core/src/lifecycle.ts +++ b/packages/dockview-core/src/lifecycle.ts @@ -24,10 +24,10 @@ export namespace Disposable { } export class CompositeDisposable { - private readonly _disposables: IDisposable[]; + private _disposables: IDisposable[]; private _isDisposed = false; - protected get isDisposed(): boolean { + get isDisposed(): boolean { return this._isDisposed; } @@ -40,9 +40,13 @@ export class CompositeDisposable { } public dispose(): void { - this._disposables.forEach((arg) => arg.dispose()); + if (this._isDisposed) { + return; + } this._isDisposed = true; + this._disposables.forEach((arg) => arg.dispose()); + this._disposables = []; } } diff --git a/packages/dockview-core/src/popoutWindow.ts b/packages/dockview-core/src/popoutWindow.ts index 33d3a1434..b289ba645 100644 --- a/packages/dockview-core/src/popoutWindow.ts +++ b/packages/dockview-core/src/popoutWindow.ts @@ -94,7 +94,6 @@ export class PopoutWindow extends CompositeDisposable { return null; } - const disposable = new CompositeDisposable(); this._window = { value: externalWindow, disposable }; @@ -108,17 +107,14 @@ export class PopoutWindow extends CompositeDisposable { * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event */ this.close(); - }), - addDisposableWindowListener(externalWindow, 'beforeunload', () => { - /** - * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event - */ - this.close(); }) ); const container = this.createPopoutWindowContainer(); - container.classList.add(this.className); + + if (this.className) { + container.classList.add(this.className); + } this.options.onDidOpen?.({ id: this.target, @@ -126,6 +122,11 @@ export class PopoutWindow extends CompositeDisposable { }); return new Promise((resolve) => { + externalWindow.addEventListener('unload', (e) => { + // if page fails to load before unloading + // this.close(); + }); + externalWindow.addEventListener('load', () => { /** * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event @@ -134,12 +135,25 @@ export class PopoutWindow extends CompositeDisposable { const externalDocument = externalWindow.document; externalDocument.title = document.title; - // externalDocument.body.replaceChildren(container); externalDocument.body.appendChild(container); - externalDocument.body.classList.add(this.className); addStyles(externalDocument, window.document.styleSheets); + /** + * beforeunload must be registered after load for reasons I could not determine + * otherwise the beforeunload event will not fire when the window is closed + */ + addDisposableWindowListener( + externalWindow, + 'beforeunload', + () => { + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event + */ + this.close(); + } + ); + resolve(container); }); }); diff --git a/packages/dockview-core/src/splitview/splitviewPanel.ts b/packages/dockview-core/src/splitview/splitviewPanel.ts index 4782e8c30..9cafdb801 100644 --- a/packages/dockview-core/src/splitview/splitviewPanel.ts +++ b/packages/dockview-core/src/splitview/splitviewPanel.ts @@ -89,10 +89,10 @@ export abstract class SplitviewPanel this.addDisposables( this._onDidChange, - this.api.onVisibilityChange((event) => { - const { isVisible } = event; + this.api.onDidHiddenChange((event) => { + const { isHidden } = event; const { accessor } = this._params as PanelViewInitParameters; - accessor.setVisible(this, isVisible); + accessor.setVisible(this, !isHidden); }), this.api.onActiveChange(() => { const { accessor } = this._params as PanelViewInitParameters; diff --git a/packages/dockview/src/gridview/view.ts b/packages/dockview/src/gridview/view.ts index fce6f0690..35ed132df 100644 --- a/packages/dockview/src/gridview/view.ts +++ b/packages/dockview/src/gridview/view.ts @@ -3,6 +3,7 @@ import { GridviewPanel, GridviewInitParameters, IFrameworkPart, + GridviewComponent, } from 'dockview-core'; import { ReactPart, ReactPortalStore } from '../react'; import { IGridviewPanelProps } from './gridview'; @@ -25,8 +26,10 @@ export class ReactGridPanelView extends GridviewPanel { { params: this._params?.params ?? {}, api: this.api, + // TODO: fix casting hack containerApi: new GridviewApi( - (this._params as GridviewInitParameters).accessor + (this._params as GridviewInitParameters) + .accessor as GridviewComponent ), } );