diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index 856df6dc8..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", @@ -29,4 +30,4 @@ "/packages/docs/sandboxes/javascript/vanilla-dockview" ], "node": "16" -} +} \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7222bc815..035c9224f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 6b82733a6..f81e2271b 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -9,16 +9,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout 🛎️ - uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly. - with: - persist-credentials: false - + uses: actions/checkout@v3 - name: Use Node.js uses: actions/setup-node@v1 with: node-version: '16.x' - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} @@ -26,7 +23,6 @@ jobs: ${{ runner.os }}-node- - run: yarn install - - run: lerna bootstrap - run: npm run build working-directory: packages/dockview-core - run: npm run build diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ebf84ee07..39a05c47c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 # might be required for sonar to work correctly with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis @@ -16,7 +16,7 @@ jobs: with: node-version: '16.x' - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} diff --git a/lerna.json b/lerna.json index bf55e0899..14afc2271 100644 --- a/lerna.json +++ b/lerna.json @@ -3,7 +3,7 @@ "packages/*" ], "useWorkspaces": true, - "version": "1.7.4", + "version": "1.7.6", "npmClient": "yarn", "command": { "publish": { diff --git a/packages/dockview-core/package.json b/packages/dockview-core/package.json index a4d45486f..6627b402b 100644 --- a/packages/dockview-core/package.json +++ b/packages/dockview-core/package.json @@ -1,6 +1,6 @@ { "name": "dockview-core", - "version": "1.7.4", + "version": "1.7.6", "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__/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 2142045d5..8091ff768 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -541,6 +541,8 @@ describe('dockviewComponent', () => { }, }); + // dockview.layout(1000, 1000, true); + expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({ activeGroup: 'group-1', grid: { @@ -1723,6 +1725,9 @@ describe('dockviewComponent', () => { test_tab_id: PanelTabPartTest, }, }); + + dockview.layout(1000, 1000); + dockview.fromJSON({ activeGroup: 'group-1', grid: { @@ -1918,6 +1923,8 @@ describe('dockviewComponent', () => { orientation: Orientation.HORIZONTAL, }); + dockview.layout(1000, 1000); + expect(dockview.orientation).toBe(Orientation.HORIZONTAL); dockview.fromJSON({ @@ -2023,6 +2030,8 @@ describe('dockviewComponent', () => { orientation: Orientation.HORIZONTAL, }); + dockview.layout(1000, 1000); + expect(dockview.orientation).toBe(Orientation.HORIZONTAL); dockview.fromJSON({ @@ -2163,6 +2172,8 @@ describe('dockviewComponent', () => { orientation: Orientation.HORIZONTAL, }); + dockview.layout(1000, 1000); + expect(dockview.orientation).toBe(Orientation.HORIZONTAL); dockview.fromJSON({ @@ -2448,4 +2459,164 @@ describe('dockviewComponent', () => { 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); + }); + + test('that fromJSON layouts are resized to the current dimensions', async () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + expect(dockview.orientation).toBe(Orientation.HORIZONTAL); + + dockview.layout(1000, 500); + + dockview.fromJSON({ + activeGroup: 'group-1', + grid: { + root: { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + views: ['panel1', 'panel2'], + id: 'group-1', + activeView: 'panel2', + }, + size: 2000, + }, + ], + size: 1000, + }, + height: 1000, + width: 2000, + orientation: Orientation.HORIZONTAL, + }, + panels: { + panel1: { + id: 'panel1', + contentComponent: 'default', + title: 'panel1', + }, + panel2: { + id: 'panel2', + contentComponent: 'default', + title: 'panel2', + }, + }, + }); + + expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({ + activeGroup: 'group-1', + grid: { + root: { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + views: ['panel1', 'panel2'], + id: 'group-1', + activeView: 'panel2', + }, + size: 1000, + }, + ], + size: 500, + }, + height: 500, + width: 1000, + orientation: Orientation.HORIZONTAL, + }, + panels: { + panel1: { + id: 'panel1', + contentComponent: 'default', + title: 'panel1', + }, + panel2: { + id: 'panel2', + contentComponent: 'default', + title: 'panel2', + }, + }, + }); + }); }); diff --git a/packages/dockview-core/src/__tests__/events.spec.ts b/packages/dockview-core/src/__tests__/events.spec.ts index 6ac17cc5c..532390a07 100644 --- a/packages/dockview-core/src/__tests__/events.spec.ts +++ b/packages/dockview-core/src/__tests__/events.spec.ts @@ -1,4 +1,9 @@ -import { Emitter, Event } from '../events'; +import { + Emitter, + Event, + addDisposableListener, + addDisposableWindowListener, +} from '../events'; describe('events', () => { describe('emitter', () => { @@ -101,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__/gridview/gridviewComponent.spec.ts b/packages/dockview-core/src/__tests__/gridview/gridviewComponent.spec.ts index 083526119..267600ffb 100644 --- a/packages/dockview-core/src/__tests__/gridview/gridviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/gridview/gridviewComponent.spec.ts @@ -471,6 +471,8 @@ describe('gridview', () => { components: { default: TestGridview }, }); + gridview.layout(800, 400); + gridview.fromJSON({ grid: { height: 400, @@ -528,7 +530,8 @@ describe('gridview', () => { components: { default: TestGridview }, }); - // gridview.layout(800, 400); + gridview.layout(800, 400); + gridview.fromJSON({ grid: { height: 400, @@ -552,7 +555,6 @@ describe('gridview', () => { }, activePanel: 'panel_1', }); - // gridview.layout(800, 400, true); expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({ grid: { @@ -587,7 +589,8 @@ describe('gridview', () => { components: { default: TestGridview }, }); - // gridview.layout(800, 400); + gridview.layout(800, 400); + gridview.fromJSON({ grid: { height: 400, @@ -620,7 +623,6 @@ describe('gridview', () => { }, activePanel: 'panel_1', }); - // gridview.layout(800, 400, true); expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({ grid: { @@ -664,7 +666,8 @@ describe('gridview', () => { components: { default: TestGridview }, }); - // gridview.layout(800, 400); + gridview.layout(800, 400); + gridview.fromJSON({ grid: { height: 400, @@ -706,7 +709,6 @@ describe('gridview', () => { }, activePanel: 'panel_1', }); - // gridview.layout(800, 400, true); expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({ grid: { @@ -759,7 +761,8 @@ describe('gridview', () => { components: { default: TestGridview }, }); - // gridview.layout(800, 400); + gridview.layout(800, 400); + gridview.fromJSON({ grid: { height: 400, @@ -801,7 +804,6 @@ describe('gridview', () => { }, activePanel: 'panel_1', }); - // gridview.layout(800, 400, true); expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({ grid: { @@ -854,6 +856,8 @@ describe('gridview', () => { components: { default: TestGridview }, }); + gridview.layout(800, 400); + gridview.fromJSON({ grid: { height: 400, @@ -895,7 +899,6 @@ describe('gridview', () => { }, activePanel: 'panel_1', }); - gridview.layout(800, 400, true); expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({ grid: { @@ -948,7 +951,8 @@ describe('gridview', () => { components: { default: TestGridview }, }); - // gridview.layout(800, 400); + gridview.layout(800, 400); + gridview.fromJSON({ grid: { height: 400, @@ -1005,7 +1009,6 @@ describe('gridview', () => { }, activePanel: 'panel_1', }); - // gridview.layout(800, 400, true); expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({ grid: { @@ -1198,6 +1201,8 @@ describe('gridview', () => { components: { default: TestGridview }, }); + gridview.layout(800, 400); + gridview.fromJSON({ grid: { height: 400, @@ -1254,7 +1259,8 @@ describe('gridview', () => { }, activePanel: 'panel_1', }); - gridview.layout(800, 400, true); + + // gridview.layout(800, 400, true); expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({ grid: { @@ -1322,6 +1328,8 @@ describe('gridview', () => { components: { default: TestGridview }, }); + gridview.layout(800, 400); + gridview.fromJSON({ grid: { height: 400, @@ -1445,6 +1453,8 @@ describe('gridview', () => { components: { default: TestGridview }, }); + gridview.layout(800, 400); + gridview.fromJSON({ grid: { height: 400, @@ -1908,4 +1918,318 @@ describe('gridview', () => { return disposable.dispose(); }); + + test('that fromJSON layouts are resized to the current dimensions', async () => { + const container = document.createElement('div'); + + const gridview = new GridviewComponent({ + parentElement: container, + proportionalLayout: true, + orientation: Orientation.VERTICAL, + components: { default: TestGridview }, + }); + + gridview.layout(1600, 800); + + gridview.fromJSON({ + grid: { + height: 400, + width: 800, + orientation: Orientation.HORIZONTAL, + root: { + type: 'branch', + size: 400, + data: [ + { + type: 'leaf', + size: 200, + data: { + id: 'panel_1', + component: 'default', + snap: false, + }, + }, + { + type: 'branch', + size: 400, + data: [ + { + type: 'leaf', + size: 250, + data: { + id: 'panel_2', + component: 'default', + snap: false, + }, + }, + { + type: 'leaf', + size: 150, + data: { + id: 'panel_3', + component: 'default', + snap: false, + }, + }, + ], + }, + { + type: 'leaf', + size: 200, + data: { + id: 'panel_4', + component: 'default', + snap: false, + }, + }, + ], + }, + }, + activePanel: 'panel_1', + }); + + expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({ + grid: { + height: 800, + width: 1600, + orientation: Orientation.HORIZONTAL, + root: { + type: 'branch', + size: 800, + data: [ + { + type: 'leaf', + size: 400, + data: { + id: 'panel_1', + component: 'default', + snap: false, + }, + }, + { + type: 'branch', + size: 800, + data: [ + { + type: 'leaf', + size: 500, + data: { + id: 'panel_2', + component: 'default', + snap: false, + }, + }, + { + type: 'leaf', + size: 300, + data: { + id: 'panel_3', + component: 'default', + snap: false, + }, + }, + ], + }, + { + type: 'leaf', + size: 400, + data: { + id: 'panel_4', + component: 'default', + snap: false, + }, + }, + ], + }, + }, + activePanel: 'panel_1', + }); + }); + + test('that a deep layout with fromJSON dimensions identical to the current dimensions loads', async () => { + const container = document.createElement('div'); + + const gridview = new GridviewComponent({ + parentElement: container, + proportionalLayout: true, + orientation: Orientation.VERTICAL, + components: { default: TestGridview }, + }); + + gridview.layout(5000, 5000); + + gridview.fromJSON({ + grid: { + height: 5000, + width: 5000, + orientation: Orientation.HORIZONTAL, + root: { + type: 'branch', + size: 5000, + data: [ + { + type: 'leaf', + size: 1000, + data: { + id: 'panel_1', + component: 'default', + snap: false, + }, + }, + { + type: 'branch', + size: 2000, + data: [ + { + type: 'branch', + size: 4000, + data: [ + { + type: 'leaf', + size: 1000, + data: { + id: 'panel_2', + component: 'default', + snap: false, + }, + }, + { + type: 'branch', + size: 1000, + data: [ + { + type: 'leaf', + size: 2000, + data: { + id: 'panel_3', + component: 'default', + snap: false, + }, + }, + { + type: 'leaf', + size: 2000, + data: { + id: 'panel_4', + component: 'default', + snap: false, + }, + }, + ], + }, + ], + }, + { + type: 'leaf', + size: 1000, + data: { + id: 'panel_5', + component: 'default', + snap: false, + }, + }, + ], + }, + { + type: 'leaf', + size: 2000, + data: { + id: 'panel_6', + component: 'default', + snap: false, + }, + }, + ], + }, + }, + activePanel: 'panel_1', + }); + + expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({ + grid: { + height: 5000, + width: 5000, + orientation: Orientation.HORIZONTAL, + root: { + type: 'branch', + size: 5000, + data: [ + { + type: 'leaf', + size: 1000, + data: { + id: 'panel_1', + component: 'default', + snap: false, + }, + }, + { + type: 'branch', + size: 2000, + data: [ + { + type: 'branch', + size: 4000, + data: [ + { + type: 'leaf', + size: 1000, + data: { + id: 'panel_2', + component: 'default', + snap: false, + }, + }, + { + type: 'branch', + size: 1000, + data: [ + { + type: 'leaf', + size: 2000, + data: { + id: 'panel_3', + component: 'default', + snap: false, + }, + }, + { + type: 'leaf', + size: 2000, + data: { + id: 'panel_4', + component: 'default', + snap: false, + }, + }, + ], + }, + ], + }, + { + type: 'leaf', + size: 1000, + data: { + id: 'panel_5', + component: 'default', + snap: false, + }, + }, + ], + }, + { + type: 'leaf', + size: 2000, + data: { + id: 'panel_6', + component: 'default', + snap: false, + }, + }, + ], + }, + }, + activePanel: 'panel_1', + }); + }); }); diff --git a/packages/dockview-core/src/__tests__/paneview/paneviewComponent.spec.ts b/packages/dockview-core/src/__tests__/paneview/paneviewComponent.spec.ts index cb5763046..9ffd22e49 100644 --- a/packages/dockview-core/src/__tests__/paneview/paneviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/paneview/paneviewComponent.spec.ts @@ -408,4 +408,85 @@ describe('componentPaneview', () => { expect(panel1Spy).toHaveBeenCalledTimes(1); expect(panel2Spy).toHaveBeenCalledTimes(1); }); + + test('that fromJSON layouts are resized to the current dimensions', async () => { + const paneview = new PaneviewComponent({ + parentElement: container, + components: { + testPanel: TestPanel, + }, + }); + + paneview.layout(400, 600); + + paneview.fromJSON({ + size: 6, + views: [ + { + size: 1, + data: { + id: 'panel1', + component: 'testPanel', + title: 'Panel 1', + }, + expanded: true, + }, + { + size: 2, + data: { + id: 'panel2', + component: 'testPanel', + title: 'Panel 2', + }, + expanded: true, + }, + { + size: 3, + data: { + id: 'panel3', + component: 'testPanel', + title: 'Panel 3', + }, + expanded: true, + }, + ], + }); + + // heights slightly differ because header height isn't accounted for + expect(JSON.parse(JSON.stringify(paneview.toJSON()))).toEqual({ + size: 600, + views: [ + { + size: 122, + data: { + id: 'panel1', + component: 'testPanel', + title: 'Panel 1', + }, + expanded: true, + minimumSize: 100, + }, + { + size: 122, + data: { + id: 'panel2', + component: 'testPanel', + title: 'Panel 2', + }, + expanded: true, + minimumSize: 100, + }, + { + size: 356, + data: { + id: 'panel3', + component: 'testPanel', + title: 'Panel 3', + }, + expanded: true, + minimumSize: 100, + }, + ], + }); + }); }); diff --git a/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts b/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts index 23c9b1df8..ec7654393 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', () => { @@ -596,4 +598,82 @@ describe('splitview', () => { expect(anyEvents).toBeFalsy(); expect(container.childNodes.length).toBe(0); }); + + test('dnd: pointer 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( + sashElement, + new MouseEvent('pointerdown', { clientX: 50, clientY: 100 }) + ); + + expect(addEventListenerSpy).toBeCalledTimes(3); + + // 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( + document, + new MouseEvent('pointermove', { clientX: 70, clientY: 110 }) + ); + expect([view1.size, view2.size]).toEqual([220, 180]); + + // expect a delta move of 75 - 70 = 5 + fireEvent( + document, + new MouseEvent('pointermove', { clientX: 75, clientY: 110 }) + ); + expect([view1.size, view2.size]).toEqual([225, 175]); + + // end the drag event + fireEvent( + document, + new MouseEvent('pointerup', { clientX: 70, clientY: 110 }) + ); + + expect(removeEventListenerSpy).toBeCalledTimes(3); + + // expect pointer-eventes on views to be restored + expect(view1.element.parentElement!.style.pointerEvents).toBe(''); + expect(view2.element.parentElement!.style.pointerEvents).toBe(''); + + fireEvent( + document, + new MouseEvent('pointermove', { clientX: 100, clientY: 100 }) + ); + // expect no additional resizes + expect([view1.size, view2.size]).toEqual([225, 175]); + // expect no additional document listeners + expect(addEventListenerSpy).toBeCalledTimes(3); + expect(removeEventListenerSpy).toBeCalledTimes(3); + }); }); diff --git a/packages/dockview-core/src/__tests__/splitview/splitviewComponent.spec.ts b/packages/dockview-core/src/__tests__/splitview/splitviewComponent.spec.ts index e43c94239..81525a26c 100644 --- a/packages/dockview-core/src/__tests__/splitview/splitviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/splitview/splitviewComponent.spec.ts @@ -330,7 +330,7 @@ describe('componentSplitview', () => { testPanel: TestPanel, }, }); - splitview.layout(600, 400); + splitview.layout(400, 6); splitview.fromJSON({ views: [ @@ -535,4 +535,57 @@ describe('componentSplitview', () => { expect(panel1Spy).toHaveBeenCalledTimes(1); expect(panel2Spy).toHaveBeenCalledTimes(1); }); + + test('that fromJSON layouts are resized to the current dimensions', async () => { + const splitview = new SplitviewComponent({ + parentElement: container, + orientation: Orientation.VERTICAL, + components: { + testPanel: TestPanel, + }, + }); + splitview.layout(400, 600); + + splitview.fromJSON({ + views: [ + { + size: 1, + data: { id: 'panel1', component: 'testPanel' }, + snap: false, + }, + { + size: 2, + data: { id: 'panel2', component: 'testPanel' }, + snap: true, + }, + { size: 3, data: { id: 'panel3', component: 'testPanel' } }, + ], + size: 6, + orientation: Orientation.VERTICAL, + activeView: 'panel1', + }); + + expect(JSON.parse(JSON.stringify(splitview.toJSON()))).toEqual({ + views: [ + { + size: 100, + data: { id: 'panel1', component: 'testPanel' }, + snap: false, + }, + { + size: 200, + data: { id: 'panel2', component: 'testPanel' }, + snap: true, + }, + { + size: 300, + data: { id: 'panel3', component: 'testPanel' }, + snap: false, + }, + ], + size: 600, + orientation: Orientation.VERTICAL, + activeView: 'panel1', + }); + }); }); diff --git a/packages/dockview-core/src/dnd/abstractDragHandler.ts b/packages/dockview-core/src/dnd/abstractDragHandler.ts index df3f49177..55e03fa42 100644 --- a/packages/dockview-core/src/dnd/abstractDragHandler.ts +++ b/packages/dockview-core/src/dnd/abstractDragHandler.ts @@ -7,17 +7,20 @@ 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.addDisposables( + this._onDragStart, + this.dataDisposable, + this.pointerEventsDisposable + ); this.configure(); } @@ -28,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'; @@ -61,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/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 52fd88b18..cce14c23d 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -413,6 +413,10 @@ export class DockviewComponent throw new Error('root must be of type branch'); } + // take note of the existing dimensions + const width = this.width; + const height = this.height; + this.gridview.deserialize(grid, { fromJSON: (node: ISerializedLeafNode) => { const { id, locked, hideHeader, views, activeView } = node.data; @@ -454,6 +458,8 @@ export class DockviewComponent }, }); + this.layout(width, height); + if (typeof activeGroup === 'string') { const panel = this.getPanel(activeGroup); if (panel) { @@ -461,8 +467,6 @@ export class DockviewComponent } } - this.gridview.layout(this.width, this.height); - this._onDidLayoutFromJSON.fire(); } diff --git a/packages/dockview-core/src/events.ts b/packages/dockview-core/src/events.ts index 91f27a1b1..cb8d95930 100644 --- a/packages/dockview-core/src/events.ts +++ b/packages/dockview-core/src/events.ts @@ -162,7 +162,7 @@ export function addDisposableWindowListener( return { dispose: () => { - element.removeEventListener(type, listener); + element.removeEventListener(type, listener, options); }, }; } @@ -177,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/gridview.ts b/packages/dockview-core/src/gridview/gridview.ts index 583b63c6f..cd49a5624 100644 --- a/packages/dockview-core/src/gridview/gridview.ts +++ b/packages/dockview-core/src/gridview/gridview.ts @@ -371,8 +371,7 @@ export class Gridview implements IDisposable { root, orientation, deserializer, - orthogonalSize, - true + orthogonalSize ) as BranchNode; } @@ -380,8 +379,7 @@ export class Gridview implements IDisposable { node: ISerializedNode, orientation: Orientation, deserializer: IViewDeserializer, - orthogonalSize: number, - isRoot = false + orthogonalSize: number ): Node { let result: Node; if (node.type === 'branch') { @@ -398,14 +396,12 @@ export class Gridview implements IDisposable { } as INodeDescriptor; }); - // HORIZONTAL => height=orthogonalsize width=size - // VERTICAL => height=size width=orthogonalsize result = new BranchNode( orientation, this.proportionalLayout, this.styles, - isRoot ? orthogonalSize : node.size, - isRoot ? node.size : orthogonalSize, + orthogonalSize, // <- size - flips at each depth + node.size, // <- orthogonal size - flips at each depth children ); } else { @@ -678,67 +674,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'); } - const view = node.view; - node.dispose(); // dispose of node + parent.removeChild(index, sizing); + nodeToRemove.dispose(); - const child = parent.removeChild(index, sizing); - child.dispose(); - - if (parent.children.length === 0) { - return view; + if (parent.children.length !== 1) { + return nodeToRemove.view; } - if (parent.children.length > 1) { - return 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 view; + // if the sibling is a leaf node no action is required + return nodeToRemove.view; } - // we must promote sibling to be the new root - const child = parent.removeChild(0, sizing); - child.dispose(); + // 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 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); - const childNode = parent.removeChild(0, sizing); - childNode.dispose(); + // 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) ); - const parentNode = grandParent.removeChild(parentIndex, sizing); - parentNode.dispose(); + + // 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), @@ -747,14 +758,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 view; + return nodeToRemove.view; } public layout(width: number, height: number): void { diff --git a/packages/dockview-core/src/gridview/gridviewComponent.ts b/packages/dockview-core/src/gridview/gridviewComponent.ts index b216cd8a9..40dea53aa 100644 --- a/packages/dockview-core/src/gridview/gridviewComponent.ts +++ b/packages/dockview-core/src/gridview/gridviewComponent.ts @@ -176,6 +176,10 @@ export class GridviewComponent const queue: Function[] = []; + // take note of the existing dimensions + const width = this.width; + const height = this.height; + this.gridview.deserialize(grid, { fromJSON: (node) => { const { data } = node; @@ -215,7 +219,7 @@ export class GridviewComponent }, }); - this.layout(this.width, this.height, true); + this.layout(width, height); queue.forEach((f) => f()); diff --git a/packages/dockview-core/src/paneview/paneviewComponent.ts b/packages/dockview-core/src/paneview/paneviewComponent.ts index 290316eb0..57c3b9beb 100644 --- a/packages/dockview-core/src/paneview/paneviewComponent.ts +++ b/packages/dockview-core/src/paneview/paneviewComponent.ts @@ -360,6 +360,10 @@ export class PaneviewComponent extends Resizable implements IPaneviewComponent { const queue: Function[] = []; + // take note of the existing dimensions + const width = this.width; + const height = this.height; + this.paneview = new Paneview(this.element, { orientation: Orientation.VERTICAL, descriptor: { @@ -437,7 +441,7 @@ export class PaneviewComponent extends Resizable implements IPaneviewComponent { }, }); - this.layout(this.width, this.height); + this.layout(width, height); queue.forEach((f) => f()); diff --git a/packages/dockview-core/src/splitview/splitview.scss b/packages/dockview-core/src/splitview/splitview.scss index c46ceb250..1f0df7ca9 100644 --- a/packages/dockview-core/src/splitview/splitview.scss +++ b/packages/dockview-core/src/splitview/splitview.scss @@ -106,6 +106,7 @@ -webkit-user-select: none; // Safari -moz-user-select: none; // Firefox -ms-user-select: none; // IE 10 and IE 11 + touch-action: none; &:active { transition: background-color 0.1s ease-in-out; diff --git a/packages/dockview-core/src/splitview/splitview.ts b/packages/dockview-core/src/splitview/splitview.ts index 48295a8c7..007a8e60a 100644 --- a/packages/dockview-core/src/splitview/splitview.ts +++ b/packages/dockview-core/src/splitview/splitview.ts @@ -393,7 +393,7 @@ export class Splitview { const sash = document.createElement('div'); sash.className = 'sash'; - const onStart = (event: MouseEvent) => { + const onPointerStart = (event: PointerEvent) => { for (const item of this.viewItems) { item.enabled = false; } @@ -486,13 +486,12 @@ export class Splitview { size: snappedViewItem.size, }; } - // - const mousemove = (mousemoveEvent: MouseEvent) => { + const onPointerMove = (event: PointerEvent) => { const current = this._orientation === Orientation.HORIZONTAL - ? mousemoveEvent.clientX - : mousemoveEvent.clientY; + ? event.clientX + : event.clientY; const delta = current - start; this.resize( @@ -521,24 +520,24 @@ export class Splitview { this.saveProportions(); - document.removeEventListener('mousemove', mousemove); - document.removeEventListener('mouseup', end); - document.removeEventListener('mouseend', end); + document.removeEventListener('pointermove', onPointerMove); + document.removeEventListener('pointerup', end); + document.removeEventListener('pointercancel', end); this._onDidSashEnd.fire(undefined); }; - document.addEventListener('mousemove', mousemove); - document.addEventListener('mouseup', end); - document.addEventListener('mouseend', end); + document.addEventListener('pointermove', onPointerMove); + document.addEventListener('pointerup', end); + document.addEventListener('pointercancel', end); }; - sash.addEventListener('mousedown', onStart); + sash.addEventListener('pointerdown', onPointerStart); const sashItem: ISashItem = { container: sash, disposable: () => { - sash.removeEventListener('mousedown', onStart); + sash.removeEventListener('pointerdown', onPointerStart); this.sashContainer.removeChild(sash); }, }; diff --git a/packages/dockview-core/src/splitview/splitviewComponent.ts b/packages/dockview-core/src/splitview/splitviewComponent.ts index 37573c40e..973a81d24 100644 --- a/packages/dockview-core/src/splitview/splitviewComponent.ts +++ b/packages/dockview-core/src/splitview/splitviewComponent.ts @@ -337,6 +337,10 @@ export class SplitviewComponent const queue: Function[] = []; + // take note of the existing dimensions + const width = this.width; + const height = this.height; + this.splitview = new Splitview(this.element, { orientation, proportionalLayout: this.options.proportionalLayout, @@ -387,7 +391,7 @@ export class SplitviewComponent }, }); - this.layout(this.width, this.height); + this.layout(width, height); queue.forEach((f) => f()); diff --git a/packages/dockview/package.json b/packages/dockview/package.json index 3d0acd2ff..86c778d43 100644 --- a/packages/dockview/package.json +++ b/packages/dockview/package.json @@ -1,6 +1,6 @@ { "name": "dockview", - "version": "1.7.4", + "version": "1.7.6", "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.4" + "dockview-core": "^1.7.6" }, "devDependencies": { "@rollup/plugin-node-resolve": "^15.0.1", diff --git a/packages/docs/blog/2022-06-12-dockview-1.5.0.mdx b/packages/docs/blog/2022-06-12-dockview-1.5.0.mdx index 2eb8a31d5..bb9f9ea59 100644 --- a/packages/docs/blog/2022-06-12-dockview-1.5.0.mdx +++ b/packages/docs/blog/2022-06-12-dockview-1.5.0.mdx @@ -20,7 +20,7 @@ import Link from '@docusaurus/Link'; - Provide a default React tab implementation to allow for simple changes to tab renderer without rewritting the entire tab - Override the default tab in `ReactDockview` with the `defaultTabComponent` prop - Group controls renderer [#138](https://github.com/mathuo/dockview/pull/138) - - Provide the `groupControlComponent` prop in `ReactDockview` to create custom control components for groups. Go + - Provide the `groupControlComponent` prop in `ReactDockview` to create custom control components for groups. ## 🛠 Miscs diff --git a/packages/docs/blog/2023-06-14-dockview-1.7.4.md b/packages/docs/blog/2023-06-10-dockview-1.7.4.md similarity index 100% rename from packages/docs/blog/2023-06-14-dockview-1.7.4.md rename to packages/docs/blog/2023-06-10-dockview-1.7.4.md 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/blog/2023-06-18-dockview-1.7.6.md b/packages/docs/blog/2023-06-18-dockview-1.7.6.md new file mode 100644 index 000000000..556a52df3 --- /dev/null +++ b/packages/docs/blog/2023-06-18-dockview-1.7.6.md @@ -0,0 +1,20 @@ +--- +slug: dockview-1.7.6-release +title: Dockview 1.7.6 +tags: [release] +--- + +# Release Notes + +Please reference to docs @ [dockview.dev](https://dockview.dev). + +## 🚀 Features + +- Touch support for resize handles [#278](https://github.com/mathuo/dockview/pull/278) + +## 🛠 Miscs + +- Internal cleanup [#275](https://github.com/mathuo/dockview/pull/275) +- iframe docs [#273](https://github.com/mathuo/dockview/pull/273) + +## 🔥 Breaking changes diff --git a/packages/docs/docs/components/dockview.mdx b/packages/docs/docs/components/dockview.mdx index f686cb1e3..d2215db7f 100644 --- a/packages/docs/docs/components/dockview.mdx +++ b/packages/docs/docs/components/dockview.mdx @@ -27,6 +27,7 @@ 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 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'; @@ -740,6 +741,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. diff --git a/packages/docs/package.json b/packages/docs/package.json index 7cb47b249..b2862a1bd 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -1,6 +1,6 @@ { "name": "dockview-docs", - "version": "1.7.4", + "version": "1.7.6", "private": true, "scripts": { "docusaurus": "docusaurus", @@ -22,7 +22,7 @@ "@minoru/react-dnd-treeview": "^3.4.3", "axios": "^1.3.3", "clsx": "^1.2.1", - "dockview": "^1.7.4", + "dockview": "^1.7.6", "prism-react-renderer": "^1.3.5", "react": "^18.2.0", "react-dnd": "^16.0.1", 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/iframe-dockview/public/index.html b/packages/docs/sandboxes/iframe-dockview/public/index.html new file mode 100644 index 000000000..1f8a52426 --- /dev/null +++ b/packages/docs/sandboxes/iframe-dockview/public/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + React App + + + + +
+ + + + 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 ( +