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 5d0c99866..14afc2271 100644 --- a/lerna.json +++ b/lerna.json @@ -3,7 +3,7 @@ "packages/*" ], "useWorkspaces": true, - "version": "1.7.3", + "version": "1.7.6", "npmClient": "yarn", "command": { "publish": { diff --git a/package.json b/package.json index 3148b38c4..114afaa58 100644 --- a/package.json +++ b/package.json @@ -35,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", @@ -58,8 +59,8 @@ "style-loader": "^3.3.1", "ts-jest": "^29.0.5", "ts-loader": "^9.4.2", - "tslib": "^2.5.0", "ts-node": "^10.9.1", + "tslib": "^2.5.0", "typedoc": "^0.24.7", "typescript": "^4.9.5", "webpack": "^5.75.0", @@ -67,4 +68,4 @@ "webpack-dev-server": "^4.11.1" }, "dependencies": {} -} \ No newline at end of file +} diff --git a/packages/dockview-core/package.json b/packages/dockview-core/package.json index e8bbfe657..6627b402b 100644 --- a/packages/dockview-core/package.json +++ b/packages/dockview-core/package.json @@ -1,6 +1,6 @@ { "name": "dockview-core", - "version": "1.7.3", + "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__/__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__/groupview/panel/content.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/panel/content.spec.ts similarity index 88% rename from packages/dockview-core/src/__tests__/groupview/panel/content.spec.ts rename to packages/dockview-core/src/__tests__/dockview/components/panel/content.spec.ts index 58de628a3..34ba707a3 100644 --- a/packages/dockview-core/src/__tests__/groupview/panel/content.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/panel/content.spec.ts @@ -1,14 +1,14 @@ import { fireEvent } from '@testing-library/dom'; -import { Emitter, Event } from '../../../events'; -import { ContentContainer } from '../../../dockview/components/panel/content'; +import { Emitter, Event } from '../../../../events'; +import { ContentContainer } from '../../../../dockview/components/panel/content'; import { GroupPanelContentPartInitParameters, IContentRenderer, -} from '../../../dockview/types'; -import { CompositeDisposable } from '../../../lifecycle'; -import { PanelUpdateEvent } from '../../../panel/types'; -import { IDockviewPanel } from '../../../dockview/dockviewPanel'; -import { IDockviewPanelModel } from '../../../dockview/dockviewPanelModel'; +} from '../../../../dockview/types'; +import { CompositeDisposable } from '../../../../lifecycle'; +import { PanelUpdateEvent } from '../../../../panel/types'; +import { IDockviewPanel } from '../../../../dockview/dockviewPanel'; +import { IDockviewPanelModel } from '../../../../dockview/dockviewPanelModel'; class TestContentRenderer extends CompositeDisposable diff --git a/packages/dockview-core/src/__tests__/groupview/tab.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts similarity index 95% rename from packages/dockview-core/src/__tests__/groupview/tab.spec.ts rename to packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts index 012732e73..2aa671d00 100644 --- a/packages/dockview-core/src/__tests__/groupview/tab.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts @@ -1,9 +1,9 @@ import { fireEvent } from '@testing-library/dom'; -import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer'; -import { DockviewComponent } from '../../dockview/dockviewComponent'; -import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; -import { DockviewGroupPanelModel } from '../../dockview/dockviewGroupPanelModel'; -import { Tab } from '../../dockview/components/tab/tab'; +import { LocalSelectionTransfer, PanelTransfer } from '../../../dnd/dataTransfer'; +import { DockviewComponent } from '../../../dockview/dockviewComponent'; +import { DockviewGroupPanel } from '../../../dockview/dockviewGroupPanel'; +import { DockviewGroupPanelModel } from '../../../dockview/dockviewGroupPanelModel'; +import { Tab } from '../../../dockview/components/tab/tab'; describe('tab', () => { test('that empty tab has inactive-tab class', () => { diff --git a/packages/dockview-core/src/__tests__/groupview/titlebar/tabsContainer.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts similarity index 68% rename from packages/dockview-core/src/__tests__/groupview/titlebar/tabsContainer.spec.ts rename to packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts index 0bcc96f50..77efaca4c 100644 --- a/packages/dockview-core/src/__tests__/groupview/titlebar/tabsContainer.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts @@ -1,13 +1,13 @@ -import { DockviewComponent } from '../../../dockview/dockviewComponent'; -import { TabsContainer } from '../../../dockview/components/titlebar/tabsContainer'; -import { fireEvent } from '@testing-library/dom'; import { LocalSelectionTransfer, PanelTransfer, -} from '../../../dnd/dataTransfer'; -import { TestPanel } from '../dockviewGroupPanelModel.spec'; -import { DockviewGroupPanelModel } from '../../../dockview/dockviewGroupPanelModel'; -import { DockviewGroupPanel } from '../../../dockview/dockviewGroupPanel'; +} from '../../../../dnd/dataTransfer'; +import { TabsContainer } from '../../../../dockview/components/titlebar/tabsContainer'; +import { DockviewComponent } from '../../../../dockview/dockviewComponent'; +import { DockviewGroupPanel } from '../../../../dockview/dockviewGroupPanel'; +import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanelModel'; +import { fireEvent } from '@testing-library/dom'; +import { TestPanel } from '../../dockviewGroupPanelModel.spec'; describe('tabsContainer', () => { test('that an external event does not render a drop target and calls through to the group mode', () => { @@ -331,4 +331,136 @@ describe('tabsContainer', () => { cut.element.getElementsByClassName('drop-target-dropzone').length ).toBe(0); }); + + test('left actions', () => { + const accessorMock = jest.fn(() => { + return (>{ + options: {}, + onDidAddPanel: jest.fn(), + onDidRemovePanel: jest.fn(), + }) as DockviewComponent; + }); + + const groupPanelMock = jest.fn(() => { + return (>{}) as DockviewGroupPanel; + }); + + const accessor = new accessorMock(); + const groupPanel = new groupPanelMock(); + + const cut = new TabsContainer(accessor, groupPanel); + + let query = cut.element.querySelectorAll( + '.tabs-and-actions-container > .left-actions-container' + ); + + expect(query.length).toBe(1); + expect(query[0].children.length).toBe(0); + + // add left action + + const left = document.createElement('div'); + left.className = 'test-left-actions-element'; + cut.setLeftActionsElement(left); + + query = cut.element.querySelectorAll( + '.tabs-and-actions-container > .left-actions-container' + ); + expect(query.length).toBe(1); + expect(query[0].children.item(0)?.className).toBe( + 'test-left-actions-element' + ); + expect(query[0].children.length).toBe(1); + + // add left action + + const left2 = document.createElement('div'); + left2.className = 'test-left-actions-element-2'; + cut.setLeftActionsElement(left2); + + query = cut.element.querySelectorAll( + '.tabs-and-actions-container > .left-actions-container' + ); + expect(query.length).toBe(1); + expect(query[0].children.item(0)?.className).toBe( + 'test-left-actions-element-2' + ); + expect(query[0].children.length).toBe(1); + + // remove left action + + cut.setLeftActionsElement(undefined); + query = cut.element.querySelectorAll( + '.tabs-and-actions-container > .left-actions-container' + ); + + expect(query.length).toBe(1); + expect(query[0].children.length).toBe(0); + }); + + test('right actions', () => { + const accessorMock = jest.fn(() => { + return (>{ + options: {}, + onDidAddPanel: jest.fn(), + onDidRemovePanel: jest.fn(), + }) as DockviewComponent; + }); + + const groupPanelMock = jest.fn(() => { + return (>{}) as DockviewGroupPanel; + }); + + const accessor = new accessorMock(); + const groupPanel = new groupPanelMock(); + + const cut = new TabsContainer(accessor, groupPanel); + + let query = cut.element.querySelectorAll( + '.tabs-and-actions-container > .right-actions-container' + ); + + expect(query.length).toBe(1); + expect(query[0].children.length).toBe(0); + + // add right action + + const right = document.createElement('div'); + right.className = 'test-right-actions-element'; + cut.setRightActionsElement(right); + + query = cut.element.querySelectorAll( + '.tabs-and-actions-container > .right-actions-container' + ); + expect(query.length).toBe(1); + expect(query[0].children.item(0)?.className).toBe( + 'test-right-actions-element' + ); + expect(query[0].children.length).toBe(1); + + // add right action + + const right2 = document.createElement('div'); + right2.className = 'test-right-actions-element-2'; + cut.setRightActionsElement(right2); + + query = cut.element.querySelectorAll( + '.tabs-and-actions-container > .right-actions-container' + ); + expect(query.length).toBe(1); + expect(query[0].children.item(0)?.className).toBe( + 'test-right-actions-element-2' + ); + expect(query[0].children.length).toBe(1); + + // remove right action + + cut.setRightActionsElement(undefined); + query = cut.element.querySelectorAll( + '.tabs-and-actions-container > .right-actions-container' + ); + + expect(query.length).toBe(1); + expect(query[0].children.length).toBe(0); + }); }); 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__/groupview/dockviewGroupPanelModel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts similarity index 99% rename from packages/dockview-core/src/__tests__/groupview/dockviewGroupPanelModel.spec.ts rename to packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts index b00522d05..249167341 100644 --- a/packages/dockview-core/src/__tests__/groupview/dockviewGroupPanelModel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/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__/dockview/dockviewPanel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewPanel.spec.ts index d87ac438b..c67546db8 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewPanel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewPanel.spec.ts @@ -43,7 +43,7 @@ describe('dockviewPanel', () => { 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__/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/api/dockviewPanelApi.ts b/packages/dockview-core/src/api/dockviewPanelApi.ts index d42ac65a4..5c6222071 100644 --- a/packages/dockview-core/src/api/dockviewPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewPanelApi.ts @@ -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 795ac1589..95d35a3b8 100644 --- a/packages/dockview-core/src/api/panelApi.ts +++ b/packages/dockview-core/src/api/panelApi.ts @@ -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 d7569f14f..2d9ce2cfa 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(); } @@ -32,25 +35,33 @@ export abstract class DragHandler extends CompositeDisposable { this.addDisposables( this._onDragStart, addDisposableListener(this.el, 'dragstart', (event) => { - if (this.isCancelled(event)) { - event.preventDefault(); - return; - } + if (this.isCancelled(event)) { + event.preventDefault(); + return; + } - this.disposable.value = this.getData(event.dataTransfer); - - 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.dataDisposable.value = this.getData(event.dataTransfer); + if (event.dataTransfer) { event.dataTransfer.effectAllowed = 'move'; @@ -70,12 +81,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/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index 68d8cfe1b..b0ce14fc8 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -28,7 +28,8 @@ export interface ITabsContainer extends IDisposable { isActive: (tab: ITab) => boolean; closePanel: (panel: IDockviewPanel) => void; openPanel: (panel: IDockviewPanel, index?: number) => void; - setActionElement(element: HTMLElement | undefined): void; + setRightActionsElement(element: HTMLElement | undefined): void; + setLeftActionsElement(element: HTMLElement | undefined): void; hidden: boolean; show(): void; hide(): void; @@ -40,12 +41,14 @@ export class TabsContainer { private readonly _element: HTMLElement; private readonly tabContainer: HTMLElement; - private readonly actionContainer: HTMLElement; + private readonly rightActionsContainer: HTMLElement; + private readonly leftActionsContainer: HTMLElement; private readonly voidContainer: VoidContainer; private tabs: IValueDisposable[] = []; private selectedIndex = -1; - private actions: HTMLElement | undefined; + private rightActions: HTMLElement | undefined; + private leftActions: HTMLElement | undefined; private _hidden = false; @@ -79,17 +82,31 @@ export class TabsContainer this._element.style.display = 'none'; } - setActionElement(element: HTMLElement | undefined): void { - if (this.actions === element) { + setRightActionsElement(element: HTMLElement | undefined): void { + if (this.rightActions === element) { return; } - if (this.actions) { - this.actions.remove(); - this.actions = undefined; + if (this.rightActions) { + this.rightActions.remove(); + this.rightActions = undefined; } if (element) { - this.actionContainer.appendChild(element); - this.actions = element; + this.rightActionsContainer.appendChild(element); + this.rightActions = element; + } + } + + setLeftActionsElement(element: HTMLElement | undefined): void { + if (this.leftActions === element) { + return; + } + if (this.leftActions) { + this.leftActions.remove(); + this.leftActions = undefined; + } + if (element) { + this.leftActionsContainer.appendChild(element); + this.leftActions = element; } } @@ -146,8 +163,11 @@ export class TabsContainer }) ); - this.actionContainer = document.createElement('div'); - this.actionContainer.className = 'action-container'; + this.rightActionsContainer = document.createElement('div'); + this.rightActionsContainer.className = 'right-actions-container'; + + this.leftActionsContainer = document.createElement('div'); + this.leftActionsContainer.className = 'left-actions-container'; this.tabContainer = document.createElement('div'); this.tabContainer.className = 'tabs-container'; @@ -155,8 +175,9 @@ export class TabsContainer this.voidContainer = new VoidContainer(this.accessor, this.group); this._element.appendChild(this.tabContainer); + this._element.appendChild(this.leftActionsContainer); this._element.appendChild(this.voidContainer.element); - this._element.appendChild(this.actionContainer); + this._element.appendChild(this.rightActionsContainer); this.addDisposables( this.voidContainer, diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 39f334bc7..10e58eed1 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -73,7 +73,8 @@ export type DockviewComponentUpdateOptions = Pick< | 'showDndOverlay' | 'watermarkFrameworkComponent' | 'defaultTabComponent' - | 'createGroupControlElement' + | 'createLeftHeaderActionsElement' + | 'createRightHeaderActionsElement' >; export interface DockviewDropEvent extends GroupviewDropEvent { @@ -500,6 +501,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; @@ -541,6 +546,8 @@ export class DockviewComponent }, }); + this.layout(width, height); + if (typeof activeGroup === 'string') { const panel = this.getPanel(activeGroup); if (panel) { @@ -548,8 +555,6 @@ export class DockviewComponent } } - this.gridview.layout(this.width, this.height); - this._onDidLayoutFromJSON.fire(); } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 4c05cefef..e68cc2e53 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -18,7 +18,7 @@ import { import { DockviewDropTargets, IWatermarkRenderer } from './types'; import { DockviewGroupPanel } from './dockviewGroupPanel'; import { IDockviewPanel } from './dockviewPanel'; -import { IGroupControlRenderer } from './options'; +import { IHeaderActionsRenderer } from './options'; export interface DndService { canDisplayOverlay( @@ -137,8 +137,9 @@ export class DockviewGroupPanelModel private watermark?: IWatermarkRenderer; private _isGroupActive = false; private _locked = false; - private _control: IGroupControlRenderer | undefined; private _isFloating = false; + private _rightHeaderActions: IHeaderActionsRenderer | undefined; + private _leftHeaderActions: IHeaderActionsRenderer | undefined; private mostRecentlyUsed: IDockviewPanel[] = []; @@ -334,16 +335,34 @@ export class DockviewGroupPanelModel this.setActive(this.isActive, true, true); this.updateContainer(); - if (this.accessor.options.createGroupControlElement) { - this._control = this.accessor.options.createGroupControlElement( - this.groupPanel - ); - this.addDisposables(this._control); - this._control.init({ + if (this.accessor.options.createRightHeaderActionsElement) { + this._rightHeaderActions = + this.accessor.options.createRightHeaderActionsElement( + this.groupPanel + ); + this.addDisposables(this._rightHeaderActions); + this._rightHeaderActions.init({ containerApi: new DockviewApi(this.accessor), api: this.groupPanel.api, }); - this.tabsContainer.setActionElement(this._control.element); + this.tabsContainer.setRightActionsElement( + this._rightHeaderActions.element + ); + } + + if (this.accessor.options.createLeftHeaderActionsElement) { + this._leftHeaderActions = + this.accessor.options.createLeftHeaderActionsElement( + this.groupPanel + ); + this.addDisposables(this._leftHeaderActions); + this._leftHeaderActions.init({ + containerApi: new DockviewApi(this.accessor), + api: this.groupPanel.api, + }); + this.tabsContainer.setLeftActionsElement( + this._leftHeaderActions.element + ); } } @@ -526,7 +545,7 @@ export class DockviewGroupPanelModel } updateActions(element: HTMLElement | undefined): void { - this.tabsContainer.setActionElement(element); + this.tabsContainer.setRightActionsElement(element); } public setActive( diff --git a/packages/dockview-core/src/dockview/dockviewPanel.ts b/packages/dockview-core/src/dockview/dockviewPanel.ts index cc34b1545..4f67933fc 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 @@ -117,19 +114,24 @@ 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: params.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]; + } } + // update the view with the updated props this.view.update({ params: { params: this._params, 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/options.ts b/packages/dockview-core/src/dockview/options.ts index 1e9b66db1..147cc5e48 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -19,7 +19,7 @@ import { Position } from '../dnd/droptarget'; import { IDockviewPanel } from './dockviewPanel'; import { FrameworkFactory } from '../panel/componentFactory'; -export interface IGroupControlRenderer extends IDisposable { +export interface IHeaderActionsRenderer extends IDisposable { readonly element: HTMLElement; init(params: { containerApi: DockviewApi; @@ -79,9 +79,12 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions { styles?: ISplitviewStyles; defaultTabComponent?: string; showDndOverlay?: (event: DockviewDndOverlayEvent) => boolean; - createGroupControlElement?: ( + createRightHeaderActionsElement?: ( group: DockviewGroupPanel - ) => IGroupControlRenderer; + ) => IHeaderActionsRenderer; + createLeftHeaderActionsElement?: ( + group: DockviewGroupPanel + ) => IHeaderActionsRenderer; singleTabMode?: 'fullwidth' | 'default'; parentElement?: HTMLElement; } 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/events.ts b/packages/dockview-core/src/events.ts index 13b8b3382..cb8d95930 100644 --- a/packages/dockview-core/src/events.ts +++ b/packages/dockview-core/src/events.ts @@ -102,10 +102,10 @@ export class Emitter implements IDisposable { if (index > -1) { this._listeners.splice(index, 1); } else if (Emitter.ENABLE_TRACKING) { - console.warn( - `Listener already disposed`, - Stacktrace.create().print() - ); + // console.warn( + // `Listener already disposed`, + // Stacktrace.create().print() + // ); } }, }; @@ -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/basePanelView.ts b/packages/dockview-core/src/gridview/basePanelView.ts index f61473864..4244fef36 100644 --- a/packages/dockview-core/src/gridview/basePanelView.ts +++ b/packages/dockview-core/src/gridview/basePanelView.ts @@ -104,6 +104,7 @@ export abstract class BasePanelView } update(event: PanelUpdateEvent): void { + // merge the new parameters with the existing parameters this._params = { ...this._params, params: { @@ -111,6 +112,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 }); } 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/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 975e2e48c..86c778d43 100644 --- a/packages/dockview/package.json +++ b/packages/dockview/package.json @@ -1,6 +1,6 @@ { "name": "dockview", - "version": "1.7.3", + "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.3" + "dockview-core": "^1.7.6" }, "devDependencies": { "@rollup/plugin-node-resolve": "^15.0.1", 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__/dockview/groupControlsRenderer.spec.ts b/packages/dockview/src/__tests__/dockview/headerActionsRenderer.spec.ts similarity index 88% rename from packages/dockview/src/__tests__/dockview/groupControlsRenderer.spec.ts rename to packages/dockview/src/__tests__/dockview/headerActionsRenderer.spec.ts index dc645d007..ec6015403 100644 --- a/packages/dockview/src/__tests__/dockview/groupControlsRenderer.spec.ts +++ b/packages/dockview/src/__tests__/dockview/headerActionsRenderer.spec.ts @@ -3,9 +3,9 @@ import { DockviewGroupPanelApi, DockviewGroupPanelModel, } from 'dockview-core'; -import { ReactGroupControlsRendererPart } from '../../dockview/groupControlsRenderer'; +import { ReactHeaderActionsRendererPart } from '../../dockview/headerActionsRenderer'; -describe('groupControlsRenderer', () => { +describe('headerActionsRenderer', () => { test('#1', () => { const groupviewMock = jest.fn, []>( () => { @@ -28,7 +28,7 @@ describe('groupControlsRenderer', () => { const groupPanel = new groupPanelMock() as DockviewGroupPanel; - const cut = new ReactGroupControlsRendererPart( + const cut = new ReactHeaderActionsRendererPart( jest.fn(), { addPortal: jest.fn(), 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/src/dockview/dockview.tsx b/packages/dockview/src/dockview/dockview.tsx index 8998c770d..b741a61d3 100644 --- a/packages/dockview/src/dockview/dockview.tsx +++ b/packages/dockview/src/dockview/dockview.tsx @@ -4,12 +4,12 @@ import { DockviewDropEvent, DockviewDndOverlayEvent, GroupPanelFrameworkComponentFactory, - IGroupControlRenderer, DockviewPanelApi, DockviewApi, IContentRenderer, ITabRenderer, DockviewGroupPanel, + IHeaderActionsRenderer, } from 'dockview-core'; import { ReactPanelContentPart } from './reactContentPart'; import { ReactPanelHeaderPart } from './reactHeaderPart'; @@ -18,17 +18,17 @@ import { ReactPortalStore, usePortalsLifecycle } from '../react'; import { IWatermarkPanelProps, ReactWatermarkPart } from './reactWatermarkPart'; import { PanelCollection, PanelParameters } from '../types'; import { - IDockviewGroupControlProps, - ReactGroupControlsRendererPart, -} from './groupControlsRenderer'; + IDockviewHeaderActionsProps, + ReactHeaderActionsRendererPart, +} from './headerActionsRenderer'; function createGroupControlElement( - component: React.FunctionComponent | undefined, + component: React.FunctionComponent | undefined, store: ReactPortalStore -): ((groupPanel: DockviewGroupPanel) => IGroupControlRenderer) | undefined { +): ((groupPanel: DockviewGroupPanel) => IHeaderActionsRenderer) | undefined { return component ? (groupPanel: DockviewGroupPanel) => { - return new ReactGroupControlsRendererPart( + return new ReactHeaderActionsRendererPart( component, store, groupPanel @@ -65,7 +65,8 @@ export interface IDockviewReactProps { className?: string; disableAutoResizing?: boolean; defaultTabComponent?: React.FunctionComponent; - groupControlComponent?: React.FunctionComponent; + rightHeaderActionsComponent?: React.FunctionComponent; + leftHeaderActionsComponent?: React.FunctionComponent; singleTabMode?: 'fullwidth' | 'default'; } @@ -150,10 +151,15 @@ export const DockviewReact = React.forwardRef( ? { separatorBorder: 'transparent' } : undefined, showDndOverlay: props.showDndOverlay, - createGroupControlElement: createGroupControlElement( - props.groupControlComponent, + createLeftHeaderActionsElement: createGroupControlElement( + props.leftHeaderActionsComponent, { addPortal } ), + createRightHeaderActionsElement: createGroupControlElement( + props.rightHeaderActionsComponent, + { addPortal } + ), + singleTabMode: props.singleTabMode, }); @@ -250,12 +256,24 @@ export const DockviewReact = React.forwardRef( return; } dockviewRef.current.updateOptions({ - createGroupControlElement: createGroupControlElement( - props.groupControlComponent, + createRightHeaderActionsElement: createGroupControlElement( + props.rightHeaderActionsComponent, { addPortal } ), }); - }, [props.groupControlComponent]); + }, [props.rightHeaderActionsComponent]); + + React.useEffect(() => { + if (!dockviewRef.current) { + return; + } + dockviewRef.current.updateOptions({ + createLeftHeaderActionsElement: createGroupControlElement( + props.leftHeaderActionsComponent, + { addPortal } + ), + }); + }, [props.leftHeaderActionsComponent]); return (
; + private _part?: ReactPart; get element(): HTMLElement { return this._element; } - get part(): ReactPart | undefined { + get part(): ReactPart | undefined { return this._part; } @@ -36,7 +37,7 @@ export class ReactGroupControlsRendererPart { } constructor( - private readonly component: React.FunctionComponent, + private readonly component: React.FunctionComponent, private readonly reactPortalStore: ReactPortalStore, private readonly _group: DockviewGroupPanel ) { @@ -77,6 +78,7 @@ export class ReactGroupControlsRendererPart { panels: this._group.model.panels, activePanel: this._group.model.activePanel, isGroupActive: this._group.api.isActive, + group: this._group, } ); } diff --git a/packages/dockview/src/index.ts b/packages/dockview/src/index.ts index acc7fec37..0f5e688b7 100644 --- a/packages/dockview/src/index.ts +++ b/packages/dockview/src/index.ts @@ -4,7 +4,7 @@ export * from './dockview/dockview'; export * from './dockview/defaultTab'; export * from './splitview/splitview'; export * from './gridview/gridview'; -export { IDockviewGroupControlProps } from './dockview/groupControlsRenderer'; +export { IDockviewHeaderActionsProps } from './dockview/headerActionsRenderer'; export { IWatermarkPanelProps } from './dockview/reactWatermarkPart'; export * from './paneview/paneview'; export * from './types'; 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-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/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 835dee189..d2215db7f 100644 --- a/packages/docs/docs/components/dockview.mdx +++ b/packages/docs/docs/components/dockview.mdx @@ -18,7 +18,7 @@ import DockviewConstraints from '@site/sandboxes/constraints-dockview/src/app'; import DndDockview from '@site/sandboxes/dnd-dockview/src/app'; import NestedDockview from '@site/sandboxes/nested-dockview/src/app'; import EventsDockview from '@site/sandboxes/events-dockview/src/app'; -import DockviewGroupControl from '@site/sandboxes/groupcontrol-dockview/src/app'; +import DockviewGroupControl from '@site/sandboxes/headeractions-dockview/src/app'; import CustomHeadersDockview from '@site/sandboxes/customheader-dockview/src/app'; import DockviewNative from '@site/sandboxes/fullwidthtab-dockview/src/app'; import DockviewNative2 from '@site/sandboxes/nativeapp-dockview/src/app'; @@ -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'; @@ -58,20 +59,21 @@ You can create a Dockview through the use of the `DockviewReact` component. import { DockviewReact } from 'dockview'; ``` -| Property | Type | Optional | Default | Description | -| --------------------- | ------------------------------------ | -------- | --------- | ------------------------------------------------------------ | -| onReady | (event: SplitviewReadyEvent) => void | No | | | -| components | object | No | | | -| tabComponents | object | Yes | | | -| watermarkComponent | object | Yes | | | -| hideBorders | boolean | Yes | false | | -| className | string | Yes | '' | | -| disableAutoResizing | boolean | Yes | false | See Auto Resizing | -| onDidDrop | Event | Yes | false | | -| showDndOverlay | Event | Yes | false | | -| defaultTabComponent | object | Yes | | | -| groupControlComponent | object | Yes | | | -| singleTabMode | 'fullwidth' \| 'default' | Yes | 'default' | | +| Property | Type | Optional | Default | Description | +| --------------------------- | ------------------------------------ | -------- | --------- | ------------------------------------------------------------ | +| onReady | (event: SplitviewReadyEvent) => void | No | | | +| components | object | No | | | +| tabComponents | object | Yes | | | +| watermarkComponent | object | Yes | | | +| hideBorders | boolean | Yes | false | | +| className | string | Yes | '' | | +| disableAutoResizing | boolean | Yes | false | See Auto Resizing | +| onDidDrop | Event | Yes | false | | +| showDndOverlay | Event | Yes | false | | +| defaultTabComponent | object | Yes | | | +| leftHeaderActionsComponent | object | Yes | | | +| rightHeaderActionsComponent | object | Yes | | | +| singleTabMode | 'fullwidth' \| 'default' | Yes | 'default' | | ## Dockview API @@ -435,6 +437,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, @@ -648,22 +684,22 @@ panel.group.locked = true; ### Group Controls Panel -`DockviewReact` accepts a prop `groupControlComponent` which expects a React component whos props are `IDockviewGroupControlProps`. -This control will be rendered inside the header bar on the right hand side for each group of tabs. +`DockviewReact` accepts `leftHeaderActionsComponent` and `rightHeaderActionsComponent` which expect a React component with props `IDockviewHeaderActionsProps`. +These controls are rendered of the left and right side of the space to the right of the tabs in the header bar. ```tsx -const Component: React.FunctionComponent = () => { +const Component: React.FunctionComponent = () => { return
{'...'}
; }; -return ; +return ; ``` As a simple example the below uses the `groupControlComponent` to render a small control that indicates whether the group is active and which panel is active in that group. ```tsx -const GroupControlComponent = (props: IDockviewGroupControlProps) => { +const RightHeaderActionsComponent = (props: IDockviewHeaderActionsProps) => { const isGroupActive = props.isGroupActive; const activePanel = props.activePanel; @@ -705,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 a1a77a4bb..b2862a1bd 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -1,6 +1,6 @@ { "name": "dockview-docs", - "version": "1.7.3", + "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.3", + "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/demo-dockview/src/app.tsx b/packages/docs/sandboxes/demo-dockview/src/app.tsx index 92ef2561a..790ef29be 100644 --- a/packages/docs/sandboxes/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/demo-dockview/src/app.tsx @@ -4,7 +4,7 @@ import { DockviewReadyEvent, IDockviewPanelHeaderProps, IDockviewPanelProps, - IDockviewGroupControlProps, + IDockviewHeaderActionsProps, } from 'dockview'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; @@ -156,7 +156,7 @@ const groupControlsComponents = { }, }; -const GroupControls = (props: IDockviewGroupControlProps) => { +const RightControls = (props: IDockviewHeaderActionsProps) => { const Component = React.useMemo(() => { if (!props.isGroupActive || !props.activePanel) { return null; @@ -183,6 +183,36 @@ const GroupControls = (props: IDockviewGroupControlProps) => { ); }; +let counter = 0; + +const LeftControls = (props: IDockviewHeaderActionsProps) => { + const onClick = () => { + props.containerApi.addPanel({ + id: `id_${Date.now().toString()}`, + component: 'default', + title: `Tab ${counter++}`, + position: { + referenceGroup: props.group, + }, + }); + }; + + return ( +
+ +
+ ); +}; + const DockviewDemo = () => { const onReady = (event: DockviewReadyEvent) => { event.api.addPanel({ @@ -218,8 +248,6 @@ const DockviewDemo = () => { title: 'Panel 6', position: { referencePanel: 'panel_4', direction: 'below' }, }); - // panel6.group.locked = true; - // panel6.group.header.hidden = true; event.api.addPanel({ id: 'panel_7', component: 'default', @@ -233,8 +261,6 @@ const DockviewDemo = () => { position: { referencePanel: 'panel_7', direction: 'within' }, }); - // event.api.addGroup(); - event.api.getPanel('panel_1')!.api.setActive(); }; @@ -247,7 +273,8 @@ const DockviewDemo = () => { diff --git a/packages/docs/sandboxes/headeractions-dockview/package.json b/packages/docs/sandboxes/headeractions-dockview/package.json new file mode 100644 index 000000000..27f907944 --- /dev/null +++ b/packages/docs/sandboxes/headeractions-dockview/package.json @@ -0,0 +1,32 @@ +{ + "name": "headeractions-dockview", + "description": "", + "keywords": [ + "dockview" + ], + "version": "1.0.0", + "main": "src/index.tsx", + "dependencies": { + "dockview": "*", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.0.28", + "@types/react-dom": "^18.0.11", + "typescript": "^4.9.5", + "react-scripts": "*" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ] +} diff --git a/packages/docs/sandboxes/groupcontrol-dockview/public/index.html b/packages/docs/sandboxes/headeractions-dockview/public/index.html similarity index 100% rename from packages/docs/sandboxes/groupcontrol-dockview/public/index.html rename to packages/docs/sandboxes/headeractions-dockview/public/index.html diff --git a/packages/docs/sandboxes/groupcontrol-dockview/src/app.scss b/packages/docs/sandboxes/headeractions-dockview/src/app.scss similarity index 100% rename from packages/docs/sandboxes/groupcontrol-dockview/src/app.scss rename to packages/docs/sandboxes/headeractions-dockview/src/app.scss diff --git a/packages/docs/sandboxes/groupcontrol-dockview/src/app.tsx b/packages/docs/sandboxes/headeractions-dockview/src/app.tsx similarity index 86% rename from packages/docs/sandboxes/groupcontrol-dockview/src/app.tsx rename to packages/docs/sandboxes/headeractions-dockview/src/app.tsx index fc4c868eb..059dd9448 100644 --- a/packages/docs/sandboxes/groupcontrol-dockview/src/app.tsx +++ b/packages/docs/sandboxes/headeractions-dockview/src/app.tsx @@ -1,7 +1,7 @@ import { DockviewReact, DockviewReadyEvent, - IDockviewGroupControlProps, + IDockviewHeaderActionsProps, IDockviewPanelProps, } from 'dockview'; import * as React from 'react'; @@ -26,9 +26,8 @@ const components = { }, }; -const GroupControlComponent = (props: IDockviewGroupControlProps) => { +const RightHeaderActions = (props: IDockviewHeaderActionsProps) => { const isGroupActive = props.isGroupActive; - const activePanel = props.activePanel; return (
@@ -40,6 +39,15 @@ const GroupControlComponent = (props: IDockviewGroupControlProps) => { > {isGroupActive ? 'Group Active' : 'Group Inactive'} +
+ ); +}; + +const LeftHeaderActions = (props: IDockviewHeaderActionsProps) => { + const activePanel = props.activePanel; + + return ( +
{`activePanel: ${ activePanel?.id || 'null' }`} @@ -87,7 +95,8 @@ const DockviewGroupControl = () => { ); diff --git a/packages/docs/sandboxes/groupcontrol-dockview/src/index.tsx b/packages/docs/sandboxes/headeractions-dockview/src/index.tsx similarity index 100% rename from packages/docs/sandboxes/groupcontrol-dockview/src/index.tsx rename to packages/docs/sandboxes/headeractions-dockview/src/index.tsx diff --git a/packages/docs/sandboxes/groupcontrol-dockview/src/styles.css b/packages/docs/sandboxes/headeractions-dockview/src/styles.css similarity index 100% rename from packages/docs/sandboxes/groupcontrol-dockview/src/styles.css rename to packages/docs/sandboxes/headeractions-dockview/src/styles.css diff --git a/packages/docs/sandboxes/groupcontrol-dockview/tsconfig.json b/packages/docs/sandboxes/headeractions-dockview/tsconfig.json similarity index 100% rename from packages/docs/sandboxes/groupcontrol-dockview/tsconfig.json rename to packages/docs/sandboxes/headeractions-dockview/tsconfig.json diff --git a/packages/docs/sandboxes/groupcontrol-dockview/package.json b/packages/docs/sandboxes/iframe-dockview/package.json similarity index 94% rename from packages/docs/sandboxes/groupcontrol-dockview/package.json rename to packages/docs/sandboxes/iframe-dockview/package.json index 7c88c11f1..0a750cb5e 100644 --- a/packages/docs/sandboxes/groupcontrol-dockview/package.json +++ b/packages/docs/sandboxes/iframe-dockview/package.json @@ -1,5 +1,5 @@ { - "name": "groupcontrol-dockview", + "name": "iframe-dockview", "description": "", "keywords": [ "dockview" 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 ( +