Merge branch 'master' of https://github.com/mathuo/dockview into 230-explore-floating-groups

This commit is contained in:
mathuo 2023-06-21 20:19:45 +01:00
commit c53d2690c3
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
89 changed files with 3020 additions and 658 deletions

View File

@ -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"
}
}

View File

@ -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

View File

@ -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

View File

@ -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') }}

View File

@ -3,7 +3,7 @@
"packages/*"
],
"useWorkspaces": true,
"version": "1.7.3",
"version": "1.7.6",
"npmClient": "yarn",
"command": {
"publish": {

View File

@ -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": {}
}
}

View File

@ -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",

View File

@ -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 {
//
}

View File

@ -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<IPanel>;
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);

View File

@ -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<DockviewPanel, []>(() => {
return {
update: jest.fn(),
setTitle: jest.fn(),
} as any;
});
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -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<IDockviewPanel> = {
id: 'test_id',
update: jest.fn(),
};
const accessor: Partial<DockviewComponent> = {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
const groupViewPanel = new DockviewGroupPanel(
<DockviewComponent>accessor,
'',
{}
);
const cut = new DockviewPanelApiImpl(
<IDockviewPanel>groupPanel,
<DockviewGroupPanel>groupViewPanel
);
cut.updateParameters({ keyA: 'valueA' });
expect(groupPanel.update).toHaveBeenCalledWith({
params: { keyA: 'valueA' },
});
expect(groupPanel.update).toHaveBeenCalledTimes(1);
});
test('onDidGroupChange', () => {

View File

@ -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();
});
});

View File

@ -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

View File

@ -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', () => {

View File

@ -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<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{}) 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<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{}) 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);
});
});

View File

@ -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',
},
},
});
});
});

View File

@ -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
}

View File

@ -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<DockviewApi, []>(() => {
return {} as any;
});
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
};
});
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = new groupMock();
const model = <IDockviewPanelModel>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,
});
});
});

View File

@ -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<number>();
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
);
});
});

View File

@ -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);
});
});

View File

@ -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',
});
});
});

View File

@ -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,
},
],
});
});
});

View File

@ -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);
});
});

View File

@ -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',
});
});
});

View File

@ -89,7 +89,7 @@ export class DockviewPanelApiImpl
}
public setTitle(title: string): void {
this.panel.update({ params: { title } });
this.panel.setTitle(title);
}
public close(): void {

View File

@ -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,
});
}
);

View File

@ -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<void>();
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();
})
);
}

View File

@ -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<ITab>[] = [];
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,

View File

@ -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<GroupPanelViewState>) => {
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();
}

View File

@ -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(

View File

@ -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,

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;

View File

@ -102,10 +102,10 @@ export class Emitter<T> 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<K extends keyof WindowEventMap>(
return {
dispose: () => {
element.removeEventListener(type, listener);
element.removeEventListener(type, listener, options);
},
};
}
@ -177,7 +177,7 @@ export function addDisposableListener<K extends keyof HTMLElementEventMap>(
return {
dispose: () => {
element.removeEventListener(type, listener);
element.removeEventListener(type, listener, options);
},
};
}

View File

@ -104,6 +104,7 @@ export abstract class BasePanelView<T extends PanelApiImpl>
}
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<T extends PanelApiImpl>
...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 });
}

View File

@ -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 {

View File

@ -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());

View File

@ -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());

View File

@ -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;

View File

@ -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);
},
};

View File

@ -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());

View File

@ -15,6 +15,7 @@ const config: JestConfigWithTsJest = {
setupFiles: [
'<rootDir>/packages/dockview/src/__tests__/__mocks__/resizeObserver.js',
],
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
coveragePathIgnorePatterns: ['/node_modules/'],
modulePathIgnorePatterns: [
'<rootDir>/packages/dockview/src/__tests__/__mocks__',

View File

@ -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",

View File

@ -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 <div>hello world</div>;
return (
<div>
{Object.keys(props.params).map((key) => {
return (
<div
key={key}
>{`key=${key},value=${props.params[key]}`}</div>
);
})}
</div>
);
},
};
});
@ -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(
<DockviewReact components={components} onReady={onReady} />
);
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();
});
});
});

View File

@ -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<Partial<DockviewGroupPanelModel>, []>(
() => {
@ -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(),

View File

@ -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 <div>hello world</div>;
return (
<div>
{Object.keys(props.params).map((key) => {
return (
<div
key={key}
>{`key=${key},value=${props.params[key]}`}</div>
);
})}
</div>
);
},
};
});
@ -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(
<GridviewReact
orientation={Orientation.VERTICAL}
components={components}
onReady={onReady}
/>
);
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();
});
});
});

View File

@ -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 <div>hello world</div>;
return (
<div>
{Object.keys(props.params).map((key) => {
return (
<div
key={key}
>{`key=${key},value=${props.params[key]}`}</div>
);
})}
</div>
);
},
};
});
@ -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(
<PaneviewReact components={components} onReady={onReady} />
);
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();
});
});
});

View File

@ -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<IDockviewPanelProps>;
beforeEach(() => {
components = {
default: (props: IDockviewPanelProps) => {
return <div>hello world</div>;
},
};
});
test('default', () => {
let api: DockviewApi | undefined;
const onReady = (event: DockviewReadyEvent) => {
api = event.api;
};
render(<DockviewReact components={components} onReady={onReady} />);
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(<DockviewReact components={components} onReady={onReady} />);
expect(api!.width).toBe(650);
expect(api!.height).toBe(450);
});
});

View File

@ -1,58 +0,0 @@
import {
DockviewGroupPanel,
DockviewGroupPanelApi,
DockviewGroupPanelModel,
} from 'dockview-core';
import { ReactGroupControlsRendererPart } from '../../../dockview/groupControlsRenderer';
describe('groupControlsRenderer', () => {
test('#1', () => {
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
onDidActivePanelChange: jest.fn(),
};
}
);
const groupview = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
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: <any>jest.fn(),
api: <any>{
onDidActiveChange: jest.fn(),
},
});
const update = jest.fn();
jest.spyOn(cut.part!, 'update').mockImplementation(update);
cut.update({ params: { valueA: 'A' } });
expect(update).toBeCalledWith({ valueA: 'A' });
});
});

View File

@ -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<IGridviewPanelProps>;
beforeEach(() => {
components = {
default: (props: IGridviewPanelProps) => {
return <div>hello world</div>;
},
};
});
test('default', () => {
let api: GridviewApi | undefined;
const onReady = (event: GridviewReadyEvent) => {
api = event.api;
};
render(
<GridviewReact
orientation={Orientation.VERTICAL}
components={components}
onReady={onReady}
/>
);
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(
<GridviewReact
orientation={Orientation.VERTICAL}
components={components}
onReady={onReady}
/>
);
expect(api!.width).toBe(650);
expect(api!.height).toBe(450);
});
});

View File

@ -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<IPaneviewPanelProps>;
beforeEach(() => {
components = {
default: (props: IPaneviewPanelProps) => {
return <div>hello world</div>;
},
};
});
test('default', () => {
let api: PaneviewApi | undefined;
const onReady = (event: PaneviewReadyEvent) => {
api = event.api;
};
render(<PaneviewReact components={components} onReady={onReady} />);
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(<PaneviewReact components={components} onReady={onReady} />);
expect(api!.width).toBe(650);
expect(api!.height).toBe(450);
});
});

View File

@ -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<TestInterface>;
const onReady = (_api: ReactPart<TestInterface>) => {
api = _api;
};
render(<TestWrapper onReady={onReady} component={Component} />);
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 (
<div>
<div data-testid="valueA">{props.valueA}</div>
<div data-testid="valueB">{props.valueB}</div>
</div>
);
};
const TestWrapper = (props: {
component: React.FunctionComponent<TestInterface>;
onReady: (api: ReactPart<TestInterface>) => void;
}) => {
const [portal, setPortal] = React.useState<React.ReactPortal[]>([]);
const ref = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
const cut = new ReactPart<TestInterface>(
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 <div ref={ref}>{portal}</div>;
};

View File

@ -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<ISplitviewPanelProps>;
beforeEach(() => {
components = {
default: (props: ISplitviewPanelProps) => {
return <div>hello world</div>;
},
};
});
test('default', () => {
let api: SplitviewApi | undefined;
const onReady = (event: SplitviewReadyEvent) => {
api = event.api;
};
render(
<SplitviewReact
orientation={Orientation.VERTICAL}
components={components}
onReady={onReady}
/>
);
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(
<SplitviewReact
orientation={Orientation.VERTICAL}
components={components}
onReady={onReady}
/>
);
expect(api!.width).toBe(650);
expect(api!.height).toBe(450);
});
});

View File

@ -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 <div>hello world</div>;
return (
<div>
{Object.keys(props.params).map((key) => {
return (
<div
key={key}
>{`key=${key},value=${props.params[key]}`}</div>
);
})}
</div>
);
},
};
});
@ -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(
<SplitviewReact
orientation={Orientation.VERTICAL}
components={components}
onReady={onReady}
/>
);
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();
});
});
});

View File

@ -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<IDockviewGroupControlProps> | undefined,
component: React.FunctionComponent<IDockviewHeaderActionsProps> | 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<IDockviewPanelHeaderProps>;
groupControlComponent?: React.FunctionComponent<IDockviewGroupControlProps>;
rightHeaderActionsComponent?: React.FunctionComponent<IDockviewHeaderActionsProps>;
leftHeaderActionsComponent?: React.FunctionComponent<IDockviewHeaderActionsProps>;
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 (
<div

View File

@ -10,24 +10,25 @@ import {
PanelUpdateEvent,
} from 'dockview-core';
export interface IDockviewGroupControlProps {
export interface IDockviewHeaderActionsProps {
api: DockviewGroupPanelApi;
containerApi: DockviewApi;
panels: IDockviewPanel[];
activePanel: IDockviewPanel | undefined;
isGroupActive: boolean;
group: DockviewGroupPanel;
}
export class ReactGroupControlsRendererPart {
export class ReactHeaderActionsRendererPart {
private mutableDisposable = new DockviewMutableDisposable();
private _element: HTMLElement;
private _part?: ReactPart<IDockviewGroupControlProps>;
private _part?: ReactPart<IDockviewHeaderActionsProps>;
get element(): HTMLElement {
return this._element;
}
get part(): ReactPart<IDockviewGroupControlProps> | undefined {
get part(): ReactPart<IDockviewHeaderActionsProps> | undefined {
return this._part;
}
@ -36,7 +37,7 @@ export class ReactGroupControlsRendererPart {
}
constructor(
private readonly component: React.FunctionComponent<IDockviewGroupControlProps>,
private readonly component: React.FunctionComponent<IDockviewHeaderActionsProps>,
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,
}
);
}

View File

@ -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';

View File

@ -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. <Link to="../../docs/components/dockview/#group-controls-panel">Go</Link>
- Provide the `groupControlComponent` prop in `ReactDockview` to create custom control components for groups.
## 🛠 Miscs

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 <Link to="../basics/#auto-resizing">Auto Resizing</Link> |
| 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 <Link to="../basics/#auto-resizing">Auto Resizing</Link> |
| 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<IDockviewGroupControlProps> = () => {
const Component: React.FunctionComponent<IDockviewHeaderActionsProps> = () => {
return <div>{'...'}</div>;
};
return <DockviewReact {...props} groupControlComponent={Component} />;
return <DockviewReact {...props} leftHeaderActionsComponent={Component} rightHeaderActionsComponent={...} />;
```
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(...)
<DockviewConstraints />
</Container>
## 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.
<Container sandboxId="iframe-dockview" height={600}>
<DockviewWithIFrames />
</Container>
## Events
A simple example showing events fired by `dockviewz that can be interacted with.

View File

@ -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",

View File

@ -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 (
<div
className="group-control"
style={{
display: 'flex',
alignItems: 'center',
padding: '0px 8px',
height: '100%',
color: 'var(--dv-activegroup-visiblepanel-tab-color)',
}}
>
<Icon onClick={onClick} icon="add" />
</div>
);
};
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 = () => {
<DockviewReact
components={components}
defaultTabComponent={headerComponents.default}
groupControlComponent={GroupControls}
rightHeaderActionsComponent={RightControls}
leftHeaderActionsComponent={LeftControls}
onReady={onReady}
className={theme}
/>

View File

@ -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"
]
}

View File

@ -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 (
<div className="dockview-groupcontrol-demo">
@ -40,6 +39,15 @@ const GroupControlComponent = (props: IDockviewGroupControlProps) => {
>
{isGroupActive ? 'Group Active' : 'Group Inactive'}
</span>
</div>
);
};
const LeftHeaderActions = (props: IDockviewHeaderActionsProps) => {
const activePanel = props.activePanel;
return (
<div className="dockview-groupcontrol-demo">
<span className="dockview-groupcontrol-demo-active-panel">{`activePanel: ${
activePanel?.id || 'null'
}`}</span>
@ -87,7 +95,8 @@ const DockviewGroupControl = () => {
<DockviewReact
onReady={onReady}
components={components}
groupControlComponent={GroupControlComponent}
leftHeaderActionsComponent={LeftHeaderActions}
rightHeaderActionsComponent={RightHeaderActions}
className="dockview-theme-abyss"
/>
);

View File

@ -1,5 +1,5 @@
{
"name": "groupcontrol-dockview",
"name": "iframe-dockview",
"description": "",
"keywords": [
"dockview"

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -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 (
<iframe
style={{
pointerEvents: 'none',
border: 'none',
width: '100%',
height: '100%',
}}
src="https://dockview.dev"
/>
);
}
),
basicComponent: () => {
return (
<div style={{ padding: '20px', color: 'white' }}>
{'This panel is just a usual component '}
</div>
);
},
};
export const App: React.FC = () => {
const onReady = (event: DockviewReadyEvent) => {
event.api.addPanel({
id: 'panel_1',
component: 'iframeComponent',
});
event.api.addPanel({
id: 'panel_2',
component: 'iframeComponent',
});
event.api.addPanel({
id: 'panel_3',
component: 'basicComponent',
});
};
return (
<DockviewReact
components={components}
onReady={onReady}
className="dockview-theme-abyss"
/>
);
};
export default App;

View File

@ -0,0 +1,91 @@
import { IDockviewPanelProps } from 'dockview';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
// get absolute position of element allowing for scroll position
function getDomNodePagePosition(domNode: HTMLElement): {
left: number;
top: number;
width: number;
height: number;
} {
const { left, top, width, height } = domNode.getBoundingClientRect();
return {
left: left + window.scrollX,
top: top + window.scrollY,
width: width,
height: height,
};
}
function toggleVisibility(element: HTMLElement, isVisible: boolean) {
element.style.visibility = isVisible ? 'visible' : 'hidden';
}
export const HoistedDockviewPanel = <T extends object>(
DockviewPanelComponent: React.FC<IDockviewPanelProps<T>>
) => {
return (props: IDockviewPanelProps<T>) => {
const ref = React.useRef<HTMLDivElement>(null);
const innerRef = React.useRef<HTMLDivElement>(null);
const positionHoistedPanel = () => {
if (!ref.current || !innerRef.current) {
return;
}
const { left, top, height, width } = getDomNodePagePosition(
ref.current.parentElement! // use the parent element to determine our size
);
innerRef.current.style.left = `${left}px`;
innerRef.current.style.top = `${top}px`;
innerRef.current.style.height = `${height}px`;
innerRef.current.style.width = `${width}px`;
};
React.useEffect(() => {
if (!innerRef.current) {
return;
}
const disposable1 = props.api.onDidVisibilityChange((event) => {
if (!innerRef.current) {
return;
}
toggleVisibility(innerRef.current, event.isVisible); // subsequent checks of visibility
});
const disposable2 = props.api.onDidDimensionsChange(() => {
positionHoistedPanel();
});
positionHoistedPanel();
return () => {
disposable1.dispose(); // cleanup
disposable2.dispose();
};
}, [props.api]);
return (
<div ref={ref}>
{ReactDOM.createPortal(
<div
/** you may want to mark these elements with some kind of attribute id */
ref={innerRef}
style={{
position: 'absolute',
overflow: 'hidden',
pointerEvents: 'none', // prevent this wrapper contain stealing events
}}
>
<DockviewPanelComponent {...props} />
</div>,
document.body // <-- you may choose to mount these 'global' elements to anywhere you see suitable
)}
</div>
);
};
};

View File

@ -0,0 +1,20 @@
import { StrictMode } from 'react';
import * as ReactDOMClient from 'react-dom/client';
import './styles.css';
import 'dockview/dist/styles/dockview.css';
import App from './app';
const rootElement = document.getElementById('root');
if (rootElement) {
const root = ReactDOMClient.createRoot(rootElement);
root.render(
<StrictMode>
<div className="app">
<App />
</div>
</StrictMode>
);
}

View File

@ -0,0 +1,15 @@
body {
margin: 0px;
color: white;
font-family: sans-serif;
text-align: center;
}
#root {
height: 100vh;
width: 100vw;
}
.app {
height: 100%;
}

View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"outDir": "build/dist",
"module": "esnext",
"target": "es5",
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"jsx": "react-jsx",
"moduleResolution": "node",
"rootDir": "src",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}

View File

@ -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';
@ -435,6 +436,40 @@ const panel2 = api.addPanel({
});
```
### Update Panel
You can programatically update the `params` passed through to the panel through the panal api using `api.updateParameters`.
```ts
const panel = api.addPanel({
id: 'panel_1',
component: 'default',
params: {
keyA: 'valueA',
},
});
// ...
panel.api.updateParameters({
keyB: 'valueB',
});
// ...
panel.api.updateParameters({
keyA: 'anotherValueA',
});
```
To delete a parameter you should pass a value of `undefined` for the key.
```ts
panel.api.updateParameters({
keyA: undefined, // this will delete 'keyA'.
});
```
### Panel Rendering
By default `DockviewReact` only adds to the DOM those panels that are visible,
@ -705,6 +740,29 @@ api.group.api.setConstraints(...)
<DockviewConstraints />
</Container>
## 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.
<Container sandboxId="iframe-dockview" height={600}>
<DockviewWithIFrames />
</Container>
## Events
A simple example showing events fired by `dockviewz that can be interacted with.

View File

@ -1,3 +1,3 @@
[
"1.7.3"
"1.7.6"
]

View File

@ -2,6 +2,11 @@
# yarn lockfile v1
"@adobe/css-tools@^4.0.1":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.2.0.tgz#e1a84fca468f4b337816fcb7f0964beb620ba855"
integrity sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==
"@algolia/autocomplete-core@1.7.4":
version "1.7.4"
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.7.4.tgz#85ff36b2673654a393c8c505345eaedd6eaa4f70"
@ -2888,6 +2893,21 @@
lz-string "^1.4.4"
pretty-format "^27.0.2"
"@testing-library/jest-dom@^5.16.5":
version "5.16.5"
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz#3912846af19a29b2dbf32a6ae9c31ef52580074e"
integrity sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==
dependencies:
"@adobe/css-tools" "^4.0.1"
"@babel/runtime" "^7.9.2"
"@types/testing-library__jest-dom" "^5.9.1"
aria-query "^5.0.0"
chalk "^3.0.0"
css.escape "^1.5.1"
dom-accessibility-api "^0.5.6"
lodash "^4.17.15"
redent "^3.0.0"
"@testing-library/react@^13.4.0":
version "13.4.0"
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-13.4.0.tgz#6a31e3bf5951615593ad984e96b9e5e2d9380966"
@ -3107,6 +3127,14 @@
dependencies:
"@types/istanbul-lib-report" "*"
"@types/jest@*":
version "29.5.2"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.2.tgz#86b4afc86e3a8f3005b297ed8a72494f89e6395b"
integrity sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==
dependencies:
expect "^29.0.0"
pretty-format "^29.0.0"
"@types/jest@^29.4.0":
version "29.5.0"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.0.tgz#337b90bbcfe42158f39c2fb5619ad044bbb518ac"
@ -3292,6 +3320,13 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
"@types/testing-library__jest-dom@^5.9.1":
version "5.14.6"
resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.6.tgz#4887f6e1af11215428ab02777873bcede98a53b0"
integrity sha512-FkHXCb+ikSoUP4Y4rOslzTdX5sqYwMxfefKh1GmZ8ce1GOkEHntSp6b5cGadmNfp5e4BMEWOMx+WSKd5/MqlDA==
dependencies:
"@types/jest" "*"
"@types/tough-cookie@*":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397"
@ -4659,6 +4694,14 @@ chalk@^2.0.0, chalk@^2.3.0:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
@ -5488,6 +5531,11 @@ css-what@^6.0.1, css-what@^6.1.0:
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
css.escape@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
@ -5928,7 +5976,7 @@ docusaurus-plugin-sass@^0.2.3:
dependencies:
sass-loader "^10.1.1"
dom-accessibility-api@^0.5.9:
dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9:
version "0.5.16"
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
@ -9918,7 +9966,7 @@ markdown-escapes@^1.0.0:
resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535"
integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==
marked@^4.3.0:
marked@^4.2.12, marked@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3"
integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==
@ -10136,6 +10184,13 @@ minimatch@^6.1.6:
dependencies:
brace-expansion "^2.0.1"
minimatch@^7.1.3:
version "7.4.6"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.6.tgz#845d6f254d8f4a5e4fd6baf44d5f10c8448365fb"
integrity sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==
dependencies:
brace-expansion "^2.0.1"
minimatch@^7.4.1, minimatch@^7.4.2:
version "7.4.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.3.tgz#012cbf110a65134bb354ae9773b55256cdb045a2"
@ -14256,6 +14311,16 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
typedoc@^0.23.25:
version "0.23.28"
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.23.28.tgz#3ce9c36ef1c273fa849d2dea18651855100d3ccd"
integrity sha512-9x1+hZWTHEQcGoP7qFmlo4unUoVJLB0H/8vfO/7wqTnZxg4kPuji9y3uRzEu0ZKez63OJAUmiGhUrtukC6Uj3w==
dependencies:
lunr "^2.3.9"
marked "^4.2.12"
minimatch "^7.1.3"
shiki "^0.14.1"
typedoc@^0.24.7:
version "0.24.7"
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.24.7.tgz#7eeb272a1894b3789acc1a94b3f2ae8e7330ee39"