diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index 4d276169b..f04bc5e88 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -13,6 +13,7 @@ "/packages/docs/sandboxes/externaldnd-dockview", "/packages/docs/sandboxes/fullwidthtab-dockview", "/packages/docs/sandboxes/groupcontol-dockview", + "/packages/docs/sandboxes/iframe-dockview", "/packages/docs/sandboxes/layout-dockview", "/packages/docs/sandboxes/nativeapp-dockview", "/packages/docs/sandboxes/nested-dockview", @@ -22,8 +23,11 @@ "/packages/docs/sandboxes/simple-dockview", "/packages/docs/sandboxes/tabheight-dockview", "/packages/docs/sandboxes/updatetitle-dockview", - "/packages/docs/sandboxes/vanilla-dockview", - "/packages/docs/sandboxes/watermark-dockview" + "/packages/docs/sandboxes/watermark-dockview", + "/packages/docs/sandboxes/javascript/fullwidthtab-dockview", + "/packages/docs/sandboxes/javascript/simple-dockview", + "/packages/docs/sandboxes/javascript/tabheight-dockview", + "/packages/docs/sandboxes/javascript/vanilla-dockview" ], "node": "16" } \ No newline at end of file diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 9c242d324..6b82733a6 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -33,8 +33,10 @@ jobs: working-directory: packages/dockview - run: npm run build working-directory: packages/docs - - run: npm run deploy-docs - working-directory: packages/docs + - run: npm run docs + working-directory: . + - run: npm run package-docs + working-directory: . - name: Deploy 🚀 uses: JamesIves/github-pages-deploy-action@3.7.1 with: diff --git a/.gitignore b/.gitignore index 5f1ae726d..e3f290550 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ test-report.xml *.code-workspace yarn-error.log /build +/docs/ diff --git a/lerna.json b/lerna.json index b2d59ced6..77cdbbf1c 100644 --- a/lerna.json +++ b/lerna.json @@ -3,7 +3,7 @@ "packages/*" ], "useWorkspaces": true, - "version": "1.7.2", + "version": "1.7.5", "npmClient": "yarn", "command": { "publish": { diff --git a/package.json b/package.json index 8d35430d0..114afaa58 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "bootstrap": "lerna bootstrap", "test:cov": "jest --coverage", "version-beta-build": "lerna version prerelease --preid beta", - "publish-app": "lerna publish" + "publish-app": "lerna publish", + "docs": "typedoc", + "package-docs": "node scripts/package-docs.js" }, "repository": { "type": "git", @@ -33,6 +35,7 @@ "homepage": "https://github.com/mathuo/dockview#readme", "devDependencies": { "@testing-library/dom": "^8.20.0", + "@testing-library/jest-dom": "^5.16.5", "@types/jest": "^29.4.0", "@typescript-eslint/eslint-plugin": "^5.52.0", "@typescript-eslint/parser": "^5.52.0", @@ -44,6 +47,7 @@ "gulp": "^4.0.2", "gulp-concat": "^2.6.1", "gulp-dart-sass": "^1.0.2", + "jest": "^29.5.0", "jest-environment-jsdom": "^29.4.3", "jest-sonar-reporter": "^2.0.0", "jsdom": "^21.1.0", @@ -55,14 +59,13 @@ "style-loader": "^3.3.1", "ts-jest": "^29.0.5", "ts-loader": "^9.4.2", + "ts-node": "^10.9.1", "tslib": "^2.5.0", + "typedoc": "^0.24.7", "typescript": "^4.9.5", "webpack": "^5.75.0", "webpack-cli": "^5.0.1", "webpack-dev-server": "^4.11.1" }, - "dependencies": { - "jest": "^29.5.0", - "ts-node": "^10.9.1" - } -} \ No newline at end of file + "dependencies": {} +} diff --git a/packages/dockview-core/package.json b/packages/dockview-core/package.json index cdceee251..1590381fd 100644 --- a/packages/dockview-core/package.json +++ b/packages/dockview-core/package.json @@ -1,6 +1,6 @@ { "name": "dockview-core", - "version": "1.7.2", + "version": "1.7.5", "description": "Zero dependency layout manager supporting tabs, grids and splitviews with ReactJS support", "main": "./dist/cjs/index.js", "types": "./dist/cjs/index.d.ts", diff --git a/packages/dockview-core/src/__tests__/__mocks__/mockDockviewPanelMode.ts b/packages/dockview-core/src/__tests__/__mocks__/mockDockviewPanelMode.ts index 0d788620e..a1e3397dd 100644 --- a/packages/dockview-core/src/__tests__/__mocks__/mockDockviewPanelMode.ts +++ b/packages/dockview-core/src/__tests__/__mocks__/mockDockviewPanelMode.ts @@ -2,10 +2,10 @@ import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel'; import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; import { GroupPanelPartInitParameters, - GroupPanelUpdateEvent, IContentRenderer, ITabRenderer, } from '../../dockview/types'; +import { PanelUpdateEvent } from '../../panel/types'; export class DockviewPanelModelMock implements IDockviewPanelModel { constructor( @@ -21,7 +21,14 @@ export class DockviewPanelModelMock implements IDockviewPanelModel { // } - update(event: GroupPanelUpdateEvent): void { + updateParentGroup( + group: DockviewGroupPanel, + isPanelVisible: boolean + ): void { + // + } + + update(event: PanelUpdateEvent): void { // } diff --git a/packages/dockview-core/src/__tests__/api/api.spec.ts b/packages/dockview-core/src/__tests__/api/api.spec.ts index 10f9bc657..5b40a6aa9 100644 --- a/packages/dockview-core/src/__tests__/api/api.spec.ts +++ b/packages/dockview-core/src/__tests__/api/api.spec.ts @@ -1,4 +1,5 @@ import { PanelApiImpl } from '../../api/panelApi'; +import { IPanel } from '../../panel/types'; describe('api', () => { let api: PanelApiImpl; @@ -7,7 +8,23 @@ describe('api', () => { api = new PanelApiImpl('dummy_id'); }); - it('should update isFcoused getter', () => { + test('updateParameters', () => { + const panel = { + update: jest.fn(), + } as Partial; + + api.initialize(panel as IPanel); + + expect(panel.update).toHaveBeenCalledTimes(0); + + api.updateParameters({ keyA: 'valueA' }); + expect(panel.update).toHaveBeenCalledTimes(1); + expect(panel.update).toHaveBeenCalledWith({ + params: { keyA: 'valueA' }, + }); + }); + + test('should update isFcoused getter', () => { expect(api.isFocused).toBeFalsy(); api._onDidChangeFocus.fire({ isFocused: true }); @@ -17,7 +34,7 @@ describe('api', () => { expect(api.isFocused).toBeFalsy(); }); - it('should update isActive getter', () => { + test('should update isActive getter', () => { expect(api.isFocused).toBeFalsy(); api._onDidActiveChange.fire({ isActive: true }); @@ -27,7 +44,7 @@ describe('api', () => { expect(api.isActive).toBeFalsy(); }); - it('should update isActive getter', () => { + test('should update isActive getter', () => { expect(api.isVisible).toBeTruthy(); api._onDidVisibilityChange.fire({ isVisible: false }); @@ -37,7 +54,7 @@ describe('api', () => { expect(api.isVisible).toBeTruthy(); }); - it('should update width and height getter', () => { + test('should update width and height getter', () => { expect(api.height).toBe(0); expect(api.width).toBe(0); diff --git a/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts b/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts index 243a2f4d1..1226b6cb2 100644 --- a/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts +++ b/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts @@ -1,4 +1,4 @@ -import { DockviewPanelApiImpl, TitleEvent } from '../../api/dockviewPanelApi'; +import { DockviewPanelApiImpl } from '../../api/dockviewPanelApi'; import { DockviewComponent } from '../../dockview/dockviewComponent'; import { DockviewPanel, IDockviewPanel } from '../../dockview/dockviewPanel'; import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; @@ -8,6 +8,7 @@ describe('groupPanelApi', () => { const panelMock = jest.fn(() => { return { update: jest.fn(), + setTitle: jest.fn(), } as any; }); const groupMock = jest.fn(() => { @@ -20,11 +21,38 @@ describe('groupPanelApi', () => { const cut = new DockviewPanelApiImpl(panel, group); cut.setTitle('test_title'); + expect(panel.setTitle).toBeCalledTimes(1); + expect(panel.setTitle).toBeCalledWith('test_title'); + }); - expect(panel.update).toBeCalledTimes(1); - expect(panel.update).toBeCalledWith({ - params: { title: 'test_title' }, + test('updateParameters', () => { + const groupPanel: Partial = { + id: 'test_id', + update: jest.fn(), + }; + + const accessor: Partial = { + onDidAddPanel: jest.fn(), + onDidRemovePanel: jest.fn(), + options: {}, + }; + const groupViewPanel = new DockviewGroupPanel( + accessor, + '', + {} + ); + + const cut = new DockviewPanelApiImpl( + groupPanel, + groupViewPanel + ); + + cut.updateParameters({ keyA: 'valueA' }); + + expect(groupPanel.update).toHaveBeenCalledWith({ + params: { keyA: 'valueA' }, }); + expect(groupPanel.update).toHaveBeenCalledTimes(1); }); test('onDidGroupChange', () => { diff --git a/packages/dockview-core/src/__tests__/dnd/abstractDragHandler.spec.ts b/packages/dockview-core/src/__tests__/dnd/abstractDragHandler.spec.ts index ebb55aae7..312916ae5 100644 --- a/packages/dockview-core/src/__tests__/dnd/abstractDragHandler.spec.ts +++ b/packages/dockview-core/src/__tests__/dnd/abstractDragHandler.spec.ts @@ -20,10 +20,6 @@ describe('abstractDragHandler', () => { }, }; } - - dispose(): void { - super.dispose(); - } })(element); expect(element.classList.contains('dv-dragged')).toBeFalsy(); @@ -62,10 +58,6 @@ describe('abstractDragHandler', () => { }, }; } - - dispose(): void { - // - } })(element); expect(iframe.style.pointerEvents).toBeFalsy(); @@ -84,4 +76,46 @@ describe('abstractDragHandler', () => { handler.dispose(); }); + + test('that the disabling of pointerEvents is restored on a premature disposal of the handler', () => { + jest.useFakeTimers(); + + const element = document.createElement('div'); + const iframe = document.createElement('iframe'); + const webview = document.createElement('webview'); + const span = document.createElement('span'); + + document.body.appendChild(element); + document.body.appendChild(iframe); + document.body.appendChild(webview); + document.body.appendChild(span); + + const handler = new (class TestClass extends DragHandler { + constructor(el: HTMLElement) { + super(el); + } + + getData(): IDisposable { + return { + dispose: () => { + // / + }, + }; + } + })(element); + + expect(iframe.style.pointerEvents).toBeFalsy(); + expect(webview.style.pointerEvents).toBeFalsy(); + expect(span.style.pointerEvents).toBeFalsy(); + + fireEvent.dragStart(element); + expect(iframe.style.pointerEvents).toBe('none'); + expect(webview.style.pointerEvents).toBe('none'); + expect(span.style.pointerEvents).toBeFalsy(); + + handler.dispose(); + expect(iframe.style.pointerEvents).toBe('auto'); + expect(webview.style.pointerEvents).toBe('auto'); + expect(span.style.pointerEvents).toBeFalsy(); + }); }); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 253cd4f1e..ae3659753 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -46,6 +46,7 @@ class PanelContentPartTest implements IContentRenderer { dispose(): void { this.isDisposed = true; this._onDidDispose.fire(); + this._onDidDispose.dispose(); } } @@ -80,6 +81,7 @@ class PanelTabPartTest implements ITabRenderer { dispose(): void { this.isDisposed = true; this._onDidDispose.fire(); + this._onDidDispose.dispose(); } } @@ -98,6 +100,68 @@ describe('dockviewComponent', () => { }); }); + test('event leakage', () => { + Emitter.setLeakageMonitorEnabled(true); + + dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + }); + + dockview.layout(500, 1000); + + dockview.addPanel({ + id: 'panel1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel2', + component: 'default', + }); + + dockview.removePanel(panel2); + + const panel3 = dockview.addPanel({ + id: 'panel3', + component: 'default', + position: { + direction: 'right', + referencePanel: 'panel1', + }, + }); + + const panel4 = dockview.addPanel({ + id: 'panel4', + component: 'default', + position: { + direction: 'above', + }, + }); + + dockview.moveGroupOrPanel( + panel4.group, + panel3.group.id, + panel3.id, + 'center' + ); + + 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'); + } + + Emitter.setLeakageMonitorEnabled(false); + }); + test('duplicate panel', () => { dockview.layout(500, 1000); @@ -112,6 +176,8 @@ describe('dockviewComponent', () => { component: 'default', }); }).toThrowError('panel with id panel1 already exists'); + + dockview.dispose(); }); test('set active panel', () => { @@ -1285,21 +1351,21 @@ describe('dockviewComponent', () => { tabComponent: 'default', }); - const panel2 = dockview.addPanel({ - id: 'panel2', - component: 'default', - tabComponent: 'default', - }); + // const panel2 = dockview.addPanel({ + // id: 'panel2', + // component: 'default', + // tabComponent: 'default', + // }); - expect(panel1.group).toEqual(panel2.group); + // expect(panel1.group).toEqual(panel2.group); const panel1Spy = jest.spyOn(panel1, 'dispose'); - const panel2Spy = jest.spyOn(panel2, 'dispose'); + // const panel2Spy = jest.spyOn(panel2, 'dispose'); dockview.dispose(); expect(panel1Spy).toBeCalledTimes(1); - expect(panel2Spy).toBeCalledTimes(1); + // expect(panel2Spy).toBeCalledTimes(1); }); test('panel is disposed of when from JSON is called', () => { @@ -2295,4 +2361,160 @@ describe('dockviewComponent', () => { panels: {}, }); }); + + test('that title and params.title do not conflict', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(100, 100); + + dockview.addPanel({ + id: 'panel1', + component: 'default', + title: 'Panel 1', + params: { + title: 'Panel 1', + }, + }); + + dockview.addPanel({ + id: 'panel2', + component: 'default', + title: 'Panel 2', + }); + + dockview.addPanel({ + id: 'panel3', + component: 'default', + params: { + title: 'Panel 3', + }, + }); + + expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({ + grid: { + root: { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + views: ['panel1', 'panel2', 'panel3'], + activeView: 'panel3', + id: '1', + }, + size: 100, + }, + ], + size: 100, + }, + width: 100, + height: 100, + orientation: 'HORIZONTAL', + }, + panels: { + panel1: { + id: 'panel1', + contentComponent: 'default', + params: { + title: 'Panel 1', + }, + title: 'Panel 1', + }, + panel2: { + id: 'panel2', + contentComponent: 'default', + title: 'Panel 2', + }, + panel3: { + id: 'panel3', + contentComponent: 'default', + params: { + title: 'Panel 3', + }, + title: 'panel3', + }, + }, + activeGroup: '1', + }); + }); + + test('check dockview component is rendering to the DOM as expected', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(100, 100); + + const panel1 = dockview.addPanel({ + id: 'panel1', + component: 'default', + }); + + expect(dockview.element.querySelectorAll('.view').length).toBe(1); + + const panel2 = dockview.addPanel({ + id: 'panel2', + component: 'default', + }); + + expect(dockview.element.querySelectorAll('.view').length).toBe(1); + + const panel3 = dockview.addPanel({ + id: 'panel3', + component: 'default', + }); + + expect(dockview.element.querySelectorAll('.view').length).toBe(1); + + dockview.moveGroupOrPanel( + panel3.group, + panel3.group.id, + panel3.id, + 'right' + ); + + expect(dockview.groups.length).toBe(2); + expect(dockview.element.querySelectorAll('.view').length).toBe(2); + + dockview.moveGroupOrPanel( + panel3.group, + panel2.group.id, + panel2.id, + 'bottom' + ); + + expect(dockview.groups.length).toBe(3); + expect(dockview.element.querySelectorAll('.view').length).toBe(4); + + dockview.moveGroupOrPanel( + panel2.group, + panel1.group.id, + panel1.id, + 'center' + ); + + expect(dockview.groups.length).toBe(2); + + expect(dockview.element.querySelectorAll('.view').length).toBe(2); + }); }); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewPanel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewPanel.spec.ts index e6d7ab10e..c67546db8 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewPanel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewPanel.spec.ts @@ -37,13 +37,13 @@ describe('dockviewPanel', () => { latestTitle = event.title; }); - expect(cut.title).toBe(''); + expect(cut.title).toBeUndefined(); cut.init({ title: 'new title', params: {} }); expect(latestTitle).toBe('new title'); expect(cut.title).toBe('new title'); - cut.update({ params: { title: 'another title' } }); + cut.setTitle('another title'); expect(latestTitle).toBe('another title'); expect(cut.title).toBe('another title'); @@ -81,6 +81,9 @@ describe('dockviewPanel', () => { cut.setTitle('newTitle'); expect(cut.title).toBe('newTitle'); + + cut.api.setTitle('new title 2'); + expect(cut.title).toBe('new title 2'); }); test('dispose cleanup', () => { @@ -142,7 +145,7 @@ describe('dockviewPanel', () => { expect(cut.params).toEqual(undefined); - cut.update({ params: { params: { variableA: 'A', variableB: 'B' } } }); + cut.update({ params: { variableA: 'A', variableB: 'B' } }); expect(cut.params).toEqual({ variableA: 'A', variableB: 'B' }); }); @@ -181,4 +184,67 @@ describe('dockviewPanel', () => { expect(group.api.setSize).toBeCalledWith({ height: 123, width: 456 }); expect(group.api.setSize).toBeCalledTimes(1); }); + + test('updateParameter', () => { + const dockviewApiMock = jest.fn(() => { + return {} as any; + }); + const accessorMock = jest.fn(() => { + return {} as any; + }); + const groupMock = jest.fn(() => { + return {} as any; + }); + const panelModelMock = jest.fn, []>(() => { + return { + update: jest.fn(), + init: jest.fn(), + dispose: jest.fn(), + }; + }); + + const api = new dockviewApiMock(); + const accessor = new accessorMock(); + const group = new groupMock(); + const model = new panelModelMock(); + + const cut = new DockviewPanel('fake-id', accessor, api, group, model); + + cut.init({ params: { a: '1', b: '2' }, title: 'A title' }); + expect(cut.params).toEqual({ a: '1', b: '2' }); + + // update 'a' and add 'c' + cut.update({ params: { a: '-1', c: '3' } }); + expect(cut.params).toEqual({ a: '-1', b: '2', c: '3' }); + + cut.update({ params: { d: '4', e: '5', f: '6' } }); + expect(cut.params).toEqual({ + a: '-1', + b: '2', + c: '3', + d: '4', + e: '5', + f: '6', + }); + + cut.update({ + params: { + d: '', + e: null, + f: undefined, + g: '', + h: null, + i: undefined, + }, + }); + expect(cut.params).toEqual({ + a: '-1', + b: '2', + c: '3', + d: '', + e: null, + g: '', + h: null, + }); + }); }); diff --git a/packages/dockview-core/src/__tests__/events.spec.ts b/packages/dockview-core/src/__tests__/events.spec.ts index 69790cee4..532390a07 100644 --- a/packages/dockview-core/src/__tests__/events.spec.ts +++ b/packages/dockview-core/src/__tests__/events.spec.ts @@ -1,7 +1,16 @@ -import { Emitter, Event } from '../events'; +import { + Emitter, + Event, + addDisposableListener, + addDisposableWindowListener, +} from '../events'; describe('events', () => { describe('emitter', () => { + it('debug mode is off', () => { + expect(Emitter.ENABLE_TRACKING).toBeFalsy(); + }); + it('should emit values', () => { const emitter = new Emitter(); let value: number | undefined = undefined; @@ -97,4 +106,138 @@ describe('events', () => { emitter3.fire(3); expect(value).toBe(3); }); + + it('addDisposableWindowListener with capture options', () => { + const element = { + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }; + + const handler = jest.fn(); + + const disposable = addDisposableWindowListener( + element as any, + 'mousedown', + handler, + true + ); + + expect(element.addEventListener).toBeCalledTimes(1); + expect(element.addEventListener).toHaveBeenCalledWith( + 'mousedown', + handler, + true + ); + expect(element.removeEventListener).toBeCalledTimes(0); + + disposable.dispose(); + + expect(element.addEventListener).toBeCalledTimes(1); + expect(element.removeEventListener).toBeCalledTimes(1); + expect(element.removeEventListener).toBeCalledWith( + 'mousedown', + handler, + true + ); + }); + + it('addDisposableWindowListener without capture options', () => { + const element = { + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }; + + const handler = jest.fn(); + + const disposable = addDisposableWindowListener( + element as any, + 'mousedown', + handler + ); + + expect(element.addEventListener).toBeCalledTimes(1); + expect(element.addEventListener).toHaveBeenCalledWith( + 'mousedown', + handler, + undefined + ); + expect(element.removeEventListener).toBeCalledTimes(0); + + disposable.dispose(); + + expect(element.addEventListener).toBeCalledTimes(1); + expect(element.removeEventListener).toBeCalledTimes(1); + expect(element.removeEventListener).toBeCalledWith( + 'mousedown', + handler, + undefined + ); + }); + + it('addDisposableListener with capture options', () => { + const element = { + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }; + + const handler = jest.fn(); + + const disposable = addDisposableListener( + element as any, + 'mousedown', + handler, + true + ); + + expect(element.addEventListener).toBeCalledTimes(1); + expect(element.addEventListener).toHaveBeenCalledWith( + 'mousedown', + handler, + true + ); + expect(element.removeEventListener).toBeCalledTimes(0); + + disposable.dispose(); + + expect(element.addEventListener).toBeCalledTimes(1); + expect(element.removeEventListener).toBeCalledTimes(1); + expect(element.removeEventListener).toBeCalledWith( + 'mousedown', + handler, + true + ); + }); + + it('addDisposableListener without capture options', () => { + const element = { + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }; + + const handler = jest.fn(); + + const disposable = addDisposableListener( + element as any, + 'mousedown', + handler + ); + + expect(element.addEventListener).toBeCalledTimes(1); + expect(element.addEventListener).toHaveBeenCalledWith( + 'mousedown', + handler, + undefined + ); + expect(element.removeEventListener).toBeCalledTimes(0); + + disposable.dispose(); + + expect(element.addEventListener).toBeCalledTimes(1); + expect(element.removeEventListener).toBeCalledTimes(1); + expect(element.removeEventListener).toBeCalledWith( + 'mousedown', + handler, + undefined + ); + }); }); diff --git a/packages/dockview-core/src/__tests__/gridview/gridview.spec.ts b/packages/dockview-core/src/__tests__/gridview/gridview.spec.ts index 79cdd3de5..bb67ad2ea 100644 --- a/packages/dockview-core/src/__tests__/gridview/gridview.spec.ts +++ b/packages/dockview-core/src/__tests__/gridview/gridview.spec.ts @@ -18,6 +18,10 @@ class MockGridview implements IGridView { >().event; element: HTMLElement = document.createElement('div'); + constructor() { + this.element.className = 'mock-grid-view'; + } + layout(width: number, height: number): void { // } @@ -116,4 +120,574 @@ describe('gridview', () => { checkOrientationFlipsAtEachLevel((gridview as any).root as BranchNode); }); + + test('removeView: remove leaf from branch where branch becomes leaf and parent is root', () => { + const gridview = new Gridview( + false, + { separatorBorder: '' }, + Orientation.HORIZONTAL + ); + gridview.layout(1000, 1000); + + gridview.addView(new MockGridview(), Sizing.Distribute, [0]); + gridview.addView(new MockGridview(), Sizing.Distribute, [1]); + + gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0]); + + expect(gridview.serialize()).toEqual({ + height: 1000, + orientation: 'HORIZONTAL', + root: { + data: [ + { + data: {}, + size: 500, + type: 'leaf', + }, + { + data: [ + { + data: {}, + size: 500, + type: 'leaf', + }, + { + data: {}, + size: 500, + type: 'leaf', + }, + ], + size: 500, + type: 'branch', + }, + ], + size: 1000, + type: 'branch', + }, + width: 1000, + }); + expect( + gridview.element.querySelectorAll('.mock-grid-view').length + ).toBe(3); + + gridview.removeView([1, 0], Sizing.Distribute); + + expect(gridview.serialize()).toEqual({ + height: 1000, + orientation: 'HORIZONTAL', + root: { + data: [ + { + data: {}, + size: 500, + type: 'leaf', + }, + { + data: {}, + size: 500, + type: 'leaf', + }, + ], + size: 1000, + type: 'branch', + }, + width: 1000, + }); + expect( + gridview.element.querySelectorAll('.mock-grid-view').length + ).toBe(2); + }); + + test('removeView: remove leaf from branch where branch remains branch and parent is root', () => { + const gridview = new Gridview( + false, + { separatorBorder: '' }, + Orientation.HORIZONTAL + ); + gridview.layout(1000, 1000); + + gridview.addView(new MockGridview(), Sizing.Distribute, [0]); + gridview.addView(new MockGridview(), Sizing.Distribute, [1]); + + gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0]); + gridview.addView(new MockGridview(), Sizing.Distribute, [1, 1]); + + expect(gridview.serialize()).toEqual({ + height: 1000, + orientation: 'HORIZONTAL', + root: { + data: [ + { + data: {}, + size: 500, + type: 'leaf', + }, + { + data: [ + { + data: {}, + size: 333, + type: 'leaf', + }, + { + data: {}, + size: 333, + type: 'leaf', + }, + { + data: {}, + size: 334, + type: 'leaf', + }, + ], + size: 500, + type: 'branch', + }, + ], + size: 1000, + type: 'branch', + }, + width: 1000, + }); + expect( + gridview.element.querySelectorAll('.mock-grid-view').length + ).toBe(4); + + gridview.removeView([1, 0], Sizing.Distribute); + + expect(gridview.serialize()).toEqual({ + height: 1000, + orientation: 'HORIZONTAL', + root: { + data: [ + { + data: {}, + size: 500, + type: 'leaf', + }, + { + data: [ + { + data: {}, + size: 500, + type: 'leaf', + }, + { + data: {}, + size: 500, + type: 'leaf', + }, + ], + size: 500, + type: 'branch', + }, + ], + size: 1000, + type: 'branch', + }, + width: 1000, + }); + expect( + gridview.element.querySelectorAll('.mock-grid-view').length + ).toBe(3); + }); + + test('removeView: remove leaf where parent is root', () => { + const gridview = new Gridview( + false, + { separatorBorder: '' }, + Orientation.HORIZONTAL + ); + gridview.layout(1000, 1000); + + gridview.addView(new MockGridview(), Sizing.Distribute, [0]); + gridview.addView(new MockGridview(), Sizing.Distribute, [1]); + + gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0]); + + expect(gridview.serialize()).toEqual({ + height: 1000, + orientation: 'HORIZONTAL', + root: { + data: [ + { + data: {}, + size: 500, + type: 'leaf', + }, + { + data: [ + { + data: {}, + size: 500, + type: 'leaf', + }, + { + data: {}, + size: 500, + type: 'leaf', + }, + ], + size: 500, + type: 'branch', + }, + ], + size: 1000, + type: 'branch', + }, + width: 1000, + }); + expect( + gridview.element.querySelectorAll('.mock-grid-view').length + ).toBe(3); + + gridview.removeView([0], Sizing.Distribute); + + expect(gridview.serialize()).toEqual({ + height: 1000, + orientation: 'VERTICAL', + root: { + data: [ + { + data: {}, + size: 500, + type: 'leaf', + }, + { + data: {}, + size: 500, + type: 'leaf', + }, + ], + size: 1000, + type: 'branch', + }, + width: 1000, + }); + expect( + gridview.element.querySelectorAll('.mock-grid-view').length + ).toBe(2); + }); + + test('removeView: remove leaf from branch where branch becomes leaf and parent is not root', () => { + const gridview = new Gridview( + false, + { separatorBorder: '' }, + Orientation.HORIZONTAL + ); + gridview.layout(1000, 1000); + + gridview.addView(new MockGridview(), Sizing.Distribute, [0]); + gridview.addView(new MockGridview(), Sizing.Distribute, [1]); + + gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0]); + gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0, 0]); + + expect(gridview.serialize()).toEqual({ + height: 1000, + orientation: 'HORIZONTAL', + root: { + data: [ + { + data: {}, + size: 500, + type: 'leaf', + }, + { + data: [ + { + data: [ + { + data: {}, + size: 250, + type: 'leaf', + }, + { + data: {}, + size: 250, + type: 'leaf', + }, + ], + size: 500, + type: 'branch', + }, + { + data: {}, + size: 500, + type: 'leaf', + }, + ], + size: 500, + type: 'branch', + }, + ], + size: 1000, + type: 'branch', + }, + width: 1000, + }); + expect( + gridview.element.querySelectorAll('.mock-grid-view').length + ).toBe(4); + + gridview.removeView([1, 0, 0], Sizing.Distribute); + + expect(gridview.serialize()).toEqual({ + height: 1000, + orientation: 'HORIZONTAL', + root: { + data: [ + { + data: {}, + size: 500, + type: 'leaf', + }, + { + data: [ + { + data: {}, + size: 500, + type: 'leaf', + }, + { + data: {}, + size: 500, + type: 'leaf', + }, + ], + size: 500, + type: 'branch', + }, + ], + size: 1000, + type: 'branch', + }, + width: 1000, + }); + expect( + gridview.element.querySelectorAll('.mock-grid-view').length + ).toBe(3); + }); + + test('removeView: remove leaf from branch where branch remains branch and parent is not root', () => { + const gridview = new Gridview( + false, + { separatorBorder: '' }, + Orientation.HORIZONTAL + ); + gridview.layout(1000, 1000); + + gridview.addView(new MockGridview(), Sizing.Distribute, [0]); + gridview.addView(new MockGridview(), Sizing.Distribute, [1]); + + gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0]); + gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0, 0]); + gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0, 1]); + + expect(gridview.serialize()).toEqual({ + height: 1000, + orientation: 'HORIZONTAL', + root: { + data: [ + { + data: {}, + size: 500, + type: 'leaf', + }, + { + data: [ + { + data: [ + { + data: {}, + size: 166, + type: 'leaf', + }, + { + data: {}, + size: 166, + type: 'leaf', + }, + { + data: {}, + size: 168, + type: 'leaf', + }, + ], + size: 500, + type: 'branch', + }, + { + data: {}, + size: 500, + type: 'leaf', + }, + ], + size: 500, + type: 'branch', + }, + ], + size: 1000, + type: 'branch', + }, + width: 1000, + }); + expect( + gridview.element.querySelectorAll('.mock-grid-view').length + ).toBe(5); + + gridview.removeView([1, 0, 1], Sizing.Distribute); + + expect(gridview.serialize()).toEqual({ + height: 1000, + orientation: 'HORIZONTAL', + root: { + data: [ + { + data: {}, + size: 500, + type: 'leaf', + }, + { + data: [ + { + data: [ + { + data: {}, + size: 250, + type: 'leaf', + }, + { + data: {}, + size: 250, + type: 'leaf', + }, + ], + size: 500, + type: 'branch', + }, + { + data: {}, + size: 500, + type: 'leaf', + }, + ], + size: 500, + type: 'branch', + }, + ], + size: 1000, + type: 'branch', + }, + width: 1000, + }); + expect( + gridview.element.querySelectorAll('.mock-grid-view').length + ).toBe(4); + }); + + test('removeView: remove leaf where parent is root', () => { + const gridview = new Gridview( + false, + { separatorBorder: '' }, + Orientation.HORIZONTAL + ); + gridview.layout(1000, 1000); + + gridview.addView(new MockGridview(), Sizing.Distribute, [0]); + gridview.addView(new MockGridview(), Sizing.Distribute, [1]); + + gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0]); + gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0, 0]); + gridview.addView(new MockGridview(), Sizing.Distribute, [1, 0, 1]); + + expect(gridview.serialize()).toEqual({ + height: 1000, + orientation: 'HORIZONTAL', + root: { + data: [ + { + data: {}, + size: 500, + type: 'leaf', + }, + { + data: [ + { + data: [ + { + data: {}, + size: 166, + type: 'leaf', + }, + { + data: {}, + size: 166, + type: 'leaf', + }, + { + data: {}, + size: 168, + type: 'leaf', + }, + ], + size: 500, + type: 'branch', + }, + { + data: {}, + size: 500, + type: 'leaf', + }, + ], + size: 500, + type: 'branch', + }, + ], + size: 1000, + type: 'branch', + }, + width: 1000, + }); + expect( + gridview.element.querySelectorAll('.mock-grid-view').length + ).toBe(5); + + gridview.removeView([1, 1], Sizing.Distribute); + + expect(gridview.serialize()).toEqual({ + height: 1000, + orientation: 'HORIZONTAL', + root: { + data: [ + { + data: {}, + size: 500, + type: 'leaf', + }, + { + data: {}, + size: 166, + type: 'leaf', + }, + { + data: {}, + size: 166, + type: 'leaf', + }, + { + data: {}, + size: 168, + type: 'leaf', + }, + ], + size: 1000, + type: 'branch', + }, + width: 1000, + }); + expect( + gridview.element.querySelectorAll('.mock-grid-view').length + ).toBe(4); + }); }); diff --git a/packages/dockview-core/src/__tests__/groupview/dockviewGroupPanelModel.spec.ts b/packages/dockview-core/src/__tests__/groupview/dockviewGroupPanelModel.spec.ts index 3cd48758c..155f06986 100644 --- a/packages/dockview-core/src/__tests__/groupview/dockviewGroupPanelModel.spec.ts +++ b/packages/dockview-core/src/__tests__/groupview/dockviewGroupPanelModel.spec.ts @@ -1,6 +1,5 @@ import {DockviewComponent} from '../../dockview/dockviewComponent'; import { - GroupPanelUpdateEvent, GroupviewPanelState, IGroupPanelInitParameters, GroupPanelPartInitParameters, @@ -39,7 +38,7 @@ class TestModel implements IDockviewPanelModel { this.tab = new TestContentPart(id); } - update(event: GroupPanelUpdateEvent): void { + update(event: PanelUpdateEvent): void { // } @@ -203,6 +202,10 @@ export class TestPanel implements IDockviewPanel { //noop } + setTitle(title: string): void { + // + } + update(event: PanelUpdateEvent) { //noop } diff --git a/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts b/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts index 60d94d513..a93d7daf4 100644 --- a/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts +++ b/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts @@ -7,7 +7,7 @@ import { Sizing, Splitview, } from '../../splitview/splitview'; - +import { fireEvent } from '@testing-library/dom'; class Testview implements IView { private _element: HTMLElement = document.createElement('div'); private _size = 0; @@ -84,6 +84,8 @@ describe('splitview', () => { beforeEach(() => { container = document.createElement('div'); container.className = 'container'; + + jest.clearAllMocks(); }); test('vertical splitview', () => { @@ -585,8 +587,149 @@ describe('splitview', () => { expect(container.childNodes.length).toBeGreaterThan(0); - splitview.dispose(); + let anyEvents = false; + const listener = splitview.onDidRemoveView((e) => { + anyEvents = true; // disposing of the splitview shouldn't fire onDidRemoveView events + }); + splitview.dispose(); + listener.dispose(); + + expect(anyEvents).toBeFalsy(); expect(container.childNodes.length).toBe(0); }); + + test('dnd: mouse events to move sash', () => { + const splitview = new Splitview(container, { + orientation: Orientation.HORIZONTAL, + proportionalLayout: false, + }); + splitview.layout(400, 500); + + const view1 = new Testview(0, 1000); + const view2 = new Testview(0, 1000); + + splitview.addView(view1); + splitview.addView(view2); + + const addEventListenerSpy = jest.spyOn(document, 'addEventListener'); + const removeEventListenerSpy = jest.spyOn( + document, + 'removeEventListener' + ); + + const sashElement = container + .getElementsByClassName('sash') + .item(0) as HTMLElement; + + // validate the expected state before drag + expect([view1.size, view2.size]).toEqual([200, 200]); + expect(sashElement).toBeTruthy(); + expect(view1.element.parentElement!.style.pointerEvents).toBe(''); + expect(view2.element.parentElement!.style.pointerEvents).toBe(''); + + // start the drag event + fireEvent.mouseDown(sashElement, { clientX: 50, clientY: 100 }); + + expect(addEventListenerSpy).toBeCalledTimes(5); + + // during a sash drag the views should have pointer-events disabled + expect(view1.element.parentElement!.style.pointerEvents).toBe('none'); + expect(view2.element.parentElement!.style.pointerEvents).toBe('none'); + + // expect a delta move of 70 - 50 = 20 + fireEvent.mouseMove(document, { clientX: 70, clientY: 110 }); + expect([view1.size, view2.size]).toEqual([220, 180]); + + // expect a delta move of 75 - 70 = 5 + fireEvent.mouseMove(document, { clientX: 75, clientY: 110 }); + expect([view1.size, view2.size]).toEqual([225, 175]); + + // end the drag event + fireEvent.mouseUp(document); + + expect(removeEventListenerSpy).toBeCalledTimes(5); + + // expect pointer-eventes on views to be restored + expect(view1.element.parentElement!.style.pointerEvents).toBe(''); + expect(view2.element.parentElement!.style.pointerEvents).toBe(''); + + fireEvent.mouseMove(document, { clientX: 100, clientY: 100 }); + // expect no additional resizes + expect([view1.size, view2.size]).toEqual([225, 175]); + // expect no additional document listeners + expect(addEventListenerSpy).toBeCalledTimes(5); + expect(removeEventListenerSpy).toBeCalledTimes(5); + }); + + test('dnd: touch events to move sash', () => { + const splitview = new Splitview(container, { + orientation: Orientation.HORIZONTAL, + proportionalLayout: false, + }); + splitview.layout(400, 500); + + const view1 = new Testview(0, 1000); + const view2 = new Testview(0, 1000); + + splitview.addView(view1); + splitview.addView(view2); + + const addEventListenerSpy = jest.spyOn(document, 'addEventListener'); + const removeEventListenerSpy = jest.spyOn( + document, + 'removeEventListener' + ); + + const sashElement = container + .getElementsByClassName('sash') + .item(0) as HTMLElement; + + // validate the expected state before drag + expect([view1.size, view2.size]).toEqual([200, 200]); + expect(sashElement).toBeTruthy(); + expect(view1.element.parentElement!.style.pointerEvents).toBe(''); + expect(view2.element.parentElement!.style.pointerEvents).toBe(''); + + // start the drag event + fireEvent.touchStart(sashElement, { + touches: [{ clientX: 50, clientY: 100 }], + }); + + expect(addEventListenerSpy).toBeCalledTimes(5); + + // during a sash drag the views should have pointer-events disabled + expect(view1.element.parentElement!.style.pointerEvents).toBe('none'); + expect(view2.element.parentElement!.style.pointerEvents).toBe('none'); + + // expect a delta move of 70 - 50 = 20 + fireEvent.touchMove(document, { + touches: [{ clientX: 70, clientY: 110 }], + }); + expect([view1.size, view2.size]).toEqual([220, 180]); + + // expect a delta move of 75 - 70 = 5 + fireEvent.touchMove(document, { + touches: [{ clientX: 75, clientY: 110 }], + }); + expect([view1.size, view2.size]).toEqual([225, 175]); + + // end the drag event + fireEvent.touchEnd(document); + + expect(removeEventListenerSpy).toBeCalledTimes(5); + + // expect pointer-eventes on views to be restored + expect(view1.element.parentElement!.style.pointerEvents).toBe(''); + expect(view2.element.parentElement!.style.pointerEvents).toBe(''); + + fireEvent.touchMove(document, { + touches: [{ clientX: 100, clientY: 100 }], + }); + // expect no additional resizes + expect([view1.size, view2.size]).toEqual([225, 175]); + // expect no additional document listeners + expect(addEventListenerSpy).toBeCalledTimes(5); + expect(removeEventListenerSpy).toBeCalledTimes(5); + }); }); diff --git a/packages/dockview-core/src/__tests__/splitview/splitviewComponent.spec.ts b/packages/dockview-core/src/__tests__/splitview/splitviewComponent.spec.ts index e3c16e751..e43c94239 100644 --- a/packages/dockview-core/src/__tests__/splitview/splitviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/splitview/splitviewComponent.spec.ts @@ -1,4 +1,5 @@ import { PanelDimensionChangeEvent } from '../../api/panelApi'; +import { Emitter } from '../../events'; import { CompositeDisposable } from '../../lifecycle'; import { Orientation } from '../../splitview/splitview'; import { SplitviewComponent } from '../../splitview/splitviewComponent'; @@ -25,6 +26,45 @@ describe('componentSplitview', () => { container.className = 'container'; }); + test('event leakage', () => { + Emitter.setLeakageMonitorEnabled(true); + + const splitview = new SplitviewComponent({ + parentElement: container, + orientation: Orientation.VERTICAL, + components: { + testPanel: TestPanel, + }, + }); + splitview.layout(600, 400); + + const panel1 = splitview.addPanel({ + id: 'panel1', + component: 'testPanel', + }); + const panel2 = splitview.addPanel({ + id: 'panel2', + component: 'testPanel', + }); + + splitview.movePanel(0, 1); + + splitview.removePanel(panel1); + + splitview.dispose(); + + if (Emitter.MEMORY_LEAK_WATCHER.size > 0) { + for (const entry of Array.from( + Emitter.MEMORY_LEAK_WATCHER.events + )) { + console.log(entry[1]); + } + throw new Error('not all listeners disposed'); + } + + Emitter.setLeakageMonitorEnabled(false); + }); + test('remove panel', () => { const splitview = new SplitviewComponent({ parentElement: container, diff --git a/packages/dockview-core/src/api/dockviewPanelApi.ts b/packages/dockview-core/src/api/dockviewPanelApi.ts index 48978c688..5c6222071 100644 --- a/packages/dockview-core/src/api/dockviewPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewPanelApi.ts @@ -19,7 +19,7 @@ export interface DockviewPanelApi > { readonly group: DockviewGroupPanel; readonly isGroupActive: boolean; - readonly title: string; + readonly title: string | undefined; readonly onDidActiveGroupChange: Event; readonly onDidGroupChange: Event; close(): void; @@ -43,7 +43,7 @@ export class DockviewPanelApiImpl private readonly disposable = new MutableDisposable(); - get title(): string { + get title(): string | undefined { return this.panel.title; } @@ -89,7 +89,7 @@ export class DockviewPanelApiImpl } public setTitle(title: string): void { - this.panel.update({ params: { title } }); + this.panel.setTitle(title); } public close(): void { diff --git a/packages/dockview-core/src/api/panelApi.ts b/packages/dockview-core/src/api/panelApi.ts index 37648cd84..95d35a3b8 100644 --- a/packages/dockview-core/src/api/panelApi.ts +++ b/packages/dockview-core/src/api/panelApi.ts @@ -126,15 +126,6 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi { super(); this.addDisposables( - this.panelUpdatesDisposable, - this._onDidDimensionChange, - this._onDidChangeFocus, - this._onDidVisibilityChange, - this._onDidActiveChange, - this._onFocusEvent, - this._onActiveChange, - this._onVisibilityChange, - this._onUpdateParameters, this.onDidFocusChange((event) => { this._isFocused = event.isFocused; }), @@ -147,7 +138,16 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi { this.onDidDimensionsChange((event) => { this._width = event.width; this._height = event.height; - }) + }), + this.panelUpdatesDisposable, + this._onDidDimensionChange, + this._onDidChangeFocus, + this._onDidVisibilityChange, + this._onDidActiveChange, + this._onFocusEvent, + this._onActiveChange, + this._onVisibilityChange, + this._onUpdateParameters ); } @@ -155,9 +155,7 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi { this.panelUpdatesDisposable.value = this._onUpdateParameters.event( (parameters) => { panel.update({ - params: { - params: parameters, - }, + params: parameters, }); } ); diff --git a/packages/dockview-core/src/dnd/abstractDragHandler.ts b/packages/dockview-core/src/dnd/abstractDragHandler.ts index 2120b8fc5..55e03fa42 100644 --- a/packages/dockview-core/src/dnd/abstractDragHandler.ts +++ b/packages/dockview-core/src/dnd/abstractDragHandler.ts @@ -7,15 +7,21 @@ import { } from '../lifecycle'; export abstract class DragHandler extends CompositeDisposable { - private readonly disposable = new MutableDisposable(); + private readonly dataDisposable = new MutableDisposable(); + private readonly pointerEventsDisposable = new MutableDisposable(); private readonly _onDragStart = new Emitter(); readonly onDragStart = this._onDragStart.event; - private iframes: HTMLElement[] = []; - constructor(protected readonly el: HTMLElement) { super(); + + this.addDisposables( + this._onDragStart, + this.dataDisposable, + this.pointerEventsDisposable + ); + this.configure(); } @@ -25,19 +31,27 @@ export abstract class DragHandler extends CompositeDisposable { this.addDisposables( this._onDragStart, addDisposableListener(this.el, 'dragstart', (event) => { - this.iframes = [ + const iframes = [ ...getElementsByTagName('iframe'), ...getElementsByTagName('webview'), ]; - for (const iframe of this.iframes) { + this.pointerEventsDisposable.value = { + dispose: () => { + for (const iframe of iframes) { + iframe.style.pointerEvents = 'auto'; + } + }, + }; + + for (const iframe of iframes) { iframe.style.pointerEvents = 'none'; } this.el.classList.add('dv-dragged'); setTimeout(() => this.el.classList.remove('dv-dragged'), 0); - this.disposable.value = this.getData(event.dataTransfer); + this.dataDisposable.value = this.getData(event.dataTransfer); if (event.dataTransfer) { event.dataTransfer.effectAllowed = 'move'; @@ -58,12 +72,8 @@ export abstract class DragHandler extends CompositeDisposable { } }), addDisposableListener(this.el, 'dragend', () => { - for (const iframe of this.iframes) { - iframe.style.pointerEvents = 'auto'; - } - this.iframes = []; - - this.disposable.dispose(); + this.pointerEventsDisposable.dispose(); + this.dataDisposable.dispose(); }) ); } diff --git a/packages/dockview-core/src/dnd/droptarget.ts b/packages/dockview-core/src/dnd/droptarget.ts index cac6d30ad..12fcb0ea0 100644 --- a/packages/dockview-core/src/dnd/droptarget.ts +++ b/packages/dockview-core/src/dnd/droptarget.ts @@ -177,6 +177,7 @@ export class Droptarget extends CompositeDisposable { public dispose(): void { this.removeDropTarget(); + super.dispose(); } private toggleClasses( diff --git a/packages/dockview-core/src/dnd/groupDragHandler.ts b/packages/dockview-core/src/dnd/groupDragHandler.ts index e7f99e062..bdd183182 100644 --- a/packages/dockview-core/src/dnd/groupDragHandler.ts +++ b/packages/dockview-core/src/dnd/groupDragHandler.ts @@ -53,8 +53,4 @@ export class GroupDragHandler extends DragHandler { }, }; } - - public dispose(): void { - // - } } diff --git a/packages/dockview-core/src/dockview/components/panel/content.ts b/packages/dockview-core/src/dockview/components/panel/content.ts index 75859e4d2..9934c5ec4 100644 --- a/packages/dockview-core/src/dockview/components/panel/content.ts +++ b/packages/dockview-core/src/dockview/components/panel/content.ts @@ -77,11 +77,12 @@ export class ContentContainer const _onDidFocus = this.panel.view.content.onDidFocus; const _onDidBlur = this.panel.view.content.onDidBlur; - const { onDidFocus, onDidBlur } = trackFocus(this._element); + const focusTracker = trackFocus(this._element); disposable.addDisposables( - onDidFocus(() => this._onDidFocus.fire()), - onDidBlur(() => this._onDidBlur.fire()) + focusTracker, + focusTracker.onDidFocus(() => this._onDidFocus.fire()), + focusTracker.onDidBlur(() => this._onDidBlur.fire()) ); if (_onDidFocus) { diff --git a/packages/dockview-core/src/dockview/components/tab/tab.ts b/packages/dockview-core/src/dockview/components/tab/tab.ts index 550752e53..15c81206b 100644 --- a/packages/dockview-core/src/dockview/components/tab/tab.ts +++ b/packages/dockview-core/src/dockview/components/tab/tab.ts @@ -12,7 +12,7 @@ import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { DroptargetEvent, Droptarget } from '../../../dnd/droptarget'; import { DragHandler } from '../../../dnd/abstractDragHandler'; -export interface ITab { +export interface ITab extends IDisposable { readonly panelId: string; readonly element: HTMLElement; setContent: (element: ITabRenderer) => void; @@ -43,8 +43,6 @@ export class Tab extends CompositeDisposable implements ITab { ) { super(); - this.addDisposables(this._onChanged, this._onDropped); - this._element = document.createElement('div'); this._element.className = 'tab'; this._element.tabIndex = 0; @@ -53,6 +51,8 @@ export class Tab extends CompositeDisposable implements ITab { toggleClass(this.element, 'inactive-tab', true); this.addDisposables( + this._onChanged, + this._onDropped, new (class Handler extends DragHandler { private readonly panelTransfer = LocalSelectionTransfer.getInstance(); @@ -71,10 +71,6 @@ export class Tab extends CompositeDisposable implements ITab { }, }; } - - public dispose(): void { - // - } })(this._element) ); @@ -127,7 +123,8 @@ export class Tab extends CompositeDisposable implements ITab { this.addDisposables( this.droptarget.onDrop((event) => { this._onDropped.fire(event); - }) + }), + this.droptarget ); } @@ -146,6 +143,5 @@ export class Tab extends CompositeDisposable implements ITab { public dispose(): void { super.dispose(); - this.droptarget.dispose(); } } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index debdf42e0..68d8cfe1b 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -216,6 +216,7 @@ export class TabsContainer const { value, disposable } = tabToRemove; disposable.dispose(); + value.dispose(); value.element.remove(); } @@ -275,9 +276,11 @@ export class TabsContainer public dispose(): void { super.dispose(); - this.tabs.forEach((tab) => { - tab.disposable.dispose(); - }); + for (const { value, disposable } of this.tabs) { + disposable.dispose(); + value.dispose(); + } + this.tabs = []; } } diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index de75ca415..804abdac0 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -251,7 +251,6 @@ export class DockviewComponent }); this.addDisposables( - dropTarget, dropTarget.onDrop((event) => { const data = getPanelData(); @@ -270,7 +269,8 @@ export class DockviewComponent getData: getPanelData, }); } - }) + }), + dropTarget ); this._api = new DockviewApi(this); @@ -706,43 +706,49 @@ export class DockviewComponent } moveGroupOrPanel( - referenceGroup: DockviewGroupPanel, - groupId: string, - itemId: string | undefined, - target: Position, - index?: number + destinationGroup: DockviewGroupPanel, + sourceGroupId: string, + sourceItemId: string | undefined, + destinationTarget: Position, + destinationIndex?: number ): void { - const sourceGroup = groupId - ? this._groups.get(groupId)?.value + const sourceGroup = sourceGroupId + ? this._groups.get(sourceGroupId)?.value : undefined; - if (itemId === undefined) { + if (sourceItemId === undefined) { if (sourceGroup) { - this.moveGroup(sourceGroup, referenceGroup, target); + this.moveGroup( + sourceGroup, + destinationGroup, + destinationTarget + ); } return; } - if (!target || target === 'center') { + if (!destinationTarget || destinationTarget === 'center') { const groupItem: IDockviewPanel | undefined = - sourceGroup?.model.removePanel(itemId) || - this.panels.find((panel) => panel.id === itemId); + sourceGroup?.model.removePanel(sourceItemId) || + this.panels.find((panel) => panel.id === sourceItemId); if (!groupItem) { - throw new Error(`No panel with id ${itemId}`); + throw new Error(`No panel with id ${sourceItemId}`); } if (sourceGroup?.model.size === 0) { this.doRemoveGroup(sourceGroup); } - referenceGroup.model.openPanel(groupItem, { index }); + destinationGroup.model.openPanel(groupItem, { + index: destinationIndex, + }); } else { - const referenceLocation = getGridLocation(referenceGroup.element); + const referenceLocation = getGridLocation(destinationGroup.element); const targetLocation = getRelativeLocation( this.gridview.orientation, referenceLocation, - target + destinationTarget ); if (sourceGroup && sourceGroup.size < 2) { @@ -766,28 +772,28 @@ export class DockviewComponent // after deleting the group we need to re-evaulate the ref location const updatedReferenceLocation = getGridLocation( - referenceGroup.element + destinationGroup.element ); const location = getRelativeLocation( this.gridview.orientation, updatedReferenceLocation, - target + destinationTarget ); this.doAddGroup(targetGroup, location); } } else { const groupItem: IDockviewPanel | undefined = - sourceGroup?.model.removePanel(itemId) || - this.panels.find((panel) => panel.id === itemId); + sourceGroup?.model.removePanel(sourceItemId) || + this.panels.find((panel) => panel.id === sourceItemId); if (!groupItem) { - throw new Error(`No panel with id ${itemId}`); + throw new Error(`No panel with id ${sourceItemId}`); } const dropLocation = getRelativeLocation( this.gridview.orientation, referenceLocation, - target + destinationTarget ); const group = this.createGroupAtLocation(dropLocation); @@ -953,11 +959,11 @@ export class DockviewComponent } public dispose(): void { - super.dispose(); - this._onDidActivePanelChange.dispose(); this._onDidAddPanel.dispose(); this._onDidRemovePanel.dispose(); this._onDidLayoutFromJSON.dispose(); + + super.dispose(); } } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 5e9773a73..11bed8de9 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -282,12 +282,6 @@ export class DockviewGroupPanelModel this.locked = !!options.locked; this.addDisposables( - this._onMove, - this._onDidChange, - this._onDidDrop, - this._onDidAddPanel, - this._onDidRemovePanel, - this._onDidActivePanelChange, this.tabsContainer.onDrop((event) => { this.handleDropEvent(event.event, 'center', event.index); }), @@ -299,7 +293,13 @@ export class DockviewGroupPanelModel }), this.dropTarget.onDrop((event) => { this.handleDropEvent(event.nativeEvent, event.position); - }) + }), + this._onMove, + this._onDidChange, + this._onDidDrop, + this._onDidAddPanel, + this._onDidRemovePanel, + this._onDidActivePanelChange ); } diff --git a/packages/dockview-core/src/dockview/dockviewPanel.ts b/packages/dockview-core/src/dockview/dockviewPanel.ts index b13f8d5b6..01a8bd487 100644 --- a/packages/dockview-core/src/dockview/dockviewPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewPanel.ts @@ -3,14 +3,10 @@ import { DockviewPanelApi, DockviewPanelApiImpl, } from '../api/dockviewPanelApi'; -import { - GroupPanelUpdateEvent, - GroupviewPanelState, - IGroupPanelInitParameters, -} from './types'; +import { GroupviewPanelState, IGroupPanelInitParameters } from './types'; import { DockviewGroupPanel } from './dockviewGroupPanel'; import { CompositeDisposable, IDisposable } from '../lifecycle'; -import { IPanel, Parameters } from '../panel/types'; +import { IPanel, PanelUpdateEvent, Parameters } from '../panel/types'; import { IDockviewPanelModel } from './dockviewPanelModel'; import { IDockviewComponent } from './dockviewComponent'; @@ -23,7 +19,8 @@ export interface IDockviewPanel extends IDisposable, IPanel { updateParentGroup(group: DockviewGroupPanel, isGroupActive: boolean): void; init(params: IGroupPanelInitParameters): void; toJSON(): GroupviewPanelState; - update(event: GroupPanelUpdateEvent): void; + setTitle(title: string): void; + update(event: PanelUpdateEvent): void; } export class DockviewPanel @@ -34,13 +31,13 @@ export class DockviewPanel private _group: DockviewGroupPanel; private _params?: Parameters; - private _title: string; + private _title: string | undefined; get params(): Parameters | undefined { return this._params; } - get title(): string { + get title(): string | undefined { return this._title; } @@ -56,7 +53,6 @@ export class DockviewPanel readonly view: IDockviewPanelModel ) { super(); - this._title = ''; this._group = group; this.api = new DockviewPanelApiImpl(this, this._group); @@ -76,13 +72,13 @@ export class DockviewPanel public init(params: IGroupPanelInitParameters): void { this._params = params.params; - this.setTitle(params.title); - this.view.init({ ...params, api: this.api, containerApi: this.containerApi, }); + + this.setTitle(params.title); } focus(): void { @@ -103,12 +99,12 @@ export class DockviewPanel } setTitle(title: string): void { - const didTitleChange = title !== this._params?.title; + const didTitleChange = title !== this.title; if (didTitleChange) { this._title = title; - this.view?.update({ + this.view.update({ params: { params: this._params, title: this.title, @@ -118,20 +114,25 @@ export class DockviewPanel } } - public update(event: GroupPanelUpdateEvent): void { - const params = event.params as IGroupPanelInitParameters; - + public update(event: PanelUpdateEvent): void { + // merge the new parameters with the existing parameters this._params = { ...(this._params || {}), - ...event.params.params, + ...event.params, }; - if (params.title !== this.title) { - this._title = params.title; - this.api._onDidTitleChange.fire({ title: this.title }); + /** + * delete new keys that have a value of undefined, + * allow values of null + */ + for (const key of Object.keys(event.params)) { + if (event.params[key] === undefined) { + delete this._params[key]; + } } - this.view?.update({ + // update the view with the updated props + this.view.update({ params: { params: this._params, title: this.title, diff --git a/packages/dockview-core/src/dockview/dockviewPanelModel.ts b/packages/dockview-core/src/dockview/dockviewPanelModel.ts index b29bcd12c..44461f6a2 100644 --- a/packages/dockview-core/src/dockview/dockviewPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewPanelModel.ts @@ -3,19 +3,19 @@ import { GroupPanelPartInitParameters, IContentRenderer, ITabRenderer, - GroupPanelUpdateEvent, } from './types'; import { DockviewGroupPanel } from './dockviewGroupPanel'; import { IDisposable } from '../lifecycle'; import { createComponent } from '../panel/componentFactory'; import { IDockviewComponent } from './dockviewComponent'; +import { PanelUpdateEvent } from '../panel/types'; export interface IDockviewPanelModel extends IDisposable { readonly contentComponent: string; readonly tabComponent?: string; readonly content: IContentRenderer; readonly tab?: ITabRenderer; - update(event: GroupPanelUpdateEvent): void; + update(event: PanelUpdateEvent): void; layout(width: number, height: number): void; init(params: GroupPanelPartInitParameters): void; updateParentGroup(group: DockviewGroupPanel, isPanelVisible: boolean): void; @@ -80,7 +80,7 @@ export class DockviewPanelModel implements IDockviewPanelModel { this.content.layout?.(width, height); } - update(event: GroupPanelUpdateEvent): void { + update(event: PanelUpdateEvent): void { this.content.update?.(event); this.tab.update?.(event); } diff --git a/packages/dockview-core/src/dockview/types.ts b/packages/dockview-core/src/dockview/types.ts index 107da33ed..7d870746c 100644 --- a/packages/dockview-core/src/dockview/types.ts +++ b/packages/dockview-core/src/dockview/types.ts @@ -1,11 +1,6 @@ import { IDockviewComponent } from './dockviewComponent'; import { DockviewPanelApi } from '../api/dockviewPanelApi'; -import { - PanelInitParameters, - IPanel, - PanelUpdateEvent, - Parameters, -} from '../panel/types'; +import { PanelInitParameters, IPanel } from '../panel/types'; import { DockviewApi } from '../api/component.api'; import { Event } from '../events'; import { Optional } from '../types'; @@ -91,11 +86,6 @@ export interface IGroupPanelInitParameters // } -export type GroupPanelUpdateEvent = PanelUpdateEvent<{ - params?: Parameters; - title?: string; -}>; - export interface GroupviewPanelState { id: string; contentComponent?: string; diff --git a/packages/dockview-core/src/dom.ts b/packages/dockview-core/src/dom.ts index eb1a37c05..4a36f4bde 100644 --- a/packages/dockview-core/src/dom.ts +++ b/packages/dockview-core/src/dom.ts @@ -111,6 +111,8 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker { constructor(element: HTMLElement | Window) { super(); + this.addDisposables(this._onDidFocus, this._onDidBlur); + let hasFocus = isAncestor(document.activeElement, element); let loosingFocus = false; @@ -169,11 +171,4 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker { refreshState(): void { this._refreshStateHandler(); } - - public dispose(): void { - super.dispose(); - - this._onDidBlur.dispose(); - this._onDidFocus.dispose(); - } } diff --git a/packages/dockview-core/src/events.ts b/packages/dockview-core/src/events.ts index 7f6b07ebf..cb8d95930 100644 --- a/packages/dockview-core/src/events.ts +++ b/packages/dockview-core/src/events.ts @@ -24,24 +24,76 @@ export namespace Event { }; } -// dumb event emitter with better typings than nodes event emitter -// https://github.com/microsoft/vscode/blob/master/src/vs/base/common/event.ts +class LeakageMonitor { + readonly events = new Map, Stacktrace>(); + + get size(): number { + return this.events.size; + } + + add(event: Event, stacktrace: Stacktrace): void { + this.events.set(event, stacktrace); + } + + delete(event: Event): void { + this.events.delete(event); + } + + clear(): void { + this.events.clear(); + } +} + +class Stacktrace { + static create(): Stacktrace { + return new Stacktrace(new Error().stack ?? ''); + } + + private constructor(readonly value: string) {} + + print(): void { + console.warn(this.value); + } +} + +class Listener { + constructor( + readonly callback: (t: T) => void, + readonly stacktrace: Stacktrace | undefined + ) {} +} + +// relatively simple event emitter taken from https://github.com/microsoft/vscode/blob/master/src/vs/base/common/event.ts export class Emitter implements IDisposable { private _event?: Event; private _last?: T; - private _listeners: Array<(e: T) => any> = []; + private _listeners: Listener[] = []; private _disposed = false; + static ENABLE_TRACKING = false; + static readonly MEMORY_LEAK_WATCHER = new LeakageMonitor(); + + static setLeakageMonitorEnabled(isEnabled: boolean) { + if (isEnabled !== Emitter.ENABLE_TRACKING) { + Emitter.MEMORY_LEAK_WATCHER.clear(); + } + Emitter.ENABLE_TRACKING = isEnabled; + } + constructor(private readonly options?: EmitterOptions) {} get event(): Event { if (!this._event) { - this._event = (listener: (e: T) => void): IDisposable => { + this._event = (callback: (e: T) => void): IDisposable => { if (this.options?.replay && this._last !== undefined) { - listener(this._last); + callback(this._last); } + const listener = new Listener( + callback, + Emitter.ENABLE_TRACKING ? Stacktrace.create() : undefined + ); this._listeners.push(listener); return { @@ -49,10 +101,22 @@ export class Emitter implements IDisposable { const index = this._listeners.indexOf(listener); if (index > -1) { this._listeners.splice(index, 1); + } else if (Emitter.ENABLE_TRACKING) { + // console.warn( + // `Listener already disposed`, + // Stacktrace.create().print() + // ); } }, }; }; + + if (Emitter.ENABLE_TRACKING) { + Emitter.MEMORY_LEAK_WATCHER.add( + this._event, + Stacktrace.create() + ); + } } return this._event; } @@ -60,13 +124,31 @@ export class Emitter implements IDisposable { public fire(e: T): void { this._last = e; for (const listener of this._listeners) { - listener(e); + listener.callback(e); } } public dispose(): void { - this._listeners = []; - this._disposed = true; + if (!this._disposed) { + this._disposed = true; + + if (this._listeners.length > 0) { + if (Emitter.ENABLE_TRACKING) { + queueMicrotask(() => { + // don't check until stack of execution is completed to allow for out-of-order disposals within the same execution block + for (const listener of this._listeners) { + console.warn(listener.stacktrace?.print()); + } + }); + } + + this._listeners = []; + } + + if (Emitter.ENABLE_TRACKING && this._event) { + Emitter.MEMORY_LEAK_WATCHER.delete(this._event); + } + } } } @@ -80,7 +162,7 @@ export function addDisposableWindowListener( return { dispose: () => { - element.removeEventListener(type, listener); + element.removeEventListener(type, listener, options); }, }; } @@ -95,7 +177,7 @@ export function addDisposableListener( return { dispose: () => { - element.removeEventListener(type, listener); + element.removeEventListener(type, listener, options); }, }; } diff --git a/packages/dockview-core/src/gridview/baseComponentGridview.ts b/packages/dockview-core/src/gridview/baseComponentGridview.ts index 7f2c281df..93c6861eb 100644 --- a/packages/dockview-core/src/gridview/baseComponentGridview.ts +++ b/packages/dockview-core/src/gridview/baseComponentGridview.ts @@ -143,10 +143,7 @@ export abstract class BaseGrid this.addDisposables( this.gridview.onDidChange(() => { this._bufferOnDidLayoutChange.fire(); - }) - ); - - this.addDisposables( + }), Event.any( this.onDidAddGroup, this.onDidRemoveGroup, @@ -297,8 +294,6 @@ export abstract class BaseGrid } public dispose(): void { - super.dispose(); - this._onDidActiveGroupChange.dispose(); this._onDidAddGroup.dispose(); this._onDidRemoveGroup.dispose(); @@ -309,5 +304,7 @@ export abstract class BaseGrid } this.gridview.dispose(); + + super.dispose(); } } diff --git a/packages/dockview-core/src/gridview/basePanelView.ts b/packages/dockview-core/src/gridview/basePanelView.ts index 31c7b920c..19651d5aa 100644 --- a/packages/dockview-core/src/gridview/basePanelView.ts +++ b/packages/dockview-core/src/gridview/basePanelView.ts @@ -69,16 +69,17 @@ export abstract class BasePanelView this._element.style.width = '100%'; this._element.style.overflow = 'hidden'; - const { onDidFocus, onDidBlur } = trackFocus(this._element); + const focusTracker = trackFocus(this._element); this.addDisposables( this.api, - onDidFocus(() => { + focusTracker.onDidFocus(() => { this.api._onDidChangeFocus.fire({ isFocused: true }); }), - onDidBlur(() => { + focusTracker.onDidBlur(() => { this.api._onDidChangeFocus.fire({ isFocused: false }); - }) + }), + focusTracker ); } @@ -104,6 +105,7 @@ export abstract class BasePanelView } update(event: PanelUpdateEvent): void { + // merge the new parameters with the existing parameters this._params = { ...this._params, params: { @@ -111,6 +113,18 @@ export abstract class BasePanelView ...event.params, }, }; + + /** + * delete new keys that have a value of undefined, + * allow values of null + */ + for (const key of Object.keys(event.params)) { + if (event.params[key] === undefined) { + delete this._params.params[key]; + } + } + + // update the view with the updated props this.part?.update({ params: this._params.params }); } @@ -125,9 +139,9 @@ export abstract class BasePanelView } dispose(): void { - super.dispose(); - this.api.dispose(); this.part?.dispose(); + + super.dispose(); } } diff --git a/packages/dockview-core/src/gridview/branchNode.ts b/packages/dockview-core/src/gridview/branchNode.ts index c97e33243..6295b3f88 100644 --- a/packages/dockview-core/src/gridview/branchNode.ts +++ b/packages/dockview-core/src/gridview/branchNode.ts @@ -260,13 +260,13 @@ export class BranchNode extends CompositeDisposable implements IView { return this.splitview.getViewCachedVisibleSize(index); } - public removeChild(index: number, sizing?: Sizing): void { + public removeChild(index: number, sizing?: Sizing): Node { if (index < 0 || index >= this.children.length) { throw new Error('Invalid index'); } this.splitview.removeView(index, sizing); - this._removeChild(index); + return this._removeChild(index); } private _addChild(node: Node, index: number): void { @@ -296,9 +296,10 @@ export class BranchNode extends CompositeDisposable implements IView { } public dispose(): void { - super.dispose(); this._childrenDisposable.dispose(); - this.children.forEach((child) => child.dispose()); this.splitview.dispose(); + this.children.forEach((child) => child.dispose()); + + super.dispose(); } } diff --git a/packages/dockview-core/src/gridview/gridview.ts b/packages/dockview-core/src/gridview/gridview.ts index 37242cc33..62c2ca9fb 100644 --- a/packages/dockview-core/src/gridview/gridview.ts +++ b/packages/dockview-core/src/gridview/gridview.ts @@ -462,7 +462,8 @@ export class Gridview implements IDisposable { if (oldRoot.children.length === 1) { // can remove one level of redundant branching if there is only a single child const childReference = oldRoot.children[0]; - oldRoot.removeChild(0); // remove to prevent disposal when disposing of unwanted root + const child = oldRoot.removeChild(0); // remove to prevent disposal when disposing of unwanted root + child.dispose(); oldRoot.dispose(); this._root.addChild( @@ -632,7 +633,8 @@ export class Gridview implements IDisposable { newSiblingSize = Sizing.Invisible(newSiblingCachedVisibleSize); } - grandParent.removeChild(parentIndex); + const child = grandParent.removeChild(parentIndex); + child.dispose(); const newParent = new BranchNode( parent.orientation, @@ -676,60 +678,82 @@ export class Gridview implements IDisposable { throw new Error('Invalid location'); } - const node = parent.children[index]; + const nodeToRemove = parent.children[index]; - if (!(node instanceof LeafNode)) { + if (!(nodeToRemove instanceof LeafNode)) { throw new Error('Invalid location'); } parent.removeChild(index, sizing); + nodeToRemove.dispose(); - if (parent.children.length === 0) { - return node.view; + if (parent.children.length !== 1) { + return nodeToRemove.view; } - if (parent.children.length > 1) { - return node.view; - } + // if the parent has only one child and we know the parent is a BranchNode we can make the tree + // more efficiently spaced by replacing the parent BranchNode with the child. + // if that child is a LeafNode then we simply replace the BranchNode with the child otherwise if the child + // is a BranchNode too we should spread it's children into the grandparent. + // refer to the remaining child as the sibling const sibling = parent.children[0]; if (pathToParent.length === 0) { - // parent is root + // if the parent is root if (sibling instanceof LeafNode) { - return node.view; + // if the sibling is a leaf node no action is required + return nodeToRemove.view; } - // we must promote sibling to be the new root + // otherwise the sibling is a branch node. since the parent is the root and the root has only one child + // which is a branch node we can just set this branch node to be the new root node + + // for good housekeeping we'll removing the sibling from it's existing tree parent.removeChild(0, sizing); + + // and set that sibling node to be root this.root = sibling; - return node.view; + + return nodeToRemove.view; } + // otherwise the parent is apart of a large sub-tree + const [grandParent, ..._] = [...pathToParent].reverse(); const [parentIndex, ...__] = [...rest].reverse(); const isSiblingVisible = parent.isChildVisible(0); + + // either way we need to remove the sibling from it's existing tree parent.removeChild(0, sizing); + // note the sizes of all of the grandparents children const sizes = grandParent.children.map((_size, i) => grandParent.getChildSize(i) ); - grandParent.removeChild(parentIndex, sizing); + + // remove the parent from the grandparent since we are moving the sibling to take the parents place + // this parent is no longer used and can be disposed of + grandParent.removeChild(parentIndex, sizing).dispose(); if (sibling instanceof BranchNode) { + // replace the parent with the siblings children sizes.splice( parentIndex, 1, ...sibling.children.map((c) => c.size) ); + // and add those siblings to the grandparent for (let i = 0; i < sibling.children.length; i++) { const child = sibling.children[i]; grandParent.addChild(child, child.size, parentIndex + i); } } else { + // otherwise create a new leaf node and add that to the grandparent + const newSibling = new LeafNode( sibling.view, orthogonal(sibling.orientation), @@ -738,14 +762,19 @@ export class Gridview implements IDisposable { const siblingSizing = isSiblingVisible ? sibling.orthogonalSize : Sizing.Invisible(sibling.orthogonalSize); + grandParent.addChild(newSibling, siblingSizing, parentIndex); } + // the containing node of the sibling is no longer required and can be disposed of + sibling.dispose(); + + // resize everything for (let i = 0; i < sizes.length; i++) { grandParent.resizeChild(i, sizes[i]); } - return node.view; + return nodeToRemove.view; } public layout(width: number, height: number): void { diff --git a/packages/dockview-core/src/gridview/gridviewPanel.ts b/packages/dockview-core/src/gridview/gridviewPanel.ts index 530a3cc70..9562bb92b 100644 --- a/packages/dockview-core/src/gridview/gridviewPanel.ts +++ b/packages/dockview-core/src/gridview/gridviewPanel.ts @@ -154,7 +154,6 @@ export abstract class GridviewPanel this.api.initialize(this); // TODO: required to by-pass 'super before this' requirement this.addDisposables( - this._onDidChange, this.api.onVisibilityChange((event) => { const { isVisible } = event; const { accessor } = this._params as GridviewInitParameters; @@ -195,7 +194,8 @@ export abstract class GridviewPanel height: event.height, width: event.width, }); - }) + }), + this._onDidChange ); } diff --git a/packages/dockview-core/src/lifecycle.ts b/packages/dockview-core/src/lifecycle.ts index e756bc803..8262b28d1 100644 --- a/packages/dockview-core/src/lifecycle.ts +++ b/packages/dockview-core/src/lifecycle.ts @@ -16,7 +16,7 @@ export namespace Disposable { } export class CompositeDisposable { - private readonly disposables: IDisposable[]; + private readonly _disposables: IDisposable[]; private _isDisposed = false; protected get isDisposed(): boolean { @@ -28,15 +28,15 @@ export class CompositeDisposable { } constructor(...args: IDisposable[]) { - this.disposables = args; + this._disposables = args; } public addDisposables(...args: IDisposable[]): void { - args.forEach((arg) => this.disposables.push(arg)); + args.forEach((arg) => this._disposables.push(arg)); } public dispose(): void { - this.disposables.forEach((arg) => arg.dispose()); + this._disposables.forEach((arg) => arg.dispose()); this._isDisposed = true; } diff --git a/packages/dockview-core/src/splitview/splitview.ts b/packages/dockview-core/src/splitview/splitview.ts index 447529cfe..98dcce6a8 100644 --- a/packages/dockview-core/src/splitview/splitview.ts +++ b/packages/dockview-core/src/splitview/splitview.ts @@ -13,6 +13,7 @@ import { Event, Emitter } from '../events'; import { pushToStart, pushToEnd, firstIndex } from '../array'; import { range, clamp } from '../math'; import { ViewItem } from './viewItem'; +import { IDisposable } from '../lifecycle'; export enum Orientation { HORIZONTAL = 'HORIZONTAL', @@ -42,7 +43,7 @@ export enum LayoutPriority { Normal = 'normal', } -export interface IBaseView { +export interface IBaseView extends IDisposable { minimumSize: number; maximumSize: number; snap?: boolean; @@ -97,7 +98,7 @@ export class Splitview { private element: HTMLElement; private viewContainer: HTMLElement; private sashContainer: HTMLElement; - private views: ViewItem[] = []; + private viewItems: ViewItem[] = []; private sashes: ISashItem[] = []; private _orientation: Orientation; private _size = 0; @@ -132,7 +133,7 @@ export class Splitview { } public get length(): number { - return this.views.length; + return this.viewItems.length; } public get proportions(): number[] | undefined { @@ -159,13 +160,13 @@ export class Splitview { } get minimumSize(): number { - return this.views.reduce((r, item) => r + item.minimumSize, 0); + return this.viewItems.reduce((r, item) => r + item.minimumSize, 0); } get maximumSize(): number { return this.length === 0 ? Number.POSITIVE_INFINITY - : this.views.reduce((r, item) => r + item.maximumSize, 0); + : this.viewItems.reduce((r, item) => r + item.maximumSize, 0); } get startSnappingEnabled(): boolean { @@ -240,7 +241,7 @@ export class Splitview { }); // Initialize content size and proportions for first layout - this.contentSize = this.views.reduce((r, i) => r + i.size, 0); + this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); this.saveProportions(); } } @@ -261,22 +262,22 @@ export class Splitview { } isViewVisible(index: number): boolean { - if (index < 0 || index >= this.views.length) { + if (index < 0 || index >= this.viewItems.length) { throw new Error('Index out of bounds'); } - const viewItem = this.views[index]; + const viewItem = this.viewItems[index]; return viewItem.visible; } setViewVisible(index: number, visible: boolean): void { - if (index < 0 || index >= this.views.length) { + if (index < 0 || index >= this.viewItems.length) { throw new Error('Index out of bounds'); } toggleClass(this.container, 'visible', visible); - const viewItem = this.views[index]; + const viewItem = this.viewItems[index]; toggleClass(this.container, 'visible', visible); @@ -288,30 +289,30 @@ export class Splitview { } getViewSize(index: number): number { - if (index < 0 || index >= this.views.length) { + if (index < 0 || index >= this.viewItems.length) { return -1; } - return this.views[index].size; + return this.viewItems[index].size; } resizeView(index: number, size: number): void { - if (index < 0 || index >= this.views.length) { + if (index < 0 || index >= this.viewItems.length) { return; } - const indexes = range(this.views.length).filter((i) => i !== index); + const indexes = range(this.viewItems.length).filter((i) => i !== index); const lowPriorityIndexes = [ ...indexes.filter( - (i) => this.views[i].priority === LayoutPriority.Low + (i) => this.viewItems[i].priority === LayoutPriority.Low ), index, ]; const highPriorityIndexes = indexes.filter( - (i) => this.views[i].priority === LayoutPriority.High + (i) => this.viewItems[i].priority === LayoutPriority.High ); - const item = this.views[index]; + const item = this.viewItems[index]; size = Math.round(size); size = clamp( size, @@ -324,13 +325,13 @@ export class Splitview { } public getViews(): T[] { - return this.views.map((x) => x.view as T); + return this.viewItems.map((x) => x.view as T); } private onDidChange(item: ViewItem, size: number | undefined): void { - const index = this.views.indexOf(item); + const index = this.viewItems.indexOf(item); - if (index < 0 || index >= this.views.length) { + if (index < 0 || index >= this.viewItems.length) { return; } @@ -345,7 +346,7 @@ export class Splitview { public addView( view: IView, size: number | Sizing = { type: 'distribute' }, - index: number = this.views.length, + index: number = this.viewItems.length, skipLayout?: boolean ): void { const container = document.createElement('div'); @@ -369,14 +370,14 @@ export class Splitview { this.onDidChange(viewItem, newSize.size) ); - const dispose = () => { - disposable?.dispose(); - this.viewContainer.removeChild(container); - }; + const viewItem = new ViewItem(container, view, viewSize, { + dispose: () => { + disposable.dispose(); + this.viewContainer.removeChild(container); + }, + }); - const viewItem = new ViewItem(container, view, viewSize, { dispose }); - - if (index === this.views.length) { + if (index === this.viewItems.length) { this.viewContainer.appendChild(container); } else { this.viewContainer.insertBefore( @@ -385,15 +386,25 @@ export class Splitview { ); } - this.views.splice(index, 0, viewItem); + this.viewItems.splice(index, 0, viewItem); - if (this.views.length > 1) { + if (this.viewItems.length > 1) { //add sash const sash = document.createElement('div'); sash.className = 'sash'; - const onStart = (event: MouseEvent) => { - for (const item of this.views) { + const onTouchStart = (event: TouchEvent) => { + event.preventDefault(); + const touch = event.touches[0]; + onStart(touch); + }; + + const onMouseDown = (event: MouseEvent) => { + onStart(event); + }; + + const onStart = (event: { clientX: number; clientY: number }) => { + for (const item of this.viewItems) { item.enabled = false; } @@ -417,19 +428,20 @@ export class Splitview { ); // - const sizes = this.views.map((x) => x.size); + const sizes = this.viewItems.map((x) => x.size); // let snapBefore: ISashDragSnapState | undefined; let snapAfter: ISashDragSnapState | undefined; const upIndexes = range(sashIndex, -1); - const downIndexes = range(sashIndex + 1, this.views.length); + const downIndexes = range(sashIndex + 1, this.viewItems.length); const minDeltaUp = upIndexes.reduce( - (r, i) => r + (this.views[i].minimumSize - sizes[i]), + (r, i) => r + (this.viewItems[i].minimumSize - sizes[i]), 0 ); const maxDeltaUp = upIndexes.reduce( - (r, i) => r + (this.views[i].viewMaximumSize - sizes[i]), + (r, i) => + r + (this.viewItems[i].viewMaximumSize - sizes[i]), 0 ); const maxDeltaDown = @@ -437,7 +449,8 @@ export class Splitview { ? Number.POSITIVE_INFINITY : downIndexes.reduce( (r, i) => - r + (sizes[i] - this.views[i].minimumSize), + r + + (sizes[i] - this.viewItems[i].minimumSize), 0 ); const minDeltaDown = @@ -446,7 +459,8 @@ export class Splitview { : downIndexes.reduce( (r, i) => r + - (sizes[i] - this.views[i].viewMaximumSize), + (sizes[i] - + this.viewItems[i].viewMaximumSize), 0 ); const minDelta = Math.max(minDeltaUp, minDeltaDown); @@ -454,7 +468,7 @@ export class Splitview { const snapBeforeIndex = this.findFirstSnapIndex(upIndexes); const snapAfterIndex = this.findFirstSnapIndex(downIndexes); if (typeof snapBeforeIndex === 'number') { - const snappedViewItem = this.views[snapBeforeIndex]; + const snappedViewItem = this.viewItems[snapBeforeIndex]; const halfSize = Math.floor( snappedViewItem.viewMinimumSize / 2 ); @@ -469,7 +483,7 @@ export class Splitview { } if (typeof snapAfterIndex === 'number') { - const snappedViewItem = this.views[snapAfterIndex]; + const snappedViewItem = this.viewItems[snapAfterIndex]; const halfSize = Math.floor( snappedViewItem.viewMinimumSize / 2 ); @@ -482,13 +496,25 @@ export class Splitview { size: snappedViewItem.size, }; } - // - const mousemove = (mousemoveEvent: MouseEvent) => { + const onMouseMove = (event: MouseEvent) => { + reposition(event); + }; + + const onTouchMove = (event: TouchEvent) => { + event.preventDefault(); + const touch = event.touches[0]; + reposition(touch); + }; + + const reposition = (event: { + clientX: number; + clientY: number; + }) => { const current = this._orientation === Orientation.HORIZONTAL - ? mousemoveEvent.clientX - : mousemoveEvent.clientY; + ? event.clientX + : event.clientY; const delta = current - start; this.resize( @@ -507,7 +533,7 @@ export class Splitview { }; const end = () => { - for (const item of this.views) { + for (const item of this.viewItems) { item.enabled = true; } @@ -517,24 +543,30 @@ export class Splitview { this.saveProportions(); - document.removeEventListener('mousemove', mousemove); + document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', end); - document.removeEventListener('mouseend', end); + document.removeEventListener('touchmove', onTouchMove); + document.removeEventListener('touchend', end); + document.removeEventListener('touchcancel', end); this._onDidSashEnd.fire(undefined); }; - document.addEventListener('mousemove', mousemove); + document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', end); - document.addEventListener('mouseend', end); + document.addEventListener('touchmove', onTouchMove); + document.addEventListener('touchend', end); + document.addEventListener('touchcancel', end); }; - sash.addEventListener('mousedown', onStart); + sash.addEventListener('mousedown', onMouseDown); + sash.addEventListener('touchstart', onTouchStart); const sashItem: ISashItem = { container: sash, disposable: () => { sash.removeEventListener('mousedown', onStart); + sash.removeEventListener('touchstart', onTouchStart); this.sashContainer.removeChild(sash); }, }; @@ -562,7 +594,7 @@ export class Splitview { const flexibleViewItems: ViewItem[] = []; let flexibleSize = 0; - for (const item of this.views) { + for (const item of this.viewItems) { if (item.maximumSize - item.minimumSize > 0) { flexibleViewItems.push(item); flexibleSize += item.size; @@ -575,12 +607,12 @@ export class Splitview { item.size = clamp(size, item.minimumSize, item.maximumSize); } - const indexes = range(this.views.length); + const indexes = range(this.viewItems.length); const lowPriorityIndexes = indexes.filter( - (i) => this.views[i].priority === LayoutPriority.Low + (i) => this.viewItems[i].priority === LayoutPriority.Low ); const highPriorityIndexes = indexes.filter( - (i) => this.views[i].priority === LayoutPriority.High + (i) => this.viewItems[i].priority === LayoutPriority.High ); this.relayout(lowPriorityIndexes, highPriorityIndexes); @@ -592,11 +624,11 @@ export class Splitview { skipLayout = false ): IView { // Remove view - const viewItem = this.views.splice(index, 1)[0]; + const viewItem = this.viewItems.splice(index, 1)[0]; viewItem.dispose(); // Remove sash - if (this.views.length >= 1) { + if (this.viewItems.length >= 1) { const sashIndex = Math.max(index - 1, 0); const sashItem = this.sashes.splice(sashIndex, 1)[0]; sashItem.disposable(); @@ -616,11 +648,11 @@ export class Splitview { } getViewCachedVisibleSize(index: number): number | undefined { - if (index < 0 || index >= this.views.length) { + if (index < 0 || index >= this.viewItems.length) { throw new Error('Index out of bounds'); } - const viewItem = this.views[index]; + const viewItem = this.viewItems[index]; return viewItem.cachedVisibleSize; } @@ -640,24 +672,24 @@ export class Splitview { this.orthogonalSize = orthogonalSize; if (!this.proportions) { - const indexes = range(this.views.length); + const indexes = range(this.viewItems.length); const lowPriorityIndexes = indexes.filter( - (i) => this.views[i].priority === LayoutPriority.Low + (i) => this.viewItems[i].priority === LayoutPriority.Low ); const highPriorityIndexes = indexes.filter( - (i) => this.views[i].priority === LayoutPriority.High + (i) => this.viewItems[i].priority === LayoutPriority.High ); this.resize( - this.views.length - 1, + this.viewItems.length - 1, size - previousSize, undefined, lowPriorityIndexes, highPriorityIndexes ); } else { - for (let i = 0; i < this.views.length; i++) { - const item = this.views[i]; + for (let i = 0; i < this.viewItems.length; i++) { + const item = this.viewItems[i]; item.size = clamp( Math.round(this.proportions[i] * size), @@ -675,10 +707,10 @@ export class Splitview { lowPriorityIndexes?: number[], highPriorityIndexes?: number[] ): void { - const contentSize = this.views.reduce((r, i) => r + i.size, 0); + const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); this.resize( - this.views.length - 1, + this.viewItems.length - 1, this._size - contentSize, undefined, lowPriorityIndexes, @@ -690,15 +722,15 @@ export class Splitview { } private distributeEmptySpace(lowPriorityIndex?: number): void { - const contentSize = this.views.reduce((r, i) => r + i.size, 0); + const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); let emptyDelta = this.size - contentSize; - const indexes = range(this.views.length - 1, -1); + const indexes = range(this.viewItems.length - 1, -1); const lowPriorityIndexes = indexes.filter( - (i) => this.views[i].priority === LayoutPriority.Low + (i) => this.viewItems[i].priority === LayoutPriority.Low ); const highPriorityIndexes = indexes.filter( - (i) => this.views[i].priority === LayoutPriority.High + (i) => this.viewItems[i].priority === LayoutPriority.High ); for (const index of highPriorityIndexes) { @@ -714,7 +746,7 @@ export class Splitview { } for (let i = 0; emptyDelta !== 0 && i < indexes.length; i++) { - const item = this.views[indexes[i]]; + const item = this.viewItems[indexes[i]]; const size = clamp( item.size + emptyDelta, item.minimumSize, @@ -729,21 +761,21 @@ export class Splitview { private saveProportions(): void { if (this.proportionalLayout && this.contentSize > 0) { - this._proportions = this.views.map( + this._proportions = this.viewItems.map( (i) => i.size / this.contentSize ); } } private layoutViews(): void { - this.contentSize = this.views.reduce((r, i) => r + i.size, 0); + this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); let sum = 0; const x: number[] = []; this.updateSashEnablement(); - for (let i = 0; i < this.views.length - 1; i++) { - sum += this.views[i].size; + for (let i = 0; i < this.viewItems.length - 1; i++) { + sum += this.viewItems[i].size; x.push(sum); const offset = Math.min(Math.max(0, sum - 2), this.size - 4); @@ -757,7 +789,7 @@ export class Splitview { this.sashes[i].container.style.top = `${offset}px`; } } - this.views.forEach((view, i) => { + this.viewItems.forEach((view, i) => { if (this._orientation === Orientation.HORIZONTAL) { view.container.style.width = `${view.size}px`; view.container.style.left = i == 0 ? '0px' : `${x[i - 1]}px`; @@ -778,7 +810,7 @@ export class Splitview { private findFirstSnapIndex(indexes: number[]): number | undefined { // visible views first for (const index of indexes) { - const viewItem = this.views[index]; + const viewItem = this.viewItems[index]; if (!viewItem.visible) { continue; @@ -791,7 +823,7 @@ export class Splitview { // then, hidden views for (const index of indexes) { - const viewItem = this.views[index]; + const viewItem = this.viewItems[index]; if ( viewItem.visible && @@ -810,16 +842,16 @@ export class Splitview { private updateSashEnablement(): void { let previous = false; - const collapsesDown = this.views.map( + const collapsesDown = this.viewItems.map( (i) => (previous = i.size - i.minimumSize > 0 || previous) ); previous = false; - const expandsDown = this.views.map( + const expandsDown = this.viewItems.map( (i) => (previous = i.maximumSize - i.size > 0 || previous) ); - const reverseViews = [...this.views].reverse(); + const reverseViews = [...this.viewItems].reverse(); previous = false; const collapsesUp = reverseViews .map((i) => (previous = i.size - i.minimumSize > 0 || previous)) @@ -833,7 +865,7 @@ export class Splitview { let position = 0; for (let index = 0; index < this.sashes.length; index++) { const sash = this.sashes[index]; - const viewItem = this.views[index]; + const viewItem = this.viewItems[index]; position += viewItem.size; const min = !(collapsesDown[index] && expandsUp[index + 1]); @@ -841,16 +873,16 @@ export class Splitview { if (min && max) { const upIndexes = range(index, -1); - const downIndexes = range(index + 1, this.views.length); + const downIndexes = range(index + 1, this.viewItems.length); const snapBeforeIndex = this.findFirstSnapIndex(upIndexes); const snapAfterIndex = this.findFirstSnapIndex(downIndexes); const snappedBefore = typeof snapBeforeIndex === 'number' && - !this.views[snapBeforeIndex].visible; + !this.viewItems[snapBeforeIndex].visible; const snappedAfter = typeof snapAfterIndex === 'number' && - !this.views[snapAfterIndex].visible; + !this.viewItems[snapAfterIndex].visible; if ( snappedBefore && @@ -887,7 +919,7 @@ export class Splitview { private resize = ( index: number, delta: number, - sizes: number[] = this.views.map((x) => x.size), + sizes: number[] = this.viewItems.map((x) => x.size), lowPriorityIndexes?: number[], highPriorityIndexes?: number[], overloadMinDelta: number = Number.NEGATIVE_INFINITY, @@ -895,12 +927,12 @@ export class Splitview { snapBefore?: ISashDragSnapState, snapAfter?: ISashDragSnapState ): number => { - if (index < 0 || index > this.views.length) { + if (index < 0 || index > this.viewItems.length) { return 0; } const upIndexes = range(index, -1); - const downIndexes = range(index + 1, this.views.length); + const downIndexes = range(index + 1, this.viewItems.length); // if (highPriorityIndexes) { for (const i of highPriorityIndexes) { @@ -916,18 +948,18 @@ export class Splitview { } } // - const upItems = upIndexes.map((i) => this.views[i]); + const upItems = upIndexes.map((i) => this.viewItems[i]); const upSizes = upIndexes.map((i) => sizes[i]); // - const downItems = downIndexes.map((i) => this.views[i]); + const downItems = downIndexes.map((i) => this.viewItems[i]); const downSizes = downIndexes.map((i) => sizes[i]); // const minDeltaUp = upIndexes.reduce( - (_, i) => _ + this.views[i].minimumSize - sizes[i], + (_, i) => _ + this.viewItems[i].minimumSize - sizes[i], 0 ); const maxDeltaUp = upIndexes.reduce( - (_, i) => _ + this.views[i].maximumSize - sizes[i], + (_, i) => _ + this.viewItems[i].maximumSize - sizes[i], 0 ); // @@ -935,7 +967,7 @@ export class Splitview { downIndexes.length === 0 ? Number.POSITIVE_INFINITY : downIndexes.reduce( - (_, i) => _ + sizes[i] - this.views[i].minimumSize, + (_, i) => _ + sizes[i] - this.viewItems[i].minimumSize, 0 ); @@ -943,7 +975,7 @@ export class Splitview { downIndexes.length === 0 ? Number.NEGATIVE_INFINITY : downIndexes.reduce( - (_, i) => _ + sizes[i] - this.views[i].maximumSize, + (_, i) => _ + sizes[i] - this.viewItems[i].maximumSize, 0 ); // @@ -952,14 +984,14 @@ export class Splitview { // let snapped = false; if (snapBefore) { - const snapView = this.views[snapBefore.index]; + const snapView = this.viewItems[snapBefore.index]; const visible = delta >= snapBefore.limitDelta; snapped = visible !== snapView.visible; snapView.setVisible(visible, snapBefore.size); } if (!snapped && snapAfter) { - const snapView = this.views[snapAfter.index]; + const snapView = this.viewItems[snapAfter.index]; const visible = delta < snapAfter.limitDelta; snapped = visible !== snapView.visible; snapView.setVisible(visible, snapAfter.size); @@ -1047,6 +1079,10 @@ export class Splitview { } } + for (const viewItem of this.viewItems) { + viewItem.dispose(); + } + this.element.remove(); } } diff --git a/packages/dockview-core/src/splitview/splitviewComponent.ts b/packages/dockview-core/src/splitview/splitviewComponent.ts index f4f0bee7c..37573c40e 100644 --- a/packages/dockview-core/src/splitview/splitviewComponent.ts +++ b/packages/dockview-core/src/splitview/splitviewComponent.ts @@ -1,7 +1,6 @@ import { CompositeDisposable, IDisposable, - IValueDisposable, MutableDisposable, } from '../lifecycle'; import { @@ -83,10 +82,10 @@ export class SplitviewComponent extends Resizable implements ISplitviewComponent { - private _disposable = new MutableDisposable(); + private _splitviewChangeDisposable = new MutableDisposable(); private _splitview!: Splitview; private _activePanel: SplitviewPanel | undefined; - private _panels = new Map>(); + private _panels = new Map(); private _options: SplitviewComponentOptions; private readonly _onDidLayoutfromJSON = new Emitter(); @@ -124,7 +123,7 @@ export class SplitviewComponent set splitview(value: Splitview) { this._splitview = value; - this._disposable.value = new CompositeDisposable( + this._splitviewChangeDisposable.value = new CompositeDisposable( this._splitview.onDidSashEnd(() => { this._onDidLayoutChange.fire(undefined); }), @@ -170,7 +169,6 @@ export class SplitviewComponent this.splitview = new Splitview(this.element, options); this.addDisposables( - this._disposable, this._onDidAddView, this._onDidLayoutfromJSON, this._onDidRemoveView, @@ -226,19 +224,19 @@ export class SplitviewComponent } removePanel(panel: SplitviewPanel, sizing?: Sizing): void { - const disposable = this._panels.get(panel.id); + const item = this._panels.get(panel.id); - if (!disposable) { + if (!item) { throw new Error(`unknown splitview panel ${panel.id}`); } - disposable.disposable.dispose(); - disposable.value.dispose(); + item.dispose(); this._panels.delete(panel.id); const index = this.panels.findIndex((_) => _ === panel); - this.splitview.removeView(index, sizing); + const removedView = this.splitview.removeView(index, sizing); + removedView.dispose(); const panels = this.panels; if (panels.length > 0) { @@ -250,7 +248,7 @@ export class SplitviewComponent return this.panels.find((view) => view.id === id); } - addPanel(options: AddSplitviewComponentOptions): ISplitviewPanel { + addPanel(options: AddSplitviewComponentOptions): SplitviewPanel { if (this._panels.has(options.id)) { throw new Error(`panel ${options.id} already exists`); } @@ -308,7 +306,7 @@ export class SplitviewComponent this.setActive(view, true); }); - this._panels.set(view.id, { disposable, value: view }); + this._panels.set(view.id, disposable); } toJSON(): SerializedSplitview { @@ -404,23 +402,34 @@ export class SplitviewComponent } clear(): void { - for (const [_, value] of this._panels.entries()) { - value.disposable.dispose(); - value.value.dispose(); + for (const disposable of this._panels.values()) { + disposable.dispose(); } + this._panels.clear(); - this.splitview.dispose(); + + while (this.splitview.length > 0) { + const view = this.splitview.removeView(0, Sizing.Distribute, true); + view.dispose(); + } } dispose(): void { - for (const [_, value] of this._panels.entries()) { - value.disposable.dispose(); - value.value.dispose(); + for (const disposable of this._panels.values()) { + disposable.dispose(); } + this._panels.clear(); + const views = this.splitview.getViews(); + + this._splitviewChangeDisposable.dispose(); this.splitview.dispose(); + for (const view of views) { + view.dispose(); + } + super.dispose(); } } diff --git a/packages/dockview-core/src/splitview/splitviewPanel.ts b/packages/dockview-core/src/splitview/splitviewPanel.ts index 4782e8c30..d0ac1c41c 100644 --- a/packages/dockview-core/src/splitview/splitviewPanel.ts +++ b/packages/dockview-core/src/splitview/splitviewPanel.ts @@ -7,6 +7,7 @@ import { SplitviewPanelApiImpl } from '../api/splitviewPanelApi'; import { LayoutPriority, Orientation } from './splitview'; import { FunctionOrValue } from '../types'; import { Emitter, Event } from '../events'; +import { CompositeDisposable } from '../lifecycle'; export interface ISplitviewPanel extends BasePanelViewExported { diff --git a/packages/dockview-core/typedoc.json b/packages/dockview-core/typedoc.json index 51c05e659..952d2032e 100644 --- a/packages/dockview-core/typedoc.json +++ b/packages/dockview-core/typedoc.json @@ -1,7 +1,4 @@ { - "out": "typedocs", - "entryPoints": ["./src/index.ts"], - "exclude": ["**/_test/**/*.*", "**/index.ts"], - "excludeExternals": true, - "excludePrivate": true + "extends": ["../../typedoc.base.json"], + "entryPoints": ["src/index.ts"] } diff --git a/packages/dockview/jest.config.ts b/packages/dockview/jest.config.ts index b909c68ef..5d952690f 100644 --- a/packages/dockview/jest.config.ts +++ b/packages/dockview/jest.config.ts @@ -15,6 +15,7 @@ const config: JestConfigWithTsJest = { setupFiles: [ '/packages/dockview/src/__tests__/__mocks__/resizeObserver.js', ], + setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], coveragePathIgnorePatterns: ['/node_modules/'], modulePathIgnorePatterns: [ '/packages/dockview/src/__tests__/__mocks__', diff --git a/packages/dockview/package.json b/packages/dockview/package.json index 5f538135d..8232edcc7 100644 --- a/packages/dockview/package.json +++ b/packages/dockview/package.json @@ -1,6 +1,6 @@ { "name": "dockview", - "version": "1.7.2", + "version": "1.7.5", "description": "Zero dependency layout manager supporting tabs, grids and splitviews with ReactJS support", "main": "./dist/cjs/index.js", "types": "./dist/cjs/index.d.ts", @@ -56,7 +56,7 @@ "author": "https://github.com/mathuo", "license": "MIT", "dependencies": { - "dockview-core": "^1.7.2" + "dockview-core": "^1.7.5" }, "devDependencies": { "@rollup/plugin-node-resolve": "^15.0.1", @@ -71,7 +71,6 @@ "react-dom": "^18.2.0", "rimraf": "^4.1.2", "rollup": "^3.15.0", - "rollup-plugin-postcss": "^4.0.2", - "typedoc": "^0.23.25" + "rollup-plugin-postcss": "^4.0.2" } } diff --git a/packages/dockview/src/__tests__/dockview/dockview.spec.tsx b/packages/dockview/src/__tests__/dockview/dockview.spec.tsx index 6fe0872a7..72e1feec5 100644 --- a/packages/dockview/src/__tests__/dockview/dockview.spec.tsx +++ b/packages/dockview/src/__tests__/dockview/dockview.spec.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { render } from '@testing-library/react'; -import { DockviewApi } from 'dockview-core'; +import { act, render, waitFor } from '@testing-library/react'; +import { DockviewApi, IDockviewPanel } from 'dockview-core'; import { IDockviewPanelProps, DockviewReact, @@ -15,7 +15,17 @@ describe('gridview react', () => { beforeEach(() => { components = { default: (props: IDockviewPanelProps) => { - return
hello world
; + return ( +
+ {Object.keys(props.params).map((key) => { + return ( +
{`key=${key},value=${props.params[key]}`}
+ ); + })} +
+ ); }, }; }); @@ -51,4 +61,84 @@ describe('gridview react', () => { expect(api!.width).toBe(650); expect(api!.height).toBe(450); }); + + test('that the component can update parameters', async () => { + let api: DockviewApi; + + const onReady = (event: DockviewReadyEvent) => { + api = event.api; + }; + + const wrapper = render( + + ); + + let panel: IDockviewPanel; + + act(() => { + panel = api!.addPanel({ + id: 'panel_1', + component: 'default', + params: { + keyA: 'valueA', + keyB: 'valueB', + }, + }); + }); + + await waitFor(() => { + expect( + wrapper.queryByText(/key=keyA,value=valueA/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyB,value=valueB/i) + ).toBeInTheDocument(); + }); + + act(() => { + panel.api.updateParameters({ keyA: 'valueAA', keyC: 'valueC' }); + }); + + await waitFor(() => { + expect( + wrapper.queryByText(/key=keyA,value=valueAA/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyB,value=valueB/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyC,value=valueC/i) + ).toBeInTheDocument(); + }); + + act(() => { + panel.api.updateParameters({ keyC: null }); + }); + + await waitFor(() => { + expect( + wrapper.queryByText(/key=keyA,value=valueAA/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyB,value=valueB/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyC,value=null/i) + ).toBeInTheDocument(); + }); + + act(() => { + panel.api.updateParameters({ keyA: undefined }); + }); + + await waitFor(() => { + expect(wrapper.queryByText(/key=keyA/i)).not.toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyB,value=valueB/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyC,value=null/i) + ).toBeInTheDocument(); + }); + }); }); diff --git a/packages/dockview/src/__tests__/gridview/gridview.spec.tsx b/packages/dockview/src/__tests__/gridview/gridview.spec.tsx index 0881e00b4..7fdce204c 100644 --- a/packages/dockview/src/__tests__/gridview/gridview.spec.tsx +++ b/packages/dockview/src/__tests__/gridview/gridview.spec.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { render } from '@testing-library/react'; -import { GridviewApi, Orientation } from 'dockview-core'; +import { act, render, waitFor } from '@testing-library/react'; +import { GridviewApi, IGridviewPanel, Orientation } from 'dockview-core'; import { IGridviewPanelProps, GridviewReact, @@ -15,7 +15,17 @@ describe('gridview react', () => { beforeEach(() => { components = { default: (props: IGridviewPanelProps) => { - return
hello world
; + return ( +
+ {Object.keys(props.params).map((key) => { + return ( +
{`key=${key},value=${props.params[key]}`}
+ ); + })} +
+ ); }, }; }); @@ -62,4 +72,88 @@ describe('gridview react', () => { expect(api!.width).toBe(650); expect(api!.height).toBe(450); }); + + test('that the component can update parameters', async () => { + let api: GridviewApi; + + const onReady = (event: GridviewReadyEvent) => { + api = event.api; + }; + + const wrapper = render( + + ); + + let panel: IGridviewPanel; + + act(() => { + panel = api!.addPanel({ + id: 'panel_1', + component: 'default', + params: { + keyA: 'valueA', + keyB: 'valueB', + }, + }); + }); + + await waitFor(() => { + expect( + wrapper.queryByText(/key=keyA,value=valueA/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyB,value=valueB/i) + ).toBeInTheDocument(); + }); + + act(() => { + panel.api.updateParameters({ keyA: 'valueAA', keyC: 'valueC' }); + }); + + await waitFor(() => { + expect( + wrapper.queryByText(/key=keyA,value=valueAA/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyB,value=valueB/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyC,value=valueC/i) + ).toBeInTheDocument(); + }); + + act(() => { + panel.api.updateParameters({ keyC: null }); + }); + + await waitFor(() => { + expect( + wrapper.queryByText(/key=keyA,value=valueAA/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyB,value=valueB/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyC,value=null/i) + ).toBeInTheDocument(); + }); + + act(() => { + panel.api.updateParameters({ keyA: undefined }); + }); + + await waitFor(() => { + expect(wrapper.queryByText(/key=keyA/i)).not.toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyB,value=valueB/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyC,value=null/i) + ).toBeInTheDocument(); + }); + }); }); diff --git a/packages/dockview/src/__tests__/paneview/paneview.spec.tsx b/packages/dockview/src/__tests__/paneview/paneview.spec.tsx index 830861e89..53be8476a 100644 --- a/packages/dockview/src/__tests__/paneview/paneview.spec.tsx +++ b/packages/dockview/src/__tests__/paneview/paneview.spec.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { render } from '@testing-library/react'; -import { PaneviewApi } from 'dockview-core'; +import { act, render, waitFor } from '@testing-library/react'; +import { IPaneviewPanel, PaneviewApi } from 'dockview-core'; import { IPaneviewPanelProps, PaneviewReact, @@ -15,7 +15,17 @@ describe('gridview react', () => { beforeEach(() => { components = { default: (props: IPaneviewPanelProps) => { - return
hello world
; + return ( +
+ {Object.keys(props.params).map((key) => { + return ( +
{`key=${key},value=${props.params[key]}`}
+ ); + })} +
+ ); }, }; }); @@ -49,4 +59,85 @@ describe('gridview react', () => { expect(api!.width).toBe(650); expect(api!.height).toBe(450); }); + + test('that the component can update parameters', async () => { + let api: PaneviewApi; + + const onReady = (event: PaneviewReadyEvent) => { + api = event.api; + }; + + const wrapper = render( + + ); + + let panel: IPaneviewPanel; + + act(() => { + panel = api!.addPanel({ + id: 'panel_1', + component: 'default', + title: 'Panel 1', + params: { + keyA: 'valueA', + keyB: 'valueB', + }, + }); + }); + + await waitFor(() => { + expect( + wrapper.queryByText(/key=keyA,value=valueA/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyB,value=valueB/i) + ).toBeInTheDocument(); + }); + + act(() => { + panel.api.updateParameters({ keyA: 'valueAA', keyC: 'valueC' }); + }); + + await waitFor(() => { + expect( + wrapper.queryByText(/key=keyA,value=valueAA/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyB,value=valueB/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyC,value=valueC/i) + ).toBeInTheDocument(); + }); + + act(() => { + panel.api.updateParameters({ keyC: null }); + }); + + await waitFor(() => { + expect( + wrapper.queryByText(/key=keyA,value=valueAA/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyB,value=valueB/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyC,value=null/i) + ).toBeInTheDocument(); + }); + + act(() => { + panel.api.updateParameters({ keyA: undefined }); + }); + + await waitFor(() => { + expect(wrapper.queryByText(/key=keyA/i)).not.toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyB,value=valueB/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyC,value=null/i) + ).toBeInTheDocument(); + }); + }); }); diff --git a/packages/dockview/src/__tests__/react/dockview/dockview.spec.tsx b/packages/dockview/src/__tests__/react/dockview/dockview.spec.tsx deleted file mode 100644 index 12048cac5..000000000 --- a/packages/dockview/src/__tests__/react/dockview/dockview.spec.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as React from 'react'; -import { render } from '@testing-library/react'; -import { DockviewApi } from 'dockview-core'; -import { - IDockviewPanelProps, - DockviewReact, - DockviewReadyEvent, -} from '../../../dockview/dockview'; -import { PanelCollection } from '../../../types'; -import { setMockRefElement } from '../../__test_utils__/utils'; - -describe('dockview', () => { - let components: PanelCollection; - - beforeEach(() => { - components = { - default: (props: IDockviewPanelProps) => { - return
hello world
; - }, - }; - }); - - test('default', () => { - let api: DockviewApi | undefined; - - const onReady = (event: DockviewReadyEvent) => { - api = event.api; - }; - - render(); - - expect(api).toBeTruthy(); - }); - - test('is sized to container', () => { - const el = document.createElement('div') as any; - jest.spyOn(el, 'clientHeight', 'get').mockReturnValue(450); - jest.spyOn(el, 'clientWidth', 'get').mockReturnValue(650); - - setMockRefElement(el); - - let api: DockviewApi | undefined; - - const onReady = (event: DockviewReadyEvent) => { - api = event.api; - }; - - render(); - - expect(api!.width).toBe(650); - expect(api!.height).toBe(450); - }); -}); diff --git a/packages/dockview/src/__tests__/react/dockview/groupControlsRenderer.spec.ts b/packages/dockview/src/__tests__/react/dockview/groupControlsRenderer.spec.ts deleted file mode 100644 index d3a056372..000000000 --- a/packages/dockview/src/__tests__/react/dockview/groupControlsRenderer.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - DockviewGroupPanel, - DockviewGroupPanelApi, - DockviewGroupPanelModel, -} from 'dockview-core'; -import { ReactGroupControlsRendererPart } from '../../../dockview/groupControlsRenderer'; - -describe('groupControlsRenderer', () => { - test('#1', () => { - const groupviewMock = jest.fn, []>( - () => { - return { - onDidAddPanel: jest.fn(), - onDidRemovePanel: jest.fn(), - onDidActivePanelChange: jest.fn(), - }; - } - ); - - const groupview = new groupviewMock() as DockviewGroupPanelModel; - - const groupPanelMock = jest.fn, []>(() => { - return { - api: {} as DockviewGroupPanelApi as any, - model: groupview, - }; - }); - - const groupPanel = new groupPanelMock() as DockviewGroupPanel; - - const cut = new ReactGroupControlsRendererPart( - jest.fn(), - { - addPortal: jest.fn(), - }, - groupPanel - ); - - expect(cut.element.childNodes.length).toBe(0); - expect(cut.element.className).toBe('dockview-react-part'); - expect(cut.part).toBeUndefined(); - - cut.init({ - containerApi: jest.fn(), - api: { - onDidActiveChange: jest.fn(), - }, - }); - - const update = jest.fn(); - - jest.spyOn(cut.part!, 'update').mockImplementation(update); - - cut.update({ params: { valueA: 'A' } }); - - expect(update).toBeCalledWith({ valueA: 'A' }); - }); -}); diff --git a/packages/dockview/src/__tests__/react/gridview/gridview.spec.tsx b/packages/dockview/src/__tests__/react/gridview/gridview.spec.tsx deleted file mode 100644 index 5e5ff70e9..000000000 --- a/packages/dockview/src/__tests__/react/gridview/gridview.spec.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import * as React from 'react'; -import { render } from '@testing-library/react'; -import { GridviewApi, Orientation } from 'dockview-core'; -import { - IGridviewPanelProps, - GridviewReact, - GridviewReadyEvent, -} from '../../../gridview/gridview'; -import { PanelCollection } from '../../../types'; -import { setMockRefElement } from '../../__test_utils__/utils'; - -describe('gridview react', () => { - let components: PanelCollection; - - beforeEach(() => { - components = { - default: (props: IGridviewPanelProps) => { - return
hello world
; - }, - }; - }); - - test('default', () => { - let api: GridviewApi | undefined; - - const onReady = (event: GridviewReadyEvent) => { - api = event.api; - }; - - render( - - ); - - expect(api).toBeTruthy(); - }); - - test('is sized to container', () => { - setMockRefElement({ - clientHeight: 450, - clientWidth: 650, - appendChild: jest.fn(), - }); - let api: GridviewApi | undefined; - - const onReady = (event: GridviewReadyEvent) => { - api = event.api; - }; - - render( - - ); - - expect(api!.width).toBe(650); - expect(api!.height).toBe(450); - }); -}); diff --git a/packages/dockview/src/__tests__/react/paneview/paneview.spec.tsx b/packages/dockview/src/__tests__/react/paneview/paneview.spec.tsx deleted file mode 100644 index 4dc8b8198..000000000 --- a/packages/dockview/src/__tests__/react/paneview/paneview.spec.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import * as React from 'react'; -import { render } from '@testing-library/react'; -import { PaneviewApi } from 'dockview-core'; -import { - IPaneviewPanelProps, - PaneviewReact, - PaneviewReadyEvent, -} from '../../../paneview/paneview'; -import { PanelCollection } from '../../../types'; -import { setMockRefElement } from '../../__test_utils__/utils'; - -describe('gridview react', () => { - let components: PanelCollection; - - beforeEach(() => { - components = { - default: (props: IPaneviewPanelProps) => { - return
hello world
; - }, - }; - }); - - test('default', () => { - let api: PaneviewApi | undefined; - - const onReady = (event: PaneviewReadyEvent) => { - api = event.api; - }; - - render(); - - expect(api).toBeTruthy(); - }); - - test('is sized to container', () => { - setMockRefElement({ - clientHeight: 450, - clientWidth: 650, - appendChild: jest.fn(), - }); - let api: PaneviewApi | undefined; - - const onReady = (event: PaneviewReadyEvent) => { - api = event.api; - }; - - render(); - - expect(api!.width).toBe(650); - expect(api!.height).toBe(450); - }); -}); diff --git a/packages/dockview/src/__tests__/react/react.spec.tsx b/packages/dockview/src/__tests__/react/react.spec.tsx deleted file mode 100644 index b89d09d74..000000000 --- a/packages/dockview/src/__tests__/react/react.spec.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { ReactPart } from '../../react'; -import * as React from 'react'; -import { render, screen, act } from '@testing-library/react'; - -interface TestInterface { - valueA: string; - valueB: number; -} - -describe('react', () => { - describe('ReactPart', () => { - test('update underlying component via ReactPart class', () => { - let api: ReactPart; - - const onReady = (_api: ReactPart) => { - api = _api; - }; - - render(); - - expect(api!).toBeTruthy(); - - expect(screen.getByTestId('valueA').textContent).toBe('stringA'); - expect(screen.getByTestId('valueB').textContent).toBe('42'); - - act(() => { - api.update({ valueB: '32' }); - }); - - expect(screen.getByTestId('valueA').textContent).toBe('stringA'); - expect(screen.getByTestId('valueB').textContent).toBe('32'); - - act(() => { - api.update({ valueA: 'anotherStringA', valueB: '22' }); - }); - - expect(screen.getByTestId('valueA').textContent).toBe( - 'anotherStringA' - ); - expect(screen.getByTestId('valueB').textContent).toBe('22'); - }); - }); -}); - -const Component = (props: TestInterface) => { - return ( -
-
{props.valueA}
-
{props.valueB}
-
- ); -}; - -const TestWrapper = (props: { - component: React.FunctionComponent; - onReady: (api: ReactPart) => void; -}) => { - const [portal, setPortal] = React.useState([]); - const ref = React.useRef(null); - - React.useEffect(() => { - const cut = new ReactPart( - ref.current!, - { - addPortal: (portal: React.ReactPortal) => { - setPortal((_) => [..._, portal]); - - return { - dispose: () => { - setPortal((_) => _.filter((_) => _ !== portal)); - }, - }; - }, - }, - props.component, - { - valueA: 'stringA', - valueB: 42, - } - ); - - props.onReady(cut); - - return () => { - cut.dispose(); - }; - }, []); - - return
{portal}
; -}; diff --git a/packages/dockview/src/__tests__/react/splitview/splitview.spec.tsx b/packages/dockview/src/__tests__/react/splitview/splitview.spec.tsx deleted file mode 100644 index 9ffc01974..000000000 --- a/packages/dockview/src/__tests__/react/splitview/splitview.spec.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import * as React from 'react'; -import { render } from '@testing-library/react'; -import { SplitviewApi, Orientation } from 'dockview-core'; -import { - ISplitviewPanelProps, - SplitviewReact, - SplitviewReadyEvent, -} from '../../../splitview/splitview'; -import { PanelCollection } from '../../../types'; -import { setMockRefElement } from '../../__test_utils__/utils'; - -describe('splitview react', () => { - let components: PanelCollection; - - beforeEach(() => { - components = { - default: (props: ISplitviewPanelProps) => { - return
hello world
; - }, - }; - }); - - test('default', () => { - let api: SplitviewApi | undefined; - - const onReady = (event: SplitviewReadyEvent) => { - api = event.api; - }; - - render( - - ); - - expect(api).toBeTruthy(); - }); - - test('is sized to container', () => { - setMockRefElement({ - clientHeight: 450, - clientWidth: 650, - appendChild: jest.fn(), - }); - let api: SplitviewApi | undefined; - - const onReady = (event: SplitviewReadyEvent) => { - api = event.api; - }; - - render( - - ); - - expect(api!.width).toBe(650); - expect(api!.height).toBe(450); - }); -}); diff --git a/packages/dockview/src/__tests__/splitview/splitview.spec.tsx b/packages/dockview/src/__tests__/splitview/splitview.spec.tsx index 34e0027db..e85959656 100644 --- a/packages/dockview/src/__tests__/splitview/splitview.spec.tsx +++ b/packages/dockview/src/__tests__/splitview/splitview.spec.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { render } from '@testing-library/react'; -import { SplitviewApi, Orientation } from 'dockview-core'; +import { act, render, waitFor } from '@testing-library/react'; +import { SplitviewApi, Orientation, ISplitviewPanel } from 'dockview-core'; import { ISplitviewPanelProps, SplitviewReact, @@ -15,7 +15,17 @@ describe('splitview react', () => { beforeEach(() => { components = { default: (props: ISplitviewPanelProps) => { - return
hello world
; + return ( +
+ {Object.keys(props.params).map((key) => { + return ( +
{`key=${key},value=${props.params[key]}`}
+ ); + })} +
+ ); }, }; }); @@ -61,4 +71,88 @@ describe('splitview react', () => { expect(api!.width).toBe(650); expect(api!.height).toBe(450); }); + + test('that the component can update parameters', async () => { + let api: SplitviewApi; + + const onReady = (event: SplitviewReadyEvent) => { + api = event.api; + }; + + const wrapper = render( + + ); + + let panel: ISplitviewPanel; + + act(() => { + panel = api!.addPanel({ + id: 'panel_1', + component: 'default', + params: { + keyA: 'valueA', + keyB: 'valueB', + }, + }); + }); + + await waitFor(() => { + expect( + wrapper.queryByText(/key=keyA,value=valueA/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyB,value=valueB/i) + ).toBeInTheDocument(); + }); + + act(() => { + panel.api.updateParameters({ keyA: 'valueAA', keyC: 'valueC' }); + }); + + await waitFor(() => { + expect( + wrapper.queryByText(/key=keyA,value=valueAA/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyB,value=valueB/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyC,value=valueC/i) + ).toBeInTheDocument(); + }); + + act(() => { + panel.api.updateParameters({ keyC: null }); + }); + + await waitFor(() => { + expect( + wrapper.queryByText(/key=keyA,value=valueAA/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyB,value=valueB/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyC,value=null/i) + ).toBeInTheDocument(); + }); + + act(() => { + panel.api.updateParameters({ keyA: undefined }); + }); + + await waitFor(() => { + expect(wrapper.queryByText(/key=keyA/i)).not.toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyB,value=valueB/i) + ).toBeInTheDocument(); + expect( + wrapper.queryByText(/key=keyC,value=null/i) + ).toBeInTheDocument(); + }); + }); }); diff --git a/packages/dockview/tsconfig.json b/packages/dockview/tsconfig.json index f1a6f570c..023097d12 100644 --- a/packages/dockview/tsconfig.json +++ b/packages/dockview/tsconfig.json @@ -6,9 +6,6 @@ "jsx": "react", "rootDir": "src" }, - "paths": { - "dockview-core": "../dockview-core" - }, "include": ["src"], "exclude": ["**/node_modules", "src/__tests__"] } diff --git a/packages/dockview/typedoc.json b/packages/dockview/typedoc.json index 51c05e659..b97dde02c 100644 --- a/packages/dockview/typedoc.json +++ b/packages/dockview/typedoc.json @@ -1,7 +1,5 @@ { - "out": "typedocs", - "entryPoints": ["./src/index.ts"], - "exclude": ["**/_test/**/*.*", "**/index.ts"], - "excludeExternals": true, - "excludePrivate": true + "extends": ["../../typedoc.base.json"], + "entryPoints": ["src/index.ts"], + "exclude": ["**/dist/**"] } diff --git a/packages/docs/blog/2023-06-03-dockview-1.7.3.md b/packages/docs/blog/2023-06-03-dockview-1.7.3.md new file mode 100644 index 000000000..7a4e6cba8 --- /dev/null +++ b/packages/docs/blog/2023-06-03-dockview-1.7.3.md @@ -0,0 +1,17 @@ +--- +slug: dockview-1.7.3-release +title: Dockview 1.7.3 +tags: [release] +--- + +# Release Notes + +Please reference to docs @ [dockview.dev](https://dockview.dev). + +## 🚀 Features + +## 🛠 Miscs + +- Fix bug custom params named 'title' conflicting with built-in tab 'title' object [#258](https://github.com/mathuo/dockview/issues/258) + +## 🔥 Breaking changes diff --git a/packages/docs/blog/2023-06-10-dockview-1.7.4.md b/packages/docs/blog/2023-06-10-dockview-1.7.4.md new file mode 100644 index 000000000..6e8e2fe3f --- /dev/null +++ b/packages/docs/blog/2023-06-10-dockview-1.7.4.md @@ -0,0 +1,20 @@ +--- +slug: dockview-1.7.4-release +title: Dockview 1.7.4 +tags: [release] +--- + +# Release Notes + +Please reference to docs @ [dockview.dev](https://dockview.dev). + +## 🚀 Features + +- Improvements and tests added to the panel `api.updateParameters(...)` method [#265](https://github.com/mathuo/dockview/pull/265) + +## 🛠 Miscs + +- Fix bug associated with overidding panel titles when using `api.updateParameters(...)` [#265](https://github.com/mathuo/dockview/pull/265) +- Cleanup listeners and disposables after use [#257](https://github.com/mathuo/dockview/pull/257) + +## 🔥 Breaking changes diff --git a/packages/docs/blog/2023-06-11-dockview-1.7.5.md b/packages/docs/blog/2023-06-11-dockview-1.7.5.md new file mode 100644 index 000000000..69e4b008f --- /dev/null +++ b/packages/docs/blog/2023-06-11-dockview-1.7.5.md @@ -0,0 +1,17 @@ +--- +slug: dockview-1.7.5-release +title: Dockview 1.7.5 +tags: [release] +--- + +# Release Notes + +Please reference to docs @ [dockview.dev](https://dockview.dev). + +## 🚀 Features + +## 🛠 Miscs + +- Fix [#255](https://github.com/mathuo/dockview/issues/255) + +## 🔥 Breaking changes diff --git a/packages/docs/docs/components/dockview.mdx b/packages/docs/docs/components/dockview.mdx index c66b33e81..576c574a3 100644 --- a/packages/docs/docs/components/dockview.mdx +++ b/packages/docs/docs/components/dockview.mdx @@ -2,7 +2,10 @@ description: Dockview Documentation --- -import { Container } from '@site/src/components/ui/container'; +import { + Container, + MultiFrameworkContainer, +} from '@site/src/components/ui/container'; import Link from '@docusaurus/Link'; import useBaseUrl from '@docusaurus/useBaseUrl'; @@ -24,7 +27,12 @@ import RenderingDockview from '@site/sandboxes/rendering-dockview/src/app'; import DockviewExternalDnd from '@site/sandboxes/externaldnd-dockview/src/app'; import DockviewResizeContainer from '@site/sandboxes/resizecontainer-dockview/src/app'; import DockviewTabheight from '@site/sandboxes/tabheight-dockview/src/app'; -import { attach as attachDockviewVanilla } from '@site/sandboxes/vanilla-dockview/src/app'; +import DockviewWithIFrames from '@site/sandboxes/iframe-dockview/src/app'; + +import { attach as attachDockviewVanilla } from '@site/sandboxes/javascript/vanilla-dockview/src/app'; +import { attach as attachSimpleDockview } from '@site/sandboxes/javascript/simple-dockview/src/app'; +import { attach as attachTabHeightDockview } from '@site/sandboxes/javascript/tabheight-dockview/src/app'; +import { attach as attachNativeDockview } from '@site/sandboxes/javascript/fullwidthtab-dockview/src/app'; # Dockview @@ -32,12 +40,16 @@ import { attach as attachDockviewVanilla } from '@site/sandboxes/vanilla-dockvie Dockview is an abstraction built on top of [Gridviews](./gridview) where each view is a container of many tabbed panels. - - - + -You can access the panels associated group through the `panel.group` variable. -The group will always be defined and will change if a panel is moved into another group. +
+ +> You can access the panels associated group through the `panel.group` variable. +> The group will always be defined and will change if a panel is moved into another group. ## DockviewReact Component @@ -424,6 +436,40 @@ const panel2 = api.addPanel({ }); ``` +### Update Panel + +You can programatically update the `params` passed through to the panel through the panal api using `api.updateParameters`. + +```ts +const panel = api.addPanel({ + id: 'panel_1', + component: 'default', + params: { + keyA: 'valueA', + }, +}); + +// ... + +panel.api.updateParameters({ + keyB: 'valueB', +}); + +// ... + +panel.api.updateParameters({ + keyA: 'anotherValueA', +}); +``` + +To delete a parameter you should pass a value of `undefined` for the key. + +```ts +panel.api.updateParameters({ + keyA: undefined, // this will delete 'keyA'. +}); +``` + ### Panel Rendering By default `DockviewReact` only adds to the DOM those panels that are visible, @@ -608,17 +654,21 @@ to the entire width of the group. For example: ``` - - - + ### Tab Height Tab height can be controlled through CSS. - - - + ## Groups @@ -690,6 +740,29 @@ api.group.api.setConstraints(...) +## iFrames + +iFrames required special attention because of a particular behaviour in how iFrames render: + +> Re-parenting an iFrame will reload the contents of the iFrame or the rephrase this, moving an iFrame within the DOM will cause a reload of its contents. + +You can find many examples of discussions on this. Two reputable forums for example are linked [here](https://bugzilla.mozilla.org/show_bug.cgi?id=254144) and [here](https://github.com/whatwg/html/issues/5484). + +The problem with iFrames and `dockview` is that when you hide or move a panel that panels DOM element may be moved within the DOM or removed from the DOM completely. +If your panel contains an iFrame then that iFrame will reload after being re-positioned within the DOM tree and all state in that iFrame will most likely be lost. + +`dockview` does not provide a built-in solution to this because it's too specific of a problem to include in the library. +However the below example does show an implementation of a higher-order component `HoistedDockviewPanel`that you could use to work around this problems and make iFrames behave in `dockview`. + +What the higher-order component is doing is to hoist the panels contents into a DOM element that is always present and then `position: absolute` that element to match the dimensions of it's linked panel. +The visibility of these hoisted elements is then controlled through some exposed api methods to hide elements that shouldn't be currently shown. + +You should open this example in CodeSandbox using the provided link to understand the code and make use of this implemention if required. + + + + + ## Events A simple example showing events fired by `dockviewz that can be interacted with. @@ -709,19 +782,11 @@ If you wish to interact with the drop event from one dockview instance in anothe -### Example - -hello +### Window-like mananger with tabs -hello 2 - -
- -
- -## VanillaJS +## Vanilla JS > Note: This section is experimental and support for Vanilla JS is a work in progress. @@ -732,6 +797,6 @@ The core library is published as an independant package under the name `dockview > `dockview-core` is a dependency of `dockview` and automatically installed during the installation process of `dockview` via `npm install dockview`. diff --git a/packages/docs/package.json b/packages/docs/package.json index c230bb45b..577f43f60 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -1,6 +1,6 @@ { "name": "dockview-docs", - "version": "1.7.2", + "version": "1.7.5", "private": true, "scripts": { "docusaurus": "docusaurus", @@ -12,8 +12,7 @@ "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", - "typecheck": "tsc", - "deploy-docs": "node scripts/package-docs.js" + "typecheck": "tsc" }, "dependencies": { "@docusaurus/core": "^2.4.0", @@ -23,7 +22,7 @@ "@minoru/react-dnd-treeview": "^3.4.3", "axios": "^1.3.3", "clsx": "^1.2.1", - "dockview": "^1.7.2", + "dockview": "^1.7.5", "prism-react-renderer": "^1.3.5", "react": "^18.2.0", "react-dnd": "^16.0.1", diff --git a/packages/docs/sandboxes/constraints-dockview/package.json b/packages/docs/sandboxes/constraints-dockview/package.json index e1497330a..3a34098c8 100644 --- a/packages/docs/sandboxes/constraints-dockview/package.json +++ b/packages/docs/sandboxes/constraints-dockview/package.json @@ -14,7 +14,8 @@ "devDependencies": { "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "react-scripts": "*" }, "scripts": { "start": "react-scripts start", diff --git a/packages/docs/sandboxes/constraints-dockview/tsconfig.json b/packages/docs/sandboxes/constraints-dockview/tsconfig.json index 6e13e47b5..cdc4fb5f5 100644 --- a/packages/docs/sandboxes/constraints-dockview/tsconfig.json +++ b/packages/docs/sandboxes/constraints-dockview/tsconfig.json @@ -1,20 +1,18 @@ { - "compilerOptions": { - "outDir": "build/dist", - "module": "esnext", - "target": "es5", - "lib": ["es6", "dom"], - "sourceMap": true, - "allowJs": true, - "jsx": "react-jsx", - "moduleResolution": "node", - "rootDir": "src", - "forceConsistentCasingInFileNames": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noImplicitAny": true, - "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true - } + "compilerOptions": { + "outDir": "build/dist", + "module": "esnext", + "target": "es5", + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "rootDir": "src", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true + } } diff --git a/packages/docs/sandboxes/customheader-dockview/package.json b/packages/docs/sandboxes/customheader-dockview/package.json index 8bf9bd86d..d3ede7462 100644 --- a/packages/docs/sandboxes/customheader-dockview/package.json +++ b/packages/docs/sandboxes/customheader-dockview/package.json @@ -14,7 +14,8 @@ "devDependencies": { "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "react-scripts": "*" }, "scripts": { "start": "react-scripts start", diff --git a/packages/docs/sandboxes/customheader-dockview/tsconfig.json b/packages/docs/sandboxes/customheader-dockview/tsconfig.json index 6e13e47b5..cdc4fb5f5 100644 --- a/packages/docs/sandboxes/customheader-dockview/tsconfig.json +++ b/packages/docs/sandboxes/customheader-dockview/tsconfig.json @@ -1,20 +1,18 @@ { - "compilerOptions": { - "outDir": "build/dist", - "module": "esnext", - "target": "es5", - "lib": ["es6", "dom"], - "sourceMap": true, - "allowJs": true, - "jsx": "react-jsx", - "moduleResolution": "node", - "rootDir": "src", - "forceConsistentCasingInFileNames": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noImplicitAny": true, - "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true - } + "compilerOptions": { + "outDir": "build/dist", + "module": "esnext", + "target": "es5", + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "rootDir": "src", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true + } } diff --git a/packages/docs/sandboxes/demo-dockview/package.json b/packages/docs/sandboxes/demo-dockview/package.json index 99ba4505f..fe47d8659 100644 --- a/packages/docs/sandboxes/demo-dockview/package.json +++ b/packages/docs/sandboxes/demo-dockview/package.json @@ -16,7 +16,8 @@ "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", "@types/uuid": "^9.0.0", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "react-scripts": "*" }, "scripts": { "start": "react-scripts start", diff --git a/packages/docs/sandboxes/demo-dockview/tsconfig.json b/packages/docs/sandboxes/demo-dockview/tsconfig.json index 6e13e47b5..cdc4fb5f5 100644 --- a/packages/docs/sandboxes/demo-dockview/tsconfig.json +++ b/packages/docs/sandboxes/demo-dockview/tsconfig.json @@ -1,20 +1,18 @@ { - "compilerOptions": { - "outDir": "build/dist", - "module": "esnext", - "target": "es5", - "lib": ["es6", "dom"], - "sourceMap": true, - "allowJs": true, - "jsx": "react-jsx", - "moduleResolution": "node", - "rootDir": "src", - "forceConsistentCasingInFileNames": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noImplicitAny": true, - "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true - } + "compilerOptions": { + "outDir": "build/dist", + "module": "esnext", + "target": "es5", + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "rootDir": "src", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true + } } diff --git a/packages/docs/sandboxes/dnd-dockview/package.json b/packages/docs/sandboxes/dnd-dockview/package.json index f9ef48454..fec39fd83 100644 --- a/packages/docs/sandboxes/dnd-dockview/package.json +++ b/packages/docs/sandboxes/dnd-dockview/package.json @@ -14,7 +14,8 @@ "devDependencies": { "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "react-scripts": "*" }, "scripts": { "start": "react-scripts start", diff --git a/packages/docs/sandboxes/dnd-dockview/tsconfig.json b/packages/docs/sandboxes/dnd-dockview/tsconfig.json index 6e13e47b5..cdc4fb5f5 100644 --- a/packages/docs/sandboxes/dnd-dockview/tsconfig.json +++ b/packages/docs/sandboxes/dnd-dockview/tsconfig.json @@ -1,20 +1,18 @@ { - "compilerOptions": { - "outDir": "build/dist", - "module": "esnext", - "target": "es5", - "lib": ["es6", "dom"], - "sourceMap": true, - "allowJs": true, - "jsx": "react-jsx", - "moduleResolution": "node", - "rootDir": "src", - "forceConsistentCasingInFileNames": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noImplicitAny": true, - "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true - } + "compilerOptions": { + "outDir": "build/dist", + "module": "esnext", + "target": "es5", + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "rootDir": "src", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true + } } diff --git a/packages/docs/sandboxes/dockview-app/package.json b/packages/docs/sandboxes/dockview-app/package.json index b4c4e1d29..edc8773be 100644 --- a/packages/docs/sandboxes/dockview-app/package.json +++ b/packages/docs/sandboxes/dockview-app/package.json @@ -14,7 +14,8 @@ "devDependencies": { "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "react-scripts": "*" }, "scripts": { "start": "react-scripts start", diff --git a/packages/docs/sandboxes/dockview-app/tsconfig.json b/packages/docs/sandboxes/dockview-app/tsconfig.json index 6e13e47b5..cdc4fb5f5 100644 --- a/packages/docs/sandboxes/dockview-app/tsconfig.json +++ b/packages/docs/sandboxes/dockview-app/tsconfig.json @@ -1,20 +1,18 @@ { - "compilerOptions": { - "outDir": "build/dist", - "module": "esnext", - "target": "es5", - "lib": ["es6", "dom"], - "sourceMap": true, - "allowJs": true, - "jsx": "react-jsx", - "moduleResolution": "node", - "rootDir": "src", - "forceConsistentCasingInFileNames": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noImplicitAny": true, - "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true - } + "compilerOptions": { + "outDir": "build/dist", + "module": "esnext", + "target": "es5", + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "rootDir": "src", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true + } } diff --git a/packages/docs/sandboxes/events-dockview/package.json b/packages/docs/sandboxes/events-dockview/package.json index d32c8451d..176fc41c3 100644 --- a/packages/docs/sandboxes/events-dockview/package.json +++ b/packages/docs/sandboxes/events-dockview/package.json @@ -14,7 +14,8 @@ "devDependencies": { "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "react-scripts": "*" }, "scripts": { "start": "react-scripts start", diff --git a/packages/docs/sandboxes/events-dockview/tsconfig.json b/packages/docs/sandboxes/events-dockview/tsconfig.json index 6e13e47b5..cdc4fb5f5 100644 --- a/packages/docs/sandboxes/events-dockview/tsconfig.json +++ b/packages/docs/sandboxes/events-dockview/tsconfig.json @@ -1,20 +1,18 @@ { - "compilerOptions": { - "outDir": "build/dist", - "module": "esnext", - "target": "es5", - "lib": ["es6", "dom"], - "sourceMap": true, - "allowJs": true, - "jsx": "react-jsx", - "moduleResolution": "node", - "rootDir": "src", - "forceConsistentCasingInFileNames": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noImplicitAny": true, - "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true - } + "compilerOptions": { + "outDir": "build/dist", + "module": "esnext", + "target": "es5", + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "rootDir": "src", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true + } } diff --git a/packages/docs/sandboxes/externaldnd-dockview/package.json b/packages/docs/sandboxes/externaldnd-dockview/package.json index 10ab655c5..fba6c4e59 100644 --- a/packages/docs/sandboxes/externaldnd-dockview/package.json +++ b/packages/docs/sandboxes/externaldnd-dockview/package.json @@ -16,7 +16,8 @@ "devDependencies": { "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "react-scripts": "*" }, "scripts": { "start": "react-scripts start", diff --git a/packages/docs/sandboxes/externaldnd-dockview/tsconfig.json b/packages/docs/sandboxes/externaldnd-dockview/tsconfig.json index 6e13e47b5..cdc4fb5f5 100644 --- a/packages/docs/sandboxes/externaldnd-dockview/tsconfig.json +++ b/packages/docs/sandboxes/externaldnd-dockview/tsconfig.json @@ -1,20 +1,18 @@ { - "compilerOptions": { - "outDir": "build/dist", - "module": "esnext", - "target": "es5", - "lib": ["es6", "dom"], - "sourceMap": true, - "allowJs": true, - "jsx": "react-jsx", - "moduleResolution": "node", - "rootDir": "src", - "forceConsistentCasingInFileNames": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noImplicitAny": true, - "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true - } + "compilerOptions": { + "outDir": "build/dist", + "module": "esnext", + "target": "es5", + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "rootDir": "src", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true + } } diff --git a/packages/docs/sandboxes/fullwidthtab-dockview/package.json b/packages/docs/sandboxes/fullwidthtab-dockview/package.json index 5fddc50b3..ade7f643e 100644 --- a/packages/docs/sandboxes/fullwidthtab-dockview/package.json +++ b/packages/docs/sandboxes/fullwidthtab-dockview/package.json @@ -14,7 +14,8 @@ "devDependencies": { "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "react-scripts": "*" }, "scripts": { "start": "react-scripts start", diff --git a/packages/docs/sandboxes/fullwidthtab-dockview/tsconfig.json b/packages/docs/sandboxes/fullwidthtab-dockview/tsconfig.json index 6e13e47b5..cdc4fb5f5 100644 --- a/packages/docs/sandboxes/fullwidthtab-dockview/tsconfig.json +++ b/packages/docs/sandboxes/fullwidthtab-dockview/tsconfig.json @@ -1,20 +1,18 @@ { - "compilerOptions": { - "outDir": "build/dist", - "module": "esnext", - "target": "es5", - "lib": ["es6", "dom"], - "sourceMap": true, - "allowJs": true, - "jsx": "react-jsx", - "moduleResolution": "node", - "rootDir": "src", - "forceConsistentCasingInFileNames": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noImplicitAny": true, - "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true - } + "compilerOptions": { + "outDir": "build/dist", + "module": "esnext", + "target": "es5", + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "rootDir": "src", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true + } } diff --git a/packages/docs/sandboxes/groupcontrol-dockview/package.json b/packages/docs/sandboxes/groupcontrol-dockview/package.json index 21e91b3a4..7c88c11f1 100644 --- a/packages/docs/sandboxes/groupcontrol-dockview/package.json +++ b/packages/docs/sandboxes/groupcontrol-dockview/package.json @@ -14,7 +14,8 @@ "devDependencies": { "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "react-scripts": "*" }, "scripts": { "start": "react-scripts start", diff --git a/packages/docs/sandboxes/groupcontrol-dockview/tsconfig.json b/packages/docs/sandboxes/groupcontrol-dockview/tsconfig.json index 6e13e47b5..cdc4fb5f5 100644 --- a/packages/docs/sandboxes/groupcontrol-dockview/tsconfig.json +++ b/packages/docs/sandboxes/groupcontrol-dockview/tsconfig.json @@ -1,20 +1,18 @@ { - "compilerOptions": { - "outDir": "build/dist", - "module": "esnext", - "target": "es5", - "lib": ["es6", "dom"], - "sourceMap": true, - "allowJs": true, - "jsx": "react-jsx", - "moduleResolution": "node", - "rootDir": "src", - "forceConsistentCasingInFileNames": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noImplicitAny": true, - "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true - } + "compilerOptions": { + "outDir": "build/dist", + "module": "esnext", + "target": "es5", + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "rootDir": "src", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true + } } diff --git a/packages/docs/sandboxes/iframe-dockview/package.json b/packages/docs/sandboxes/iframe-dockview/package.json new file mode 100644 index 000000000..0a750cb5e --- /dev/null +++ b/packages/docs/sandboxes/iframe-dockview/package.json @@ -0,0 +1,32 @@ +{ + "name": "iframe-dockview", + "description": "", + "keywords": [ + "dockview" + ], + "version": "1.0.0", + "main": "src/index.tsx", + "dependencies": { + "dockview": "*", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.0.28", + "@types/react-dom": "^18.0.11", + "typescript": "^4.9.5", + "react-scripts": "*" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ] +} \ No newline at end of file diff --git a/packages/docs/sandboxes/vanilla-dockview/public/index.html b/packages/docs/sandboxes/iframe-dockview/public/index.html similarity index 100% rename from packages/docs/sandboxes/vanilla-dockview/public/index.html rename to packages/docs/sandboxes/iframe-dockview/public/index.html diff --git a/packages/docs/sandboxes/iframe-dockview/src/app.tsx b/packages/docs/sandboxes/iframe-dockview/src/app.tsx new file mode 100644 index 000000000..8aefb6524 --- /dev/null +++ b/packages/docs/sandboxes/iframe-dockview/src/app.tsx @@ -0,0 +1,61 @@ +import { + DockviewReact, + DockviewReadyEvent, + IDockviewPanelProps, +} from 'dockview'; +import * as React from 'react'; +import { HoistedDockviewPanel } from './hoistedDockviewPanel'; + +const components = { + iframeComponent: HoistedDockviewPanel( + (props: IDockviewPanelProps<{ color: string }>) => { + return ( +