From a40ce0d428c6b550ec947a3b07d3084fc101614b Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sat, 23 Apr 2022 18:57:44 +0100 Subject: [PATCH] feat: dispose of view panels correctly --- .../dockview/defaultGroupPanelView.spec.ts | 46 +++ .../dockview/dockviewComponent.spec.ts | 336 ++++++++++++++++++ .../dockview/dockviewGroupPanel.spec.ts | 28 ++ .../gridview/gridviewComponent.spec.ts | 95 +++++ .../paneview/paneviewComponent.spec.ts | 96 +++++ .../splitview/splitviewComponent.spec.ts | 97 +++++ packages/dockview/src/api/component.api.ts | 7 +- .../src/dockview/dockviewComponent.ts | 11 +- .../src/gridview/baseComponentGridview.ts | 5 + .../dockview/src/gridview/basePanelView.ts | 2 + .../src/gridview/gridviewComponent.ts | 23 +- packages/dockview/src/groupview/groupview.ts | 6 +- packages/dockview/src/paneview/paneview.ts | 15 +- .../src/paneview/paneviewComponent.ts | 48 ++- packages/dockview/src/react/gridview/view.ts | 3 +- .../src/splitview/splitviewComponent.ts | 26 +- 16 files changed, 798 insertions(+), 46 deletions(-) create mode 100644 packages/dockview/src/__tests__/dockview/defaultGroupPanelView.spec.ts diff --git a/packages/dockview/src/__tests__/dockview/defaultGroupPanelView.spec.ts b/packages/dockview/src/__tests__/dockview/defaultGroupPanelView.spec.ts new file mode 100644 index 000000000..ab91faeb4 --- /dev/null +++ b/packages/dockview/src/__tests__/dockview/defaultGroupPanelView.spec.ts @@ -0,0 +1,46 @@ +import { DefaultGroupPanelView } from '../../dockview/defaultGroupPanelView'; +import { + IActionsRenderer, + IContentRenderer, + ITabRenderer, +} from '../../groupview/types'; + +describe('defaultGroupPanelView', () => { + test('dispose cleanup', () => { + const contentMock = jest.fn(() => { + const partial: Partial = { + element: document.createElement('div'), + dispose: jest.fn(), + }; + return partial as IContentRenderer; + }); + + const tabMock = jest.fn(() => { + const partial: Partial = { + element: document.createElement('div'), + dispose: jest.fn(), + }; + return partial as IContentRenderer; + }); + + const actionsMock = jest.fn(() => { + const partial: Partial = { + element: document.createElement('div'), + dispose: jest.fn(), + }; + return partial as IContentRenderer; + }); + + const content = new contentMock(); + const tab = new tabMock(); + const actions = new actionsMock(); + + const cut = new DefaultGroupPanelView({ content, tab, actions }); + + cut.dispose(); + + expect(content.dispose).toHaveBeenCalled(); + expect(tab.dispose).toHaveBeenCalled(); + expect(actions.dispose).toHaveBeenCalled(); + }); +}); diff --git a/packages/dockview/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview/src/__tests__/dockview/dockviewComponent.spec.ts index c982a8bc9..6a11419ff 100644 --- a/packages/dockview/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview/src/__tests__/dockview/dockviewComponent.spec.ts @@ -25,10 +25,16 @@ import { DockviewPanelApiImpl, } from '../../api/groupPanelApi'; import { DefaultTab } from '../../dockview/components/tab/defaultTab'; +import { Emitter } from '../../events'; class PanelContentPartTest implements IContentRenderer { element: HTMLElement = document.createElement('div'); + readonly _onDidDispose = new Emitter(); + readonly onDidDispose = this._onDidDispose.event; + + isDisposed: boolean = false; + constructor(public readonly id: string, component: string) { this.element.classList.add(`testpanel-${id}`); } @@ -58,8 +64,51 @@ class PanelContentPartTest implements IContentRenderer { } dispose(): void { + this.isDisposed = true; + this._onDidDispose.fire(); + } +} + +class PanelTabPartTest implements ITabRenderer { + element: HTMLElement = document.createElement('div'); + + readonly _onDidDispose = new Emitter(); + readonly onDidDispose = this._onDidDispose.event; + + isDisposed: boolean = false; + + constructor(public readonly id: string, component: string) { + this.element.classList.add(`testpanel-${id}`); + } + + updateParentGroup(group: GroupviewPanel, isPanelVisible: boolean): void { //noop } + + init(parameters: GroupPanelPartInitParameters): void { + //noop + } + + layout(width: number, height: number): void { + //noop + } + + update(event: PanelUpdateEvent): void { + //noop + } + + toJSON(): object { + return { id: this.id }; + } + + focus(): void { + //noop + } + + dispose(): void { + this.isDisposed = true; + this._onDidDispose.fire(); + } } class TestGroupPanelView implements IGroupPanelView { @@ -1088,4 +1137,291 @@ describe('dockviewComponent', () => { expect(container.childNodes.length).toBe(0); }); + + test('panel is disposed of when closed', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent(container, { + components: { default: PanelContentPartTest }, + }); + + dockview.layout(500, 1000); + + const panel1 = dockview.addPanel({ + id: 'panel1', + component: 'default', + tabComponent: 'default', + }); + + const panel1Spy = jest.spyOn(panel1, 'dispose'); + + expect(panel1Spy).not.toHaveBeenCalled(); + + panel1.api.close(); + + expect(panel1Spy).toBeCalledTimes(1); + }); + + test('panel is disposed of when removed', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent(container, { + components: { default: PanelContentPartTest }, + }); + + dockview.layout(500, 1000); + + const panel1 = dockview.addPanel({ + id: 'panel1', + component: 'default', + tabComponent: 'default', + }); + + const panel1Spy = jest.spyOn(panel1, 'dispose'); + + expect(panel1Spy).not.toHaveBeenCalled(); + + dockview.removePanel(panel1); + + expect(panel1Spy).toBeCalledTimes(1); + }); + + test('panel is not disposed of when moved to a new group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent(container, { + components: { + default: PanelContentPartTest, + }, + }); + + dockview.layout(500, 1000); + + const panel1 = dockview.addPanel({ + id: 'panel1', + component: 'default', + tabComponent: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel2', + component: 'default', + tabComponent: 'default', + position: { + referencePanel: 'panel1', + direction: 'right', + }, + }); + + const panel1Spy = jest.spyOn(panel1, 'dispose'); + const panel2Spy = jest.spyOn(panel2, 'dispose'); + + dockview.moveGroupOrPanel( + panel1.group, + panel2.group.id, + 'panel2', + Position.Left + ); + + expect(panel1Spy).not.toHaveBeenCalled(); + expect(panel2Spy).not.toHaveBeenCalled(); + }); + + test('panel is not disposed of when moved within another group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent(container, { + components: { + default: PanelContentPartTest, + }, + }); + + dockview.layout(500, 1000); + + const panel1 = dockview.addPanel({ + id: 'panel1', + component: 'default', + tabComponent: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel2', + component: 'default', + tabComponent: 'default', + position: { + referencePanel: 'panel1', + direction: 'right', + }, + }); + + const panel1Spy = jest.spyOn(panel1, 'dispose'); + const panel2Spy = jest.spyOn(panel2, 'dispose'); + + dockview.moveGroupOrPanel( + panel1.group, + panel2.group.id, + 'panel2', + Position.Center + ); + + expect(panel1Spy).not.toHaveBeenCalled(); + expect(panel2Spy).not.toHaveBeenCalled(); + }); + + test('panel is not disposed of when moved within another group', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent(container, { + components: { + default: PanelContentPartTest, + }, + }); + + dockview.layout(500, 1000); + + const panel1 = dockview.addPanel({ + id: 'panel1', + component: 'default', + tabComponent: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel2', + component: 'default', + tabComponent: 'default', + }); + + expect(panel1.group).toEqual(panel2.group); + + const panel1Spy = jest.spyOn(panel1, 'dispose'); + const panel2Spy = jest.spyOn(panel2, 'dispose'); + + dockview.moveGroupOrPanel( + panel1.group, + panel1.group.id, + 'panel1', + Position.Center, + 0 + ); + + expect(panel1Spy).not.toHaveBeenCalled(); + expect(panel2Spy).not.toHaveBeenCalled(); + }); + + test('panel is disposed of when group is disposed', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent(container, { + components: { + default: PanelContentPartTest, + }, + }); + + dockview.layout(500, 1000); + + const panel1 = dockview.addPanel({ + id: 'panel1', + component: 'default', + tabComponent: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel2', + component: 'default', + tabComponent: 'default', + }); + + expect(panel1.group).toEqual(panel2.group); + + const panel1Spy = jest.spyOn(panel1, 'dispose'); + const panel2Spy = jest.spyOn(panel2, 'dispose'); + + dockview.removeGroup(panel1.group); + + expect(panel1Spy).toBeCalledTimes(1); + expect(panel2Spy).toBeCalledTimes(1); + }); + + test('panel is disposed of when component is disposed', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent(container, { + components: { + default: PanelContentPartTest, + }, + }); + + dockview.layout(500, 1000); + + const panel1 = dockview.addPanel({ + id: 'panel1', + component: 'default', + tabComponent: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel2', + component: 'default', + tabComponent: 'default', + }); + + expect(panel1.group).toEqual(panel2.group); + + const panel1Spy = jest.spyOn(panel1, 'dispose'); + const panel2Spy = jest.spyOn(panel2, 'dispose'); + + dockview.dispose(); + + expect(panel1Spy).toBeCalledTimes(1); + expect(panel2Spy).toBeCalledTimes(1); + }); + + test('panel is disposed of when from JSON is called', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent(container, { + components: { + default: PanelContentPartTest, + }, + }); + dockview.deserializer = new ReactPanelDeserialzier(dockview); + + dockview.layout(500, 1000); + + const panel1 = dockview.addPanel({ + id: 'panel1', + component: 'default', + tabComponent: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel2', + component: 'default', + tabComponent: 'default', + }); + + expect(panel1.group).toEqual(panel2.group); + + const groupSpy = jest.spyOn(panel1.group, 'dispose'); + const panel1Spy = jest.spyOn(panel1, 'dispose'); + const panel2Spy = jest.spyOn(panel2, 'dispose'); + + dockview.fromJSON({ + grid: { + height: 0, + width: 0, + root: { type: 'branch', data: [] }, + orientation: Orientation.HORIZONTAL, + }, + panels: {}, + }); + + expect(groupSpy).toBeCalledTimes(1); + expect(panel1Spy).toBeCalledTimes(1); + expect(panel2Spy).toBeCalledTimes(1); + }); + + // group is disposed of when dockview is disposed + // watermark is disposed of when removed + // watermark is disposed of when dockview is disposed }); diff --git a/packages/dockview/src/__tests__/dockview/dockviewGroupPanel.spec.ts b/packages/dockview/src/__tests__/dockview/dockviewGroupPanel.spec.ts index 43a755a12..f3bb9d402 100644 --- a/packages/dockview/src/__tests__/dockview/dockviewGroupPanel.spec.ts +++ b/packages/dockview/src/__tests__/dockview/dockviewGroupPanel.spec.ts @@ -1,5 +1,6 @@ import { DockviewComponent } from '../..'; import { DockviewApi } from '../../api/component.api'; +import { IGroupPanelView } from '../../dockview/defaultGroupPanelView'; import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; describe('dockviewGroupPanel', () => { @@ -68,4 +69,31 @@ describe('dockviewGroupPanel', () => { disposable.dispose(); }); + + test('dispose cleanup', () => { + const dockviewApiMock = jest.fn(() => { + return {} as any; + }); + const accessorMock = jest.fn(() => { + return {} as any; + }); + const api = new dockviewApiMock(); + const accessor = new accessorMock(); + + const cut = new DockviewGroupPanel('fake-id', accessor, api); + + const viewMock = jest.fn(() => { + return { + init: jest.fn(), + dispose: jest.fn(), + } as any; + }); + const view = new viewMock(); + + cut.init({ params: {}, view, title: 'title' }); + + cut.dispose(); + + expect(view.dispose).toHaveBeenCalled(); + }); }); diff --git a/packages/dockview/src/__tests__/gridview/gridviewComponent.spec.ts b/packages/dockview/src/__tests__/gridview/gridviewComponent.spec.ts index 1657669d2..77a748333 100644 --- a/packages/dockview/src/__tests__/gridview/gridviewComponent.spec.ts +++ b/packages/dockview/src/__tests__/gridview/gridviewComponent.spec.ts @@ -1666,4 +1666,99 @@ describe('gridview', () => { activePanel: 'panel_1', }); }); + + test('panel is disposed of when component is disposed', () => { + const gridview = new GridviewComponent(container, { + proportionalLayout: false, + orientation: Orientation.VERTICAL, + components: { default: TestGridview }, + }); + + gridview.layout(1000, 1000); + + gridview.addPanel({ + id: 'panel1', + component: 'default', + }); + gridview.addPanel({ + id: 'panel2', + component: 'default', + }); + + const panel1 = gridview.getPanel('panel1'); + const panel2 = gridview.getPanel('panel2'); + + const panel1Spy = jest.spyOn(panel1, 'dispose'); + const panel2Spy = jest.spyOn(panel2, 'dispose'); + + gridview.dispose(); + + expect(panel1Spy).toHaveBeenCalledTimes(1); + expect(panel2Spy).toHaveBeenCalledTimes(1); + }); + + test('panel is disposed of when removed', () => { + const gridview = new GridviewComponent(container, { + proportionalLayout: false, + orientation: Orientation.VERTICAL, + components: { default: TestGridview }, + }); + gridview.layout(1000, 1000); + + gridview.addPanel({ + id: 'panel1', + component: 'default', + }); + gridview.addPanel({ + id: 'panel2', + component: 'default', + }); + + const panel1 = gridview.getPanel('panel1'); + const panel2 = gridview.getPanel('panel2'); + + const panel1Spy = jest.spyOn(panel1, 'dispose'); + const panel2Spy = jest.spyOn(panel2, 'dispose'); + + gridview.removePanel(panel2); + + expect(panel1Spy).not.toHaveBeenCalled(); + expect(panel2Spy).toHaveBeenCalledTimes(1); + }); + + test('panel is disposed of when fromJSON is called', () => { + const gridview = new GridviewComponent(container, { + proportionalLayout: false, + orientation: Orientation.VERTICAL, + components: { default: TestGridview }, + }); + gridview.layout(1000, 1000); + + gridview.addPanel({ + id: 'panel1', + component: 'default', + }); + gridview.addPanel({ + id: 'panel2', + component: 'default', + }); + + const panel1 = gridview.getPanel('panel1'); + const panel2 = gridview.getPanel('panel2'); + + const panel1Spy = jest.spyOn(panel1, 'dispose'); + const panel2Spy = jest.spyOn(panel2, 'dispose'); + + gridview.fromJSON({ + grid: { + height: 0, + width: 0, + root: { type: 'branch', data: [] }, + orientation: Orientation.HORIZONTAL, + }, + }); + + expect(panel1Spy).toHaveBeenCalledTimes(1); + expect(panel2Spy).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/dockview/src/__tests__/paneview/paneviewComponent.spec.ts b/packages/dockview/src/__tests__/paneview/paneviewComponent.spec.ts index 0c2b81c96..f47da6f5d 100644 --- a/packages/dockview/src/__tests__/paneview/paneviewComponent.spec.ts +++ b/packages/dockview/src/__tests__/paneview/paneviewComponent.spec.ts @@ -305,4 +305,100 @@ describe('componentPaneview', () => { expect(container.childNodes.length).toBe(0); }); + + test('panel is disposed of when component is disposed', () => { + const paneview = new PaneviewComponent(container, { + components: { + testPanel: TestPanel, + }, + }); + + paneview.layout(1000, 1000); + + paneview.addPanel({ + id: 'panel1', + component: 'testPanel', + title: 'Panel 1', + }); + paneview.addPanel({ + id: 'panel2', + component: 'testPanel', + title: 'Panel 2', + }); + + const panel1 = paneview.getPanel('panel1'); + const panel2 = paneview.getPanel('panel2'); + + const panel1Spy = jest.spyOn(panel1, 'dispose'); + const panel2Spy = jest.spyOn(panel2, 'dispose'); + + paneview.dispose(); + + expect(panel1Spy).toHaveBeenCalledTimes(1); + expect(panel2Spy).toHaveBeenCalledTimes(1); + }); + + test('panel is disposed of when removed', () => { + const paneview = new PaneviewComponent(container, { + components: { + testPanel: TestPanel, + }, + }); + + paneview.layout(1000, 1000); + + paneview.addPanel({ + id: 'panel1', + component: 'testPanel', + title: 'Panel 1', + }); + paneview.addPanel({ + id: 'panel2', + component: 'testPanel', + title: 'Panel 2', + }); + + const panel1 = paneview.getPanel('panel1'); + const panel2 = paneview.getPanel('panel2'); + + const panel1Spy = jest.spyOn(panel1, 'dispose'); + const panel2Spy = jest.spyOn(panel2, 'dispose'); + + paneview.removePanel(panel2); + + expect(panel1Spy).not.toHaveBeenCalled(); + expect(panel2Spy).toHaveBeenCalledTimes(1); + }); + + test('panel is disposed of when fromJSON is called', () => { + const paneview = new PaneviewComponent(container, { + components: { + testPanel: TestPanel, + }, + }); + + paneview.layout(1000, 1000); + + paneview.addPanel({ + id: 'panel1', + component: 'testPanel', + title: 'Panel 1', + }); + paneview.addPanel({ + id: 'panel2', + component: 'testPanel', + title: 'Panel 2', + }); + + const panel1 = paneview.getPanel('panel1'); + const panel2 = paneview.getPanel('panel2'); + + const panel1Spy = jest.spyOn(panel1, 'dispose'); + const panel2Spy = jest.spyOn(panel2, 'dispose'); + + paneview.fromJSON({ views: [], size: 0 }); + + expect(panel1Spy).toHaveBeenCalledTimes(1); + expect(panel2Spy).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/dockview/src/__tests__/splitview/splitviewComponent.spec.ts b/packages/dockview/src/__tests__/splitview/splitviewComponent.spec.ts index 884ca5849..a6cca1bf6 100644 --- a/packages/dockview/src/__tests__/splitview/splitviewComponent.spec.ts +++ b/packages/dockview/src/__tests__/splitview/splitviewComponent.spec.ts @@ -385,4 +385,101 @@ describe('componentSplitview', () => { expect(container.childNodes.length).toBe(0); }); + + test('panel is disposed of when component is disposed', () => { + const splitview = new SplitviewComponent(container, { + orientation: Orientation.HORIZONTAL, + components: { + default: TestPanel, + }, + }); + + splitview.layout(1000, 1000); + + splitview.addPanel({ + id: 'panel1', + component: 'default', + }); + splitview.addPanel({ + id: 'panel2', + component: 'default', + }); + + const panel1 = splitview.getPanel('panel1'); + const panel2 = splitview.getPanel('panel2'); + + const panel1Spy = jest.spyOn(panel1, 'dispose'); + const panel2Spy = jest.spyOn(panel2, 'dispose'); + + splitview.dispose(); + + expect(panel1Spy).toHaveBeenCalledTimes(1); + expect(panel2Spy).toHaveBeenCalledTimes(1); + }); + + test('panel is disposed of when removed', () => { + const splitview = new SplitviewComponent(container, { + orientation: Orientation.HORIZONTAL, + components: { + default: TestPanel, + }, + }); + + splitview.layout(1000, 1000); + + splitview.addPanel({ + id: 'panel1', + component: 'default', + }); + splitview.addPanel({ + id: 'panel2', + component: 'default', + }); + + const panel1 = splitview.getPanel('panel1'); + const panel2 = splitview.getPanel('panel2'); + + const panel1Spy = jest.spyOn(panel1, 'dispose'); + const panel2Spy = jest.spyOn(panel2, 'dispose'); + + splitview.removePanel(panel2); + + expect(panel1Spy).not.toHaveBeenCalled(); + expect(panel2Spy).toHaveBeenCalledTimes(1); + }); + + test('panel is disposed of when fromJSON is called', () => { + const splitview = new SplitviewComponent(container, { + orientation: Orientation.HORIZONTAL, + components: { + default: TestPanel, + }, + }); + + splitview.layout(1000, 1000); + + splitview.addPanel({ + id: 'panel1', + component: 'default', + }); + splitview.addPanel({ + id: 'panel2', + component: 'default', + }); + + const panel1 = splitview.getPanel('panel1'); + const panel2 = splitview.getPanel('panel2'); + + const panel1Spy = jest.spyOn(panel1, 'dispose'); + const panel2Spy = jest.spyOn(panel2, 'dispose'); + + splitview.fromJSON({ + orientation: Orientation.HORIZONTAL, + size: 0, + views: [], + }); + + expect(panel1Spy).toHaveBeenCalledTimes(1); + expect(panel2Spy).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/dockview/src/api/component.api.ts b/packages/dockview/src/api/component.api.ts index 98c8830ab..324b53c6d 100644 --- a/packages/dockview/src/api/component.api.ts +++ b/packages/dockview/src/api/component.api.ts @@ -16,7 +16,7 @@ import { import { IGridviewPanel } from '../gridview/gridviewPanel'; import { IGroupPanel } from '../groupview/groupPanel'; import { - AddPaneviewCompponentOptions, + AddPaneviewComponentOptions, SerializedPaneview, IPaneviewComponent, } from '../paneview/paneviewComponent'; @@ -31,7 +31,6 @@ import { IView, Orientation, Sizing } from '../splitview/core/splitview'; import { ISplitviewPanel } from '../splitview/splitviewPanel'; import { GroupviewPanel, IGroupviewPanel } from '../groupview/groupviewPanel'; import { Emitter, Event } from '../events'; -import { IDisposable } from '../lifecycle'; import { PaneviewDropEvent } from '../react'; export interface CommonApi { @@ -205,8 +204,8 @@ export class PaneviewApi implements CommonApi { this.component.layout(width, height); } - addPanel(options: AddPaneviewCompponentOptions): IDisposable { - return this.component.addPanel(options); + addPanel(options: AddPaneviewComponentOptions): void { + this.component.addPanel(options); } resizeToFit(): void { diff --git a/packages/dockview/src/dockview/dockviewComponent.ts b/packages/dockview/src/dockview/dockviewComponent.ts index 5c1532ae2..464eb4959 100644 --- a/packages/dockview/src/dockview/dockviewComponent.ts +++ b/packages/dockview/src/dockview/dockviewComponent.ts @@ -59,7 +59,7 @@ export interface SerializedDockview { }; panels: { [key: string]: GroupviewPanelState }; activeGroup?: string; - options: { tabHeight?: number }; + options?: { tabHeight?: number }; } export type DockviewComponentUpdateOptions = Pick< @@ -449,7 +449,10 @@ export class DockviewComponent removePanel( panel: IGroupPanel, - options: { removeEmptyGroup: boolean } = { removeEmptyGroup: true } + options: { removeEmptyGroup: boolean; skipDispose: boolean } = { + removeEmptyGroup: true, + skipDispose: false, + } ): void { const group = panel.group; @@ -461,6 +464,8 @@ export class DockviewComponent group.model.removePanel(panel); + panel.dispose(); + if (group.model.size === 0 && options.removeEmptyGroup) { this.removeGroup(group); } @@ -522,6 +527,7 @@ export class DockviewComponent for (const panel of panels) { this.removePanel(panel, { removeEmptyGroup: false, + skipDispose: false, }); } @@ -650,6 +656,7 @@ export class DockviewComponent } const view = new GroupviewPanel(this, id, options); + view.init({ params: {}, containerApi: null }); // required to initialized .part and allow for correct disposal of group if (!this._groups.has(view.id)) { const disposable = new CompositeDisposable( diff --git a/packages/dockview/src/gridview/baseComponentGridview.ts b/packages/dockview/src/gridview/baseComponentGridview.ts index 1453367f6..2e2ee9c8a 100644 --- a/packages/dockview/src/gridview/baseComponentGridview.ts +++ b/packages/dockview/src/gridview/baseComponentGridview.ts @@ -205,6 +205,7 @@ export abstract class BaseGrid if (item && !options?.skipDispose) { item.disposable.dispose(); + item.value.dispose(); this._groups.delete(group.id); } @@ -318,6 +319,10 @@ export abstract class BaseGrid this._onDidRemoveGroup.dispose(); this._onDidLayoutChange.dispose(); + for (const group of this.groups) { + group.dispose(); + } + this.gridview.dispose(); } } diff --git a/packages/dockview/src/gridview/basePanelView.ts b/packages/dockview/src/gridview/basePanelView.ts index 485be2422..f6e5f7567 100644 --- a/packages/dockview/src/gridview/basePanelView.ts +++ b/packages/dockview/src/gridview/basePanelView.ts @@ -127,6 +127,8 @@ export abstract class BasePanelView dispose() { super.dispose(); + this.api.dispose(); + this.part?.dispose(); } } diff --git a/packages/dockview/src/gridview/gridviewComponent.ts b/packages/dockview/src/gridview/gridviewComponent.ts index 1dc7da77f..6fdb9341b 100644 --- a/packages/dockview/src/gridview/gridviewComponent.ts +++ b/packages/dockview/src/gridview/gridviewComponent.ts @@ -22,16 +22,11 @@ import { IGridviewPanel, } from './gridviewPanel'; import { BaseComponentOptions } from '../panel/types'; -import { GridviewPanelApiImpl } from '../api/gridviewPanelApi'; import { GridviewApi } from '../api/component.api'; import { Orientation, Sizing } from '../splitview/core/splitview'; import { createComponent } from '../panel/componentFactory'; import { Emitter, Event } from '../events'; -interface PanelReference { - api: GridviewPanelApiImpl; -} - export interface SerializedGridview { grid: { height: number; @@ -193,8 +188,13 @@ export class GridviewComponent ) { const { grid, activePanel } = serializedGridview; + const groups = Array.from(this._groups.values()); // reassign since group panels will mutate + for (const group of groups) { + group.disposable.dispose(); + this.doRemoveGroup(group.value, { skipActive: true }); + } + this.gridview.clear(); - this._groups.clear(); const queue: Function[] = []; @@ -286,7 +286,7 @@ export class GridviewComponent this.doAddGroup(removedPanel, relativeLocation, options.size); } - public addPanel(options: AddComponentOptions): PanelReference { + public addPanel(options: AddComponentOptions): void { let relativeLocation: number[] = options.location || [0]; if (options.position?.reference) { @@ -342,8 +342,6 @@ export class GridviewComponent this.registerPanel(view); this.doAddGroup(view, relativeLocation, options.size); - - return { api: view.api }; } private registerPanel(panel: GridviewPanel) { @@ -420,13 +418,6 @@ export class GridviewComponent removeGroup(group: GridviewPanel) { super.removeGroup(group); - - const panel = this._groups.get(group.id); - - if (panel) { - panel.disposable.dispose(); - this._groups.delete(group.id); - } } public dispose() { diff --git a/packages/dockview/src/groupview/groupview.ts b/packages/dockview/src/groupview/groupview.ts index 8f8d16ec7..b59e31fb8 100644 --- a/packages/dockview/src/groupview/groupview.ts +++ b/packages/dockview/src/groupview/groupview.ts @@ -645,12 +645,14 @@ export class Groupview extends CompositeDisposable implements IGroupview { } public dispose() { + super.dispose(); + + this.watermark?.dispose(); + for (const panel of this.panels) { panel.dispose(); } - super.dispose(); - this.dropTarget.dispose(); this.tabsContainer.dispose(); this.contentContainer.dispose(); diff --git a/packages/dockview/src/paneview/paneview.ts b/packages/dockview/src/paneview/paneview.ts index 73742e3a2..3ca79a582 100644 --- a/packages/dockview/src/paneview/paneview.ts +++ b/packages/dockview/src/paneview/paneview.ts @@ -142,10 +142,18 @@ export class Paneview extends CompositeDisposable implements IDisposable { return this.splitview.getViews(); } - public removePane(index: number) { + public removePane( + index: number, + options: { skipDispose: boolean } = { skipDispose: false } + ) { const paneItem = this.paneItems.splice(index, 1)[0]; this.splitview.removeView(index); - paneItem.disposable.dispose(); + + if (!options.skipDispose) { + paneItem.disposable.dispose(); + paneItem.pane.dispose(); + } + return paneItem; } @@ -154,7 +162,7 @@ export class Paneview extends CompositeDisposable implements IDisposable { return; } - const view = this.removePane(from); + const view = this.removePane(from, { skipDispose: true }); this.skipAnimation = true; try { @@ -196,6 +204,7 @@ export class Paneview extends CompositeDisposable implements IDisposable { this.paneItems.forEach((paneItem) => { paneItem.disposable.dispose(); + paneItem.pane.dispose(); }); this.paneItems = []; diff --git a/packages/dockview/src/paneview/paneviewComponent.ts b/packages/dockview/src/paneview/paneviewComponent.ts index 2b5c72a2b..c3fbd6a8a 100644 --- a/packages/dockview/src/paneview/paneviewComponent.ts +++ b/packages/dockview/src/paneview/paneviewComponent.ts @@ -78,7 +78,7 @@ export class PaneFramework extends DraggablePaneviewPanel { } } -export interface AddPaneviewCompponentOptions { +export interface AddPaneviewComponentOptions { id: string; component: string; headerComponent?: string; @@ -102,7 +102,7 @@ export interface IPaneviewComponent extends IDisposable { readonly onDidRemoveView: Event; readonly onDidDrop: Event; readonly onDidLayoutChange: Event; - addPanel(options: AddPaneviewCompponentOptions): IDisposable; + addPanel(options: AddPaneviewComponentOptions): IPaneviewPanel; layout(width: number, height: number): void; toJSON(): SerializedPaneview; fromJSON( @@ -123,6 +123,7 @@ export class PaneviewComponent implements IPaneviewComponent { private _disposable = new MutableDisposable(); + private _viewDisposables = new Map(); private _paneview!: Paneview; private readonly _onDidLayoutChange = new Emitter(); @@ -217,7 +218,7 @@ export class PaneviewComponent this._options = { ...this.options, ...options }; } - addPanel(options: AddPaneviewCompponentOptions): IDisposable { + addPanel(options: AddPaneviewComponentOptions): IPaneviewPanel { const body = createComponent( options.id, options.component, @@ -262,11 +263,7 @@ export class PaneviewComponent disableDnd: !!this.options.disableDnd, }); - const disposable = new CompositeDisposable( - view.onDidDrop((event) => { - this._onDidDrop.fire(event); - }) - ); + this.doAddPanel(view); const size: Sizing | number = typeof options.size === 'number' ? options.size : Sizing.Distribute; @@ -286,7 +283,7 @@ export class PaneviewComponent view.orientation = this.paneview.orientation; - return disposable; + return view; } getPanels(): PaneviewPanel[] { @@ -297,6 +294,8 @@ export class PaneviewComponent const views = this.getPanels(); const index = views.findIndex((_) => _ === panel); this.paneview.removePane(index); + + this.doRemovePanel(panel); } movePanel(from: number, to: number): void { @@ -362,6 +361,11 @@ export class PaneviewComponent const queue: Function[] = []; + for (const [_, value] of this._viewDisposables.entries()) { + value.dispose(); + } + this._viewDisposables.clear(); + this.paneview.dispose(); this.paneview = new Paneview(this.element, { @@ -416,9 +420,7 @@ export class PaneviewComponent disableDnd: !!this.options.disableDnd, }); - panel.onDidDrop((event) => { - this._onDidDrop.fire(event); - }); + this.doAddPanel(panel); queue.push(() => { panel.init({ @@ -453,9 +455,31 @@ export class PaneviewComponent } } + private doAddPanel(panel: PaneFramework) { + const disposable = panel.onDidDrop((event) => { + this._onDidDrop.fire(event); + }); + + this._viewDisposables.set(panel.id, disposable); + } + + private doRemovePanel(panel: PaneviewPanel) { + const disposable = this._viewDisposables.get(panel.id); + + if (disposable) { + disposable.dispose(); + this._viewDisposables.delete(panel.id); + } + } + public dispose(): void { super.dispose(); + for (const [_, value] of this._viewDisposables.entries()) { + value.dispose(); + } + this._viewDisposables.clear(); + this.paneview.dispose(); } } diff --git a/packages/dockview/src/react/gridview/view.ts b/packages/dockview/src/react/gridview/view.ts index fa82d8f86..9fa0f2446 100644 --- a/packages/dockview/src/react/gridview/view.ts +++ b/packages/dockview/src/react/gridview/view.ts @@ -2,6 +2,7 @@ import { GridviewPanel, GridviewInitParameters, } from '../../gridview/gridviewPanel'; +import { IFrameworkPart } from '../../panel/types'; import { ReactPart, ReactPortalStore } from '../react'; import { IGridviewPanelProps } from './gridview'; @@ -15,7 +16,7 @@ export class ReactGridPanelView extends GridviewPanel { super(id, component); } - getComponent() { + getComponent(): IFrameworkPart { return new ReactPart( this.element, this.reactPortalStore, diff --git a/packages/dockview/src/splitview/splitviewComponent.ts b/packages/dockview/src/splitview/splitviewComponent.ts index 66775a33c..4cbf03bf7 100644 --- a/packages/dockview/src/splitview/splitviewComponent.ts +++ b/packages/dockview/src/splitview/splitviewComponent.ts @@ -1,6 +1,7 @@ import { CompositeDisposable, IDisposable, + IValueDisposable, MutableDisposable, } from '../lifecycle'; import { @@ -89,7 +90,7 @@ export class SplitviewComponent private _disposable = new MutableDisposable(); private _splitview!: Splitview; private _activePanel: SplitviewPanel | undefined; - private panels = new Map(); + private panels = new Map>(); private _options: SplitviewComponentOptions; private readonly _onDidAddView = new Emitter(); @@ -229,7 +230,14 @@ export class SplitviewComponent removePanel(panel: SplitviewPanel, sizing?: Sizing) { const disposable = this.panels.get(panel.id); - disposable?.dispose(); + + if (!disposable) { + throw new Error(`unknown splitview panel ${panel.id}`); + } + + disposable.disposable.dispose(); + disposable.value.dispose(); + this.panels.delete(panel.id); const index = this.getPanels().findIndex((_) => _ === panel); @@ -313,7 +321,7 @@ export class SplitviewComponent this.setActive(view, true); }); - this.panels.set(view.id, disposable); + this.panels.set(view.id, { disposable, value: view }); } toJSON(): SerializedSplitview { @@ -343,6 +351,11 @@ export class SplitviewComponent ): void { const { views, orientation, size, activeView } = serializedSplitview; + for (const [_, value] of this.panels.entries()) { + value.disposable.dispose(); + value.value.dispose(); + } + this.panels.clear(); this.splitview.dispose(); const queue: Function[] = []; @@ -416,9 +429,10 @@ export class SplitviewComponent } dispose() { - Array.from(this.panels.values()).forEach((value) => { - value.dispose(); - }); + for (const [_, value] of this.panels.entries()) { + value.disposable.dispose(); + value.value.dispose(); + } this.panels.clear(); this.splitview.dispose();