Merge branch '281-type-hints-for-panel-parameters' into mpearson/component-params-typing

This commit is contained in:
mathuo 2023-06-13 19:53:08 +01:00 committed by GitHub
commit 853a5be569
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
158 changed files with 4215 additions and 1220 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",
@ -22,8 +23,11 @@
"/packages/docs/sandboxes/simple-dockview",
"/packages/docs/sandboxes/tabheight-dockview",
"/packages/docs/sandboxes/updatetitle-dockview",
"/packages/docs/sandboxes/vanilla-dockview",
"/packages/docs/sandboxes/watermark-dockview"
"/packages/docs/sandboxes/watermark-dockview",
"/packages/docs/sandboxes/javascript/fullwidthtab-dockview",
"/packages/docs/sandboxes/javascript/simple-dockview",
"/packages/docs/sandboxes/javascript/tabheight-dockview",
"/packages/docs/sandboxes/javascript/vanilla-dockview"
],
"node": "16"
}

View File

@ -33,8 +33,10 @@ jobs:
working-directory: packages/dockview
- run: npm run build
working-directory: packages/docs
- run: npm run deploy-docs
working-directory: packages/docs
- run: npm run docs
working-directory: .
- run: npm run package-docs
working-directory: .
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@3.7.1
with:

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ test-report.xml
*.code-workspace
yarn-error.log
/build
/docs/

View File

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

View File

@ -19,7 +19,9 @@
"bootstrap": "lerna bootstrap",
"test:cov": "jest --coverage",
"version-beta-build": "lerna version prerelease --preid beta",
"publish-app": "lerna publish"
"publish-app": "lerna publish",
"docs": "typedoc",
"package-docs": "node scripts/package-docs.js"
},
"repository": {
"type": "git",
@ -33,6 +35,7 @@
"homepage": "https://github.com/mathuo/dockview#readme",
"devDependencies": {
"@testing-library/dom": "^8.20.0",
"@testing-library/jest-dom": "^5.16.5",
"@types/jest": "^29.4.0",
"@typescript-eslint/eslint-plugin": "^5.52.0",
"@typescript-eslint/parser": "^5.52.0",
@ -44,6 +47,7 @@
"gulp": "^4.0.2",
"gulp-concat": "^2.6.1",
"gulp-dart-sass": "^1.0.2",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.4.3",
"jest-sonar-reporter": "^2.0.0",
"jsdom": "^21.1.0",
@ -55,14 +59,13 @@
"style-loader": "^3.3.1",
"ts-jest": "^29.0.5",
"ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"tslib": "^2.5.0",
"typedoc": "^0.24.7",
"typescript": "^4.9.5",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
},
"dependencies": {
"jest": "^29.5.0",
"ts-node": "^10.9.1"
}
}
"dependencies": {}
}

View File

@ -1,6 +1,6 @@
{
"name": "dockview-core",
"version": "1.7.2",
"version": "1.7.5",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews with ReactJS support",
"main": "./dist/cjs/index.js",
"types": "./dist/cjs/index.d.ts",

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

@ -46,6 +46,7 @@ class PanelContentPartTest implements IContentRenderer {
dispose(): void {
this.isDisposed = true;
this._onDidDispose.fire();
this._onDidDispose.dispose();
}
}
@ -80,6 +81,7 @@ class PanelTabPartTest implements ITabRenderer {
dispose(): void {
this.isDisposed = true;
this._onDidDispose.fire();
this._onDidDispose.dispose();
}
}
@ -98,6 +100,68 @@ describe('dockviewComponent', () => {
});
});
test('event leakage', () => {
Emitter.setLeakageMonitorEnabled(true);
dockview = new DockviewComponent({
parentElement: container,
components: {
default: PanelContentPartTest,
},
});
dockview.layout(500, 1000);
dockview.addPanel({
id: 'panel1',
component: 'default',
});
const panel2 = dockview.addPanel({
id: 'panel2',
component: 'default',
});
dockview.removePanel(panel2);
const panel3 = dockview.addPanel({
id: 'panel3',
component: 'default',
position: {
direction: 'right',
referencePanel: 'panel1',
},
});
const panel4 = dockview.addPanel({
id: 'panel4',
component: 'default',
position: {
direction: 'above',
},
});
dockview.moveGroupOrPanel(
panel4.group,
panel3.group.id,
panel3.id,
'center'
);
dockview.dispose();
if (Emitter.MEMORY_LEAK_WATCHER.size > 0) {
for (const entry of Array.from(
Emitter.MEMORY_LEAK_WATCHER.events
)) {
console.log('disposal', entry[1]);
}
throw new Error('not all listeners disposed');
}
Emitter.setLeakageMonitorEnabled(false);
});
test('duplicate panel', () => {
dockview.layout(500, 1000);
@ -112,6 +176,8 @@ describe('dockviewComponent', () => {
component: 'default',
});
}).toThrowError('panel with id panel1 already exists');
dockview.dispose();
});
test('set active panel', () => {
@ -1285,21 +1351,21 @@ describe('dockviewComponent', () => {
tabComponent: 'default',
});
const panel2 = dockview.addPanel({
id: 'panel2',
component: 'default',
tabComponent: 'default',
});
// const panel2 = dockview.addPanel({
// id: 'panel2',
// component: 'default',
// tabComponent: 'default',
// });
expect(panel1.group).toEqual(panel2.group);
// expect(panel1.group).toEqual(panel2.group);
const panel1Spy = jest.spyOn(panel1, 'dispose');
const panel2Spy = jest.spyOn(panel2, 'dispose');
// const panel2Spy = jest.spyOn(panel2, 'dispose');
dockview.dispose();
expect(panel1Spy).toBeCalledTimes(1);
expect(panel2Spy).toBeCalledTimes(1);
// expect(panel2Spy).toBeCalledTimes(1);
});
test('panel is disposed of when from JSON is called', () => {
@ -2295,4 +2361,160 @@ describe('dockviewComponent', () => {
panels: {},
});
});
test('that title and params.title do not conflict', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent({
parentElement: container,
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
orientation: Orientation.HORIZONTAL,
});
dockview.layout(100, 100);
dockview.addPanel({
id: 'panel1',
component: 'default',
title: 'Panel 1',
params: {
title: 'Panel 1',
},
});
dockview.addPanel({
id: 'panel2',
component: 'default',
title: 'Panel 2',
});
dockview.addPanel({
id: 'panel3',
component: 'default',
params: {
title: 'Panel 3',
},
});
expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel1', 'panel2', 'panel3'],
activeView: 'panel3',
id: '1',
},
size: 100,
},
],
size: 100,
},
width: 100,
height: 100,
orientation: 'HORIZONTAL',
},
panels: {
panel1: {
id: 'panel1',
contentComponent: 'default',
params: {
title: 'Panel 1',
},
title: 'Panel 1',
},
panel2: {
id: 'panel2',
contentComponent: 'default',
title: 'Panel 2',
},
panel3: {
id: 'panel3',
contentComponent: 'default',
params: {
title: 'Panel 3',
},
title: 'panel3',
},
},
activeGroup: '1',
});
});
test('check dockview component is rendering to the DOM as expected', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent({
parentElement: container,
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
orientation: Orientation.HORIZONTAL,
});
dockview.layout(100, 100);
const panel1 = dockview.addPanel({
id: 'panel1',
component: 'default',
});
expect(dockview.element.querySelectorAll('.view').length).toBe(1);
const panel2 = dockview.addPanel({
id: 'panel2',
component: 'default',
});
expect(dockview.element.querySelectorAll('.view').length).toBe(1);
const panel3 = dockview.addPanel({
id: 'panel3',
component: 'default',
});
expect(dockview.element.querySelectorAll('.view').length).toBe(1);
dockview.moveGroupOrPanel(
panel3.group,
panel3.group.id,
panel3.id,
'right'
);
expect(dockview.groups.length).toBe(2);
expect(dockview.element.querySelectorAll('.view').length).toBe(2);
dockview.moveGroupOrPanel(
panel3.group,
panel2.group.id,
panel2.id,
'bottom'
);
expect(dockview.groups.length).toBe(3);
expect(dockview.element.querySelectorAll('.view').length).toBe(4);
dockview.moveGroupOrPanel(
panel2.group,
panel1.group.id,
panel1.id,
'center'
);
expect(dockview.groups.length).toBe(2);
expect(dockview.element.querySelectorAll('.view').length).toBe(2);
});
});

View File

@ -37,13 +37,13 @@ describe('dockviewPanel', () => {
latestTitle = event.title;
});
expect(cut.title).toBe('');
expect(cut.title).toBeUndefined();
cut.init({ title: 'new title', params: {} });
expect(latestTitle).toBe('new title');
expect(cut.title).toBe('new title');
cut.update({ params: { title: 'another title' } });
cut.setTitle('another title');
expect(latestTitle).toBe('another title');
expect(cut.title).toBe('another title');
@ -81,6 +81,9 @@ describe('dockviewPanel', () => {
cut.setTitle('newTitle');
expect(cut.title).toBe('newTitle');
cut.api.setTitle('new title 2');
expect(cut.title).toBe('new title 2');
});
test('dispose cleanup', () => {
@ -142,7 +145,7 @@ describe('dockviewPanel', () => {
expect(cut.params).toEqual(undefined);
cut.update({ params: { params: { variableA: 'A', variableB: 'B' } } });
cut.update({ params: { variableA: 'A', variableB: 'B' } });
expect(cut.params).toEqual({ variableA: 'A', variableB: 'B' });
});
@ -181,4 +184,67 @@ describe('dockviewPanel', () => {
expect(group.api.setSize).toBeCalledWith({ height: 123, width: 456 });
expect(group.api.setSize).toBeCalledTimes(1);
});
test('updateParameter', () => {
const dockviewApiMock = jest.fn<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

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

@ -7,7 +7,7 @@ import {
Sizing,
Splitview,
} from '../../splitview/splitview';
import { fireEvent } from '@testing-library/dom';
class Testview implements IView {
private _element: HTMLElement = document.createElement('div');
private _size = 0;
@ -84,6 +84,8 @@ describe('splitview', () => {
beforeEach(() => {
container = document.createElement('div');
container.className = 'container';
jest.clearAllMocks();
});
test('vertical splitview', () => {
@ -585,8 +587,149 @@ describe('splitview', () => {
expect(container.childNodes.length).toBeGreaterThan(0);
splitview.dispose();
let anyEvents = false;
const listener = splitview.onDidRemoveView((e) => {
anyEvents = true; // disposing of the splitview shouldn't fire onDidRemoveView events
});
splitview.dispose();
listener.dispose();
expect(anyEvents).toBeFalsy();
expect(container.childNodes.length).toBe(0);
});
test('dnd: mouse events to move sash', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: false,
});
splitview.layout(400, 500);
const view1 = new Testview(0, 1000);
const view2 = new Testview(0, 1000);
splitview.addView(view1);
splitview.addView(view2);
const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
const removeEventListenerSpy = jest.spyOn(
document,
'removeEventListener'
);
const sashElement = container
.getElementsByClassName('sash')
.item(0) as HTMLElement;
// validate the expected state before drag
expect([view1.size, view2.size]).toEqual([200, 200]);
expect(sashElement).toBeTruthy();
expect(view1.element.parentElement!.style.pointerEvents).toBe('');
expect(view2.element.parentElement!.style.pointerEvents).toBe('');
// start the drag event
fireEvent.mouseDown(sashElement, { clientX: 50, clientY: 100 });
expect(addEventListenerSpy).toBeCalledTimes(5);
// during a sash drag the views should have pointer-events disabled
expect(view1.element.parentElement!.style.pointerEvents).toBe('none');
expect(view2.element.parentElement!.style.pointerEvents).toBe('none');
// expect a delta move of 70 - 50 = 20
fireEvent.mouseMove(document, { clientX: 70, clientY: 110 });
expect([view1.size, view2.size]).toEqual([220, 180]);
// expect a delta move of 75 - 70 = 5
fireEvent.mouseMove(document, { clientX: 75, clientY: 110 });
expect([view1.size, view2.size]).toEqual([225, 175]);
// end the drag event
fireEvent.mouseUp(document);
expect(removeEventListenerSpy).toBeCalledTimes(5);
// expect pointer-eventes on views to be restored
expect(view1.element.parentElement!.style.pointerEvents).toBe('');
expect(view2.element.parentElement!.style.pointerEvents).toBe('');
fireEvent.mouseMove(document, { clientX: 100, clientY: 100 });
// expect no additional resizes
expect([view1.size, view2.size]).toEqual([225, 175]);
// expect no additional document listeners
expect(addEventListenerSpy).toBeCalledTimes(5);
expect(removeEventListenerSpy).toBeCalledTimes(5);
});
test('dnd: touch events to move sash', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: false,
});
splitview.layout(400, 500);
const view1 = new Testview(0, 1000);
const view2 = new Testview(0, 1000);
splitview.addView(view1);
splitview.addView(view2);
const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
const removeEventListenerSpy = jest.spyOn(
document,
'removeEventListener'
);
const sashElement = container
.getElementsByClassName('sash')
.item(0) as HTMLElement;
// validate the expected state before drag
expect([view1.size, view2.size]).toEqual([200, 200]);
expect(sashElement).toBeTruthy();
expect(view1.element.parentElement!.style.pointerEvents).toBe('');
expect(view2.element.parentElement!.style.pointerEvents).toBe('');
// start the drag event
fireEvent.touchStart(sashElement, {
touches: [{ clientX: 50, clientY: 100 }],
});
expect(addEventListenerSpy).toBeCalledTimes(5);
// during a sash drag the views should have pointer-events disabled
expect(view1.element.parentElement!.style.pointerEvents).toBe('none');
expect(view2.element.parentElement!.style.pointerEvents).toBe('none');
// expect a delta move of 70 - 50 = 20
fireEvent.touchMove(document, {
touches: [{ clientX: 70, clientY: 110 }],
});
expect([view1.size, view2.size]).toEqual([220, 180]);
// expect a delta move of 75 - 70 = 5
fireEvent.touchMove(document, {
touches: [{ clientX: 75, clientY: 110 }],
});
expect([view1.size, view2.size]).toEqual([225, 175]);
// end the drag event
fireEvent.touchEnd(document);
expect(removeEventListenerSpy).toBeCalledTimes(5);
// expect pointer-eventes on views to be restored
expect(view1.element.parentElement!.style.pointerEvents).toBe('');
expect(view2.element.parentElement!.style.pointerEvents).toBe('');
fireEvent.touchMove(document, {
touches: [{ clientX: 100, clientY: 100 }],
});
// expect no additional resizes
expect([view1.size, view2.size]).toEqual([225, 175]);
// expect no additional document listeners
expect(addEventListenerSpy).toBeCalledTimes(5);
expect(removeEventListenerSpy).toBeCalledTimes(5);
});
});

View File

@ -1,4 +1,5 @@
import { PanelDimensionChangeEvent } from '../../api/panelApi';
import { Emitter } from '../../events';
import { CompositeDisposable } from '../../lifecycle';
import { Orientation } from '../../splitview/splitview';
import { SplitviewComponent } from '../../splitview/splitviewComponent';
@ -25,6 +26,45 @@ describe('componentSplitview', () => {
container.className = 'container';
});
test('event leakage', () => {
Emitter.setLeakageMonitorEnabled(true);
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL,
components: {
testPanel: TestPanel,
},
});
splitview.layout(600, 400);
const panel1 = splitview.addPanel({
id: 'panel1',
component: 'testPanel',
});
const panel2 = splitview.addPanel({
id: 'panel2',
component: 'testPanel',
});
splitview.movePanel(0, 1);
splitview.removePanel(panel1);
splitview.dispose();
if (Emitter.MEMORY_LEAK_WATCHER.size > 0) {
for (const entry of Array.from(
Emitter.MEMORY_LEAK_WATCHER.events
)) {
console.log(entry[1]);
}
throw new Error('not all listeners disposed');
}
Emitter.setLeakageMonitorEnabled(false);
});
test('remove panel', () => {
const splitview = new SplitviewComponent({
parentElement: container,

View File

@ -19,7 +19,7 @@ export interface DockviewPanelApi
> {
readonly group: DockviewGroupPanel;
readonly isGroupActive: boolean;
readonly title: string;
readonly title: string | undefined;
readonly onDidActiveGroupChange: Event<void>;
readonly onDidGroupChange: Event<void>;
close(): void;
@ -43,7 +43,7 @@ export class DockviewPanelApiImpl
private readonly disposable = new MutableDisposable();
get title(): string {
get title(): string | undefined {
return this.panel.title;
}
@ -89,7 +89,7 @@ export class DockviewPanelApiImpl
}
public setTitle(title: string): void {
this.panel.update({ params: { title } });
this.panel.setTitle(title);
}
public close(): void {

View File

@ -126,15 +126,6 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
super();
this.addDisposables(
this.panelUpdatesDisposable,
this._onDidDimensionChange,
this._onDidChangeFocus,
this._onDidVisibilityChange,
this._onDidActiveChange,
this._onFocusEvent,
this._onActiveChange,
this._onVisibilityChange,
this._onUpdateParameters,
this.onDidFocusChange((event) => {
this._isFocused = event.isFocused;
}),
@ -147,7 +138,16 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
this.onDidDimensionsChange((event) => {
this._width = event.width;
this._height = event.height;
})
}),
this.panelUpdatesDisposable,
this._onDidDimensionChange,
this._onDidChangeFocus,
this._onDidVisibilityChange,
this._onDidActiveChange,
this._onFocusEvent,
this._onActiveChange,
this._onVisibilityChange,
this._onUpdateParameters
);
}
@ -155,9 +155,7 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
this.panelUpdatesDisposable.value = this._onUpdateParameters.event(
(parameters) => {
panel.update({
params: {
params: parameters,
},
params: parameters,
});
}
);

View File

@ -7,15 +7,21 @@ import {
} from '../lifecycle';
export abstract class DragHandler extends CompositeDisposable {
private readonly disposable = new MutableDisposable();
private readonly dataDisposable = new MutableDisposable();
private readonly pointerEventsDisposable = new MutableDisposable();
private readonly _onDragStart = new Emitter<void>();
readonly onDragStart = this._onDragStart.event;
private iframes: HTMLElement[] = [];
constructor(protected readonly el: HTMLElement) {
super();
this.addDisposables(
this._onDragStart,
this.dataDisposable,
this.pointerEventsDisposable
);
this.configure();
}
@ -25,19 +31,27 @@ export abstract class DragHandler extends CompositeDisposable {
this.addDisposables(
this._onDragStart,
addDisposableListener(this.el, 'dragstart', (event) => {
this.iframes = [
const iframes = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
for (const iframe of this.iframes) {
this.pointerEventsDisposable.value = {
dispose: () => {
for (const iframe of iframes) {
iframe.style.pointerEvents = 'auto';
}
},
};
for (const iframe of iframes) {
iframe.style.pointerEvents = 'none';
}
this.el.classList.add('dv-dragged');
setTimeout(() => this.el.classList.remove('dv-dragged'), 0);
this.disposable.value = this.getData(event.dataTransfer);
this.dataDisposable.value = this.getData(event.dataTransfer);
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
@ -58,12 +72,8 @@ export abstract class DragHandler extends CompositeDisposable {
}
}),
addDisposableListener(this.el, 'dragend', () => {
for (const iframe of this.iframes) {
iframe.style.pointerEvents = 'auto';
}
this.iframes = [];
this.disposable.dispose();
this.pointerEventsDisposable.dispose();
this.dataDisposable.dispose();
})
);
}

View File

@ -177,6 +177,7 @@ export class Droptarget extends CompositeDisposable {
public dispose(): void {
this.removeDropTarget();
super.dispose();
}
private toggleClasses(

View File

@ -53,8 +53,4 @@ export class GroupDragHandler extends DragHandler {
},
};
}
public dispose(): void {
//
}
}

View File

@ -77,11 +77,12 @@ export class ContentContainer
const _onDidFocus = this.panel.view.content.onDidFocus;
const _onDidBlur = this.panel.view.content.onDidBlur;
const { onDidFocus, onDidBlur } = trackFocus(this._element);
const focusTracker = trackFocus(this._element);
disposable.addDisposables(
onDidFocus(() => this._onDidFocus.fire()),
onDidBlur(() => this._onDidBlur.fire())
focusTracker,
focusTracker.onDidFocus(() => this._onDidFocus.fire()),
focusTracker.onDidBlur(() => this._onDidBlur.fire())
);
if (_onDidFocus) {

View File

@ -12,7 +12,7 @@ import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { DroptargetEvent, Droptarget } from '../../../dnd/droptarget';
import { DragHandler } from '../../../dnd/abstractDragHandler';
export interface ITab {
export interface ITab extends IDisposable {
readonly panelId: string;
readonly element: HTMLElement;
setContent: (element: ITabRenderer) => void;
@ -43,8 +43,6 @@ export class Tab extends CompositeDisposable implements ITab {
) {
super();
this.addDisposables(this._onChanged, this._onDropped);
this._element = document.createElement('div');
this._element.className = 'tab';
this._element.tabIndex = 0;
@ -53,6 +51,8 @@ export class Tab extends CompositeDisposable implements ITab {
toggleClass(this.element, 'inactive-tab', true);
this.addDisposables(
this._onChanged,
this._onDropped,
new (class Handler extends DragHandler {
private readonly panelTransfer =
LocalSelectionTransfer.getInstance<PanelTransfer>();
@ -71,10 +71,6 @@ export class Tab extends CompositeDisposable implements ITab {
},
};
}
public dispose(): void {
//
}
})(this._element)
);
@ -127,7 +123,8 @@ export class Tab extends CompositeDisposable implements ITab {
this.addDisposables(
this.droptarget.onDrop((event) => {
this._onDropped.fire(event);
})
}),
this.droptarget
);
}
@ -146,6 +143,5 @@ export class Tab extends CompositeDisposable implements ITab {
public dispose(): void {
super.dispose();
this.droptarget.dispose();
}
}

View File

@ -216,6 +216,7 @@ export class TabsContainer
const { value, disposable } = tabToRemove;
disposable.dispose();
value.dispose();
value.element.remove();
}
@ -275,9 +276,11 @@ export class TabsContainer
public dispose(): void {
super.dispose();
this.tabs.forEach((tab) => {
tab.disposable.dispose();
});
for (const { value, disposable } of this.tabs) {
disposable.dispose();
value.dispose();
}
this.tabs = [];
}
}

View File

@ -251,7 +251,6 @@ export class DockviewComponent
});
this.addDisposables(
dropTarget,
dropTarget.onDrop((event) => {
const data = getPanelData();
@ -270,7 +269,8 @@ export class DockviewComponent
getData: getPanelData,
});
}
})
}),
dropTarget
);
this._api = new DockviewApi(this);
@ -706,43 +706,49 @@ export class DockviewComponent
}
moveGroupOrPanel(
referenceGroup: DockviewGroupPanel,
groupId: string,
itemId: string | undefined,
target: Position,
index?: number
destinationGroup: DockviewGroupPanel,
sourceGroupId: string,
sourceItemId: string | undefined,
destinationTarget: Position,
destinationIndex?: number
): void {
const sourceGroup = groupId
? this._groups.get(groupId)?.value
const sourceGroup = sourceGroupId
? this._groups.get(sourceGroupId)?.value
: undefined;
if (itemId === undefined) {
if (sourceItemId === undefined) {
if (sourceGroup) {
this.moveGroup(sourceGroup, referenceGroup, target);
this.moveGroup(
sourceGroup,
destinationGroup,
destinationTarget
);
}
return;
}
if (!target || target === 'center') {
if (!destinationTarget || destinationTarget === 'center') {
const groupItem: IDockviewPanel | undefined =
sourceGroup?.model.removePanel(itemId) ||
this.panels.find((panel) => panel.id === itemId);
sourceGroup?.model.removePanel(sourceItemId) ||
this.panels.find((panel) => panel.id === sourceItemId);
if (!groupItem) {
throw new Error(`No panel with id ${itemId}`);
throw new Error(`No panel with id ${sourceItemId}`);
}
if (sourceGroup?.model.size === 0) {
this.doRemoveGroup(sourceGroup);
}
referenceGroup.model.openPanel(groupItem, { index });
destinationGroup.model.openPanel(groupItem, {
index: destinationIndex,
});
} else {
const referenceLocation = getGridLocation(referenceGroup.element);
const referenceLocation = getGridLocation(destinationGroup.element);
const targetLocation = getRelativeLocation(
this.gridview.orientation,
referenceLocation,
target
destinationTarget
);
if (sourceGroup && sourceGroup.size < 2) {
@ -766,28 +772,28 @@ export class DockviewComponent
// after deleting the group we need to re-evaulate the ref location
const updatedReferenceLocation = getGridLocation(
referenceGroup.element
destinationGroup.element
);
const location = getRelativeLocation(
this.gridview.orientation,
updatedReferenceLocation,
target
destinationTarget
);
this.doAddGroup(targetGroup, location);
}
} else {
const groupItem: IDockviewPanel | undefined =
sourceGroup?.model.removePanel(itemId) ||
this.panels.find((panel) => panel.id === itemId);
sourceGroup?.model.removePanel(sourceItemId) ||
this.panels.find((panel) => panel.id === sourceItemId);
if (!groupItem) {
throw new Error(`No panel with id ${itemId}`);
throw new Error(`No panel with id ${sourceItemId}`);
}
const dropLocation = getRelativeLocation(
this.gridview.orientation,
referenceLocation,
target
destinationTarget
);
const group = this.createGroupAtLocation(dropLocation);
@ -953,11 +959,11 @@ export class DockviewComponent
}
public dispose(): void {
super.dispose();
this._onDidActivePanelChange.dispose();
this._onDidAddPanel.dispose();
this._onDidRemovePanel.dispose();
this._onDidLayoutFromJSON.dispose();
super.dispose();
}
}

View File

@ -282,12 +282,6 @@ export class DockviewGroupPanelModel
this.locked = !!options.locked;
this.addDisposables(
this._onMove,
this._onDidChange,
this._onDidDrop,
this._onDidAddPanel,
this._onDidRemovePanel,
this._onDidActivePanelChange,
this.tabsContainer.onDrop((event) => {
this.handleDropEvent(event.event, 'center', event.index);
}),
@ -299,7 +293,13 @@ export class DockviewGroupPanelModel
}),
this.dropTarget.onDrop((event) => {
this.handleDropEvent(event.nativeEvent, event.position);
})
}),
this._onMove,
this._onDidChange,
this._onDidDrop,
this._onDidAddPanel,
this._onDidRemovePanel,
this._onDidActivePanelChange
);
}

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
@ -34,13 +31,13 @@ export class DockviewPanel
private _group: DockviewGroupPanel;
private _params?: Parameters;
private _title: string;
private _title: string | undefined;
get params(): Parameters | undefined {
return this._params;
}
get title(): string {
get title(): string | undefined {
return this._title;
}
@ -56,7 +53,6 @@ export class DockviewPanel
readonly view: IDockviewPanelModel
) {
super();
this._title = '';
this._group = group;
this.api = new DockviewPanelApiImpl(this, this._group);
@ -76,13 +72,13 @@ export class DockviewPanel
public init(params: IGroupPanelInitParameters): void {
this._params = params.params;
this.setTitle(params.title);
this.view.init({
...params,
api: this.api,
containerApi: this.containerApi,
});
this.setTitle(params.title);
}
focus(): void {
@ -103,12 +99,12 @@ export class DockviewPanel
}
setTitle(title: string): void {
const didTitleChange = title !== this._params?.title;
const didTitleChange = title !== this.title;
if (didTitleChange) {
this._title = title;
this.view?.update({
this.view.update({
params: {
params: this._params,
title: this.title,
@ -118,20 +114,25 @@ export class DockviewPanel
}
}
public update(event: GroupPanelUpdateEvent): void {
const params = event.params as IGroupPanelInitParameters;
public update(event: PanelUpdateEvent): void {
// merge the new parameters with the existing parameters
this._params = {
...(this._params || {}),
...event.params.params,
...event.params,
};
if (params.title !== this.title) {
this._title = params.title;
this.api._onDidTitleChange.fire({ title: this.title });
/**
* delete new keys that have a value of undefined,
* allow values of null
*/
for (const key of Object.keys(event.params)) {
if (event.params[key] === undefined) {
delete this._params[key];
}
}
this.view?.update({
// update the view with the updated props
this.view.update({
params: {
params: this._params,
title: this.title,

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

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

@ -111,6 +111,8 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker {
constructor(element: HTMLElement | Window) {
super();
this.addDisposables(this._onDidFocus, this._onDidBlur);
let hasFocus = isAncestor(document.activeElement, <HTMLElement>element);
let loosingFocus = false;
@ -169,11 +171,4 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker {
refreshState(): void {
this._refreshStateHandler();
}
public dispose(): void {
super.dispose();
this._onDidBlur.dispose();
this._onDidFocus.dispose();
}
}

View File

@ -24,24 +24,76 @@ export namespace Event {
};
}
// dumb event emitter with better typings than nodes event emitter
// https://github.com/microsoft/vscode/blob/master/src/vs/base/common/event.ts
class LeakageMonitor {
readonly events = new Map<Event<any>, Stacktrace>();
get size(): number {
return this.events.size;
}
add<T>(event: Event<T>, stacktrace: Stacktrace): void {
this.events.set(event, stacktrace);
}
delete<T>(event: Event<T>): void {
this.events.delete(event);
}
clear(): void {
this.events.clear();
}
}
class Stacktrace {
static create(): Stacktrace {
return new Stacktrace(new Error().stack ?? '');
}
private constructor(readonly value: string) {}
print(): void {
console.warn(this.value);
}
}
class Listener<T> {
constructor(
readonly callback: (t: T) => void,
readonly stacktrace: Stacktrace | undefined
) {}
}
// relatively simple event emitter taken from https://github.com/microsoft/vscode/blob/master/src/vs/base/common/event.ts
export class Emitter<T> implements IDisposable {
private _event?: Event<T>;
private _last?: T;
private _listeners: Array<(e: T) => any> = [];
private _listeners: Listener<any>[] = [];
private _disposed = false;
static ENABLE_TRACKING = false;
static readonly MEMORY_LEAK_WATCHER = new LeakageMonitor();
static setLeakageMonitorEnabled(isEnabled: boolean) {
if (isEnabled !== Emitter.ENABLE_TRACKING) {
Emitter.MEMORY_LEAK_WATCHER.clear();
}
Emitter.ENABLE_TRACKING = isEnabled;
}
constructor(private readonly options?: EmitterOptions) {}
get event(): Event<T> {
if (!this._event) {
this._event = (listener: (e: T) => void): IDisposable => {
this._event = (callback: (e: T) => void): IDisposable => {
if (this.options?.replay && this._last !== undefined) {
listener(this._last);
callback(this._last);
}
const listener = new Listener(
callback,
Emitter.ENABLE_TRACKING ? Stacktrace.create() : undefined
);
this._listeners.push(listener);
return {
@ -49,10 +101,22 @@ export class Emitter<T> implements IDisposable {
const index = this._listeners.indexOf(listener);
if (index > -1) {
this._listeners.splice(index, 1);
} else if (Emitter.ENABLE_TRACKING) {
// console.warn(
// `Listener already disposed`,
// Stacktrace.create().print()
// );
}
},
};
};
if (Emitter.ENABLE_TRACKING) {
Emitter.MEMORY_LEAK_WATCHER.add(
this._event,
Stacktrace.create()
);
}
}
return this._event;
}
@ -60,13 +124,31 @@ export class Emitter<T> implements IDisposable {
public fire(e: T): void {
this._last = e;
for (const listener of this._listeners) {
listener(e);
listener.callback(e);
}
}
public dispose(): void {
this._listeners = [];
this._disposed = true;
if (!this._disposed) {
this._disposed = true;
if (this._listeners.length > 0) {
if (Emitter.ENABLE_TRACKING) {
queueMicrotask(() => {
// don't check until stack of execution is completed to allow for out-of-order disposals within the same execution block
for (const listener of this._listeners) {
console.warn(listener.stacktrace?.print());
}
});
}
this._listeners = [];
}
if (Emitter.ENABLE_TRACKING && this._event) {
Emitter.MEMORY_LEAK_WATCHER.delete(this._event);
}
}
}
}
@ -80,7 +162,7 @@ export function addDisposableWindowListener<K extends keyof WindowEventMap>(
return {
dispose: () => {
element.removeEventListener(type, listener);
element.removeEventListener(type, listener, options);
},
};
}
@ -95,7 +177,7 @@ export function addDisposableListener<K extends keyof HTMLElementEventMap>(
return {
dispose: () => {
element.removeEventListener(type, listener);
element.removeEventListener(type, listener, options);
},
};
}

View File

@ -143,10 +143,7 @@ export abstract class BaseGrid<T extends IGridPanelView>
this.addDisposables(
this.gridview.onDidChange(() => {
this._bufferOnDidLayoutChange.fire();
})
);
this.addDisposables(
}),
Event.any(
this.onDidAddGroup,
this.onDidRemoveGroup,
@ -297,8 +294,6 @@ export abstract class BaseGrid<T extends IGridPanelView>
}
public dispose(): void {
super.dispose();
this._onDidActiveGroupChange.dispose();
this._onDidAddGroup.dispose();
this._onDidRemoveGroup.dispose();
@ -309,5 +304,7 @@ export abstract class BaseGrid<T extends IGridPanelView>
}
this.gridview.dispose();
super.dispose();
}
}

View File

@ -69,16 +69,17 @@ export abstract class BasePanelView<T extends PanelApiImpl>
this._element.style.width = '100%';
this._element.style.overflow = 'hidden';
const { onDidFocus, onDidBlur } = trackFocus(this._element);
const focusTracker = trackFocus(this._element);
this.addDisposables(
this.api,
onDidFocus(() => {
focusTracker.onDidFocus(() => {
this.api._onDidChangeFocus.fire({ isFocused: true });
}),
onDidBlur(() => {
focusTracker.onDidBlur(() => {
this.api._onDidChangeFocus.fire({ isFocused: false });
})
}),
focusTracker
);
}
@ -104,6 +105,7 @@ export abstract class BasePanelView<T extends PanelApiImpl>
}
update(event: PanelUpdateEvent): void {
// merge the new parameters with the existing parameters
this._params = {
...this._params,
params: {
@ -111,6 +113,18 @@ export abstract class BasePanelView<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 });
}
@ -125,9 +139,9 @@ export abstract class BasePanelView<T extends PanelApiImpl>
}
dispose(): void {
super.dispose();
this.api.dispose();
this.part?.dispose();
super.dispose();
}
}

View File

@ -260,13 +260,13 @@ export class BranchNode extends CompositeDisposable implements IView {
return this.splitview.getViewCachedVisibleSize(index);
}
public removeChild(index: number, sizing?: Sizing): void {
public removeChild(index: number, sizing?: Sizing): Node {
if (index < 0 || index >= this.children.length) {
throw new Error('Invalid index');
}
this.splitview.removeView(index, sizing);
this._removeChild(index);
return this._removeChild(index);
}
private _addChild(node: Node, index: number): void {
@ -296,9 +296,10 @@ export class BranchNode extends CompositeDisposable implements IView {
}
public dispose(): void {
super.dispose();
this._childrenDisposable.dispose();
this.children.forEach((child) => child.dispose());
this.splitview.dispose();
this.children.forEach((child) => child.dispose());
super.dispose();
}
}

View File

@ -462,7 +462,8 @@ export class Gridview implements IDisposable {
if (oldRoot.children.length === 1) {
// can remove one level of redundant branching if there is only a single child
const childReference = oldRoot.children[0];
oldRoot.removeChild(0); // remove to prevent disposal when disposing of unwanted root
const child = oldRoot.removeChild(0); // remove to prevent disposal when disposing of unwanted root
child.dispose();
oldRoot.dispose();
this._root.addChild(
@ -632,7 +633,8 @@ export class Gridview implements IDisposable {
newSiblingSize = Sizing.Invisible(newSiblingCachedVisibleSize);
}
grandParent.removeChild(parentIndex);
const child = grandParent.removeChild(parentIndex);
child.dispose();
const newParent = new BranchNode(
parent.orientation,
@ -676,60 +678,82 @@ export class Gridview implements IDisposable {
throw new Error('Invalid location');
}
const node = parent.children[index];
const nodeToRemove = parent.children[index];
if (!(node instanceof LeafNode)) {
if (!(nodeToRemove instanceof LeafNode)) {
throw new Error('Invalid location');
}
parent.removeChild(index, sizing);
nodeToRemove.dispose();
if (parent.children.length === 0) {
return node.view;
if (parent.children.length !== 1) {
return nodeToRemove.view;
}
if (parent.children.length > 1) {
return node.view;
}
// if the parent has only one child and we know the parent is a BranchNode we can make the tree
// more efficiently spaced by replacing the parent BranchNode with the child.
// if that child is a LeafNode then we simply replace the BranchNode with the child otherwise if the child
// is a BranchNode too we should spread it's children into the grandparent.
// refer to the remaining child as the sibling
const sibling = parent.children[0];
if (pathToParent.length === 0) {
// parent is root
// if the parent is root
if (sibling instanceof LeafNode) {
return node.view;
// if the sibling is a leaf node no action is required
return nodeToRemove.view;
}
// we must promote sibling to be the new root
// otherwise the sibling is a branch node. since the parent is the root and the root has only one child
// which is a branch node we can just set this branch node to be the new root node
// for good housekeeping we'll removing the sibling from it's existing tree
parent.removeChild(0, sizing);
// and set that sibling node to be root
this.root = sibling;
return node.view;
return nodeToRemove.view;
}
// otherwise the parent is apart of a large sub-tree
const [grandParent, ..._] = [...pathToParent].reverse();
const [parentIndex, ...__] = [...rest].reverse();
const isSiblingVisible = parent.isChildVisible(0);
// either way we need to remove the sibling from it's existing tree
parent.removeChild(0, sizing);
// note the sizes of all of the grandparents children
const sizes = grandParent.children.map((_size, i) =>
grandParent.getChildSize(i)
);
grandParent.removeChild(parentIndex, sizing);
// remove the parent from the grandparent since we are moving the sibling to take the parents place
// this parent is no longer used and can be disposed of
grandParent.removeChild(parentIndex, sizing).dispose();
if (sibling instanceof BranchNode) {
// replace the parent with the siblings children
sizes.splice(
parentIndex,
1,
...sibling.children.map((c) => c.size)
);
// and add those siblings to the grandparent
for (let i = 0; i < sibling.children.length; i++) {
const child = sibling.children[i];
grandParent.addChild(child, child.size, parentIndex + i);
}
} else {
// otherwise create a new leaf node and add that to the grandparent
const newSibling = new LeafNode(
sibling.view,
orthogonal(sibling.orientation),
@ -738,14 +762,19 @@ export class Gridview implements IDisposable {
const siblingSizing = isSiblingVisible
? sibling.orthogonalSize
: Sizing.Invisible(sibling.orthogonalSize);
grandParent.addChild(newSibling, siblingSizing, parentIndex);
}
// the containing node of the sibling is no longer required and can be disposed of
sibling.dispose();
// resize everything
for (let i = 0; i < sizes.length; i++) {
grandParent.resizeChild(i, sizes[i]);
}
return node.view;
return nodeToRemove.view;
}
public layout(width: number, height: number): void {

View File

@ -154,7 +154,6 @@ export abstract class GridviewPanel
this.api.initialize(this); // TODO: required to by-pass 'super before this' requirement
this.addDisposables(
this._onDidChange,
this.api.onVisibilityChange((event) => {
const { isVisible } = event;
const { accessor } = this._params as GridviewInitParameters;
@ -195,7 +194,8 @@ export abstract class GridviewPanel
height: event.height,
width: event.width,
});
})
}),
this._onDidChange
);
}

View File

@ -16,7 +16,7 @@ export namespace Disposable {
}
export class CompositeDisposable {
private readonly disposables: IDisposable[];
private readonly _disposables: IDisposable[];
private _isDisposed = false;
protected get isDisposed(): boolean {
@ -28,15 +28,15 @@ export class CompositeDisposable {
}
constructor(...args: IDisposable[]) {
this.disposables = args;
this._disposables = args;
}
public addDisposables(...args: IDisposable[]): void {
args.forEach((arg) => this.disposables.push(arg));
args.forEach((arg) => this._disposables.push(arg));
}
public dispose(): void {
this.disposables.forEach((arg) => arg.dispose());
this._disposables.forEach((arg) => arg.dispose());
this._isDisposed = true;
}

View File

@ -13,6 +13,7 @@ import { Event, Emitter } from '../events';
import { pushToStart, pushToEnd, firstIndex } from '../array';
import { range, clamp } from '../math';
import { ViewItem } from './viewItem';
import { IDisposable } from '../lifecycle';
export enum Orientation {
HORIZONTAL = 'HORIZONTAL',
@ -42,7 +43,7 @@ export enum LayoutPriority {
Normal = 'normal',
}
export interface IBaseView {
export interface IBaseView extends IDisposable {
minimumSize: number;
maximumSize: number;
snap?: boolean;
@ -97,7 +98,7 @@ export class Splitview {
private element: HTMLElement;
private viewContainer: HTMLElement;
private sashContainer: HTMLElement;
private views: ViewItem[] = [];
private viewItems: ViewItem[] = [];
private sashes: ISashItem[] = [];
private _orientation: Orientation;
private _size = 0;
@ -132,7 +133,7 @@ export class Splitview {
}
public get length(): number {
return this.views.length;
return this.viewItems.length;
}
public get proportions(): number[] | undefined {
@ -159,13 +160,13 @@ export class Splitview {
}
get minimumSize(): number {
return this.views.reduce((r, item) => r + item.minimumSize, 0);
return this.viewItems.reduce((r, item) => r + item.minimumSize, 0);
}
get maximumSize(): number {
return this.length === 0
? Number.POSITIVE_INFINITY
: this.views.reduce((r, item) => r + item.maximumSize, 0);
: this.viewItems.reduce((r, item) => r + item.maximumSize, 0);
}
get startSnappingEnabled(): boolean {
@ -240,7 +241,7 @@ export class Splitview {
});
// Initialize content size and proportions for first layout
this.contentSize = this.views.reduce((r, i) => r + i.size, 0);
this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
this.saveProportions();
}
}
@ -261,22 +262,22 @@ export class Splitview {
}
isViewVisible(index: number): boolean {
if (index < 0 || index >= this.views.length) {
if (index < 0 || index >= this.viewItems.length) {
throw new Error('Index out of bounds');
}
const viewItem = this.views[index];
const viewItem = this.viewItems[index];
return viewItem.visible;
}
setViewVisible(index: number, visible: boolean): void {
if (index < 0 || index >= this.views.length) {
if (index < 0 || index >= this.viewItems.length) {
throw new Error('Index out of bounds');
}
toggleClass(this.container, 'visible', visible);
const viewItem = this.views[index];
const viewItem = this.viewItems[index];
toggleClass(this.container, 'visible', visible);
@ -288,30 +289,30 @@ export class Splitview {
}
getViewSize(index: number): number {
if (index < 0 || index >= this.views.length) {
if (index < 0 || index >= this.viewItems.length) {
return -1;
}
return this.views[index].size;
return this.viewItems[index].size;
}
resizeView(index: number, size: number): void {
if (index < 0 || index >= this.views.length) {
if (index < 0 || index >= this.viewItems.length) {
return;
}
const indexes = range(this.views.length).filter((i) => i !== index);
const indexes = range(this.viewItems.length).filter((i) => i !== index);
const lowPriorityIndexes = [
...indexes.filter(
(i) => this.views[i].priority === LayoutPriority.Low
(i) => this.viewItems[i].priority === LayoutPriority.Low
),
index,
];
const highPriorityIndexes = indexes.filter(
(i) => this.views[i].priority === LayoutPriority.High
(i) => this.viewItems[i].priority === LayoutPriority.High
);
const item = this.views[index];
const item = this.viewItems[index];
size = Math.round(size);
size = clamp(
size,
@ -324,13 +325,13 @@ export class Splitview {
}
public getViews<T extends IView>(): T[] {
return this.views.map((x) => x.view as T);
return this.viewItems.map((x) => x.view as T);
}
private onDidChange(item: ViewItem, size: number | undefined): void {
const index = this.views.indexOf(item);
const index = this.viewItems.indexOf(item);
if (index < 0 || index >= this.views.length) {
if (index < 0 || index >= this.viewItems.length) {
return;
}
@ -345,7 +346,7 @@ export class Splitview {
public addView(
view: IView,
size: number | Sizing = { type: 'distribute' },
index: number = this.views.length,
index: number = this.viewItems.length,
skipLayout?: boolean
): void {
const container = document.createElement('div');
@ -369,14 +370,14 @@ export class Splitview {
this.onDidChange(viewItem, newSize.size)
);
const dispose = () => {
disposable?.dispose();
this.viewContainer.removeChild(container);
};
const viewItem = new ViewItem(container, view, viewSize, {
dispose: () => {
disposable.dispose();
this.viewContainer.removeChild(container);
},
});
const viewItem = new ViewItem(container, view, viewSize, { dispose });
if (index === this.views.length) {
if (index === this.viewItems.length) {
this.viewContainer.appendChild(container);
} else {
this.viewContainer.insertBefore(
@ -385,15 +386,25 @@ export class Splitview {
);
}
this.views.splice(index, 0, viewItem);
this.viewItems.splice(index, 0, viewItem);
if (this.views.length > 1) {
if (this.viewItems.length > 1) {
//add sash
const sash = document.createElement('div');
sash.className = 'sash';
const onStart = (event: MouseEvent) => {
for (const item of this.views) {
const onTouchStart = (event: TouchEvent) => {
event.preventDefault();
const touch = event.touches[0];
onStart(touch);
};
const onMouseDown = (event: MouseEvent) => {
onStart(event);
};
const onStart = (event: { clientX: number; clientY: number }) => {
for (const item of this.viewItems) {
item.enabled = false;
}
@ -417,19 +428,20 @@ export class Splitview {
);
//
const sizes = this.views.map((x) => x.size);
const sizes = this.viewItems.map((x) => x.size);
//
let snapBefore: ISashDragSnapState | undefined;
let snapAfter: ISashDragSnapState | undefined;
const upIndexes = range(sashIndex, -1);
const downIndexes = range(sashIndex + 1, this.views.length);
const downIndexes = range(sashIndex + 1, this.viewItems.length);
const minDeltaUp = upIndexes.reduce(
(r, i) => r + (this.views[i].minimumSize - sizes[i]),
(r, i) => r + (this.viewItems[i].minimumSize - sizes[i]),
0
);
const maxDeltaUp = upIndexes.reduce(
(r, i) => r + (this.views[i].viewMaximumSize - sizes[i]),
(r, i) =>
r + (this.viewItems[i].viewMaximumSize - sizes[i]),
0
);
const maxDeltaDown =
@ -437,7 +449,8 @@ export class Splitview {
? Number.POSITIVE_INFINITY
: downIndexes.reduce(
(r, i) =>
r + (sizes[i] - this.views[i].minimumSize),
r +
(sizes[i] - this.viewItems[i].minimumSize),
0
);
const minDeltaDown =
@ -446,7 +459,8 @@ export class Splitview {
: downIndexes.reduce(
(r, i) =>
r +
(sizes[i] - this.views[i].viewMaximumSize),
(sizes[i] -
this.viewItems[i].viewMaximumSize),
0
);
const minDelta = Math.max(minDeltaUp, minDeltaDown);
@ -454,7 +468,7 @@ export class Splitview {
const snapBeforeIndex = this.findFirstSnapIndex(upIndexes);
const snapAfterIndex = this.findFirstSnapIndex(downIndexes);
if (typeof snapBeforeIndex === 'number') {
const snappedViewItem = this.views[snapBeforeIndex];
const snappedViewItem = this.viewItems[snapBeforeIndex];
const halfSize = Math.floor(
snappedViewItem.viewMinimumSize / 2
);
@ -469,7 +483,7 @@ export class Splitview {
}
if (typeof snapAfterIndex === 'number') {
const snappedViewItem = this.views[snapAfterIndex];
const snappedViewItem = this.viewItems[snapAfterIndex];
const halfSize = Math.floor(
snappedViewItem.viewMinimumSize / 2
);
@ -482,13 +496,25 @@ export class Splitview {
size: snappedViewItem.size,
};
}
//
const mousemove = (mousemoveEvent: MouseEvent) => {
const onMouseMove = (event: MouseEvent) => {
reposition(event);
};
const onTouchMove = (event: TouchEvent) => {
event.preventDefault();
const touch = event.touches[0];
reposition(touch);
};
const reposition = (event: {
clientX: number;
clientY: number;
}) => {
const current =
this._orientation === Orientation.HORIZONTAL
? mousemoveEvent.clientX
: mousemoveEvent.clientY;
? event.clientX
: event.clientY;
const delta = current - start;
this.resize(
@ -507,7 +533,7 @@ export class Splitview {
};
const end = () => {
for (const item of this.views) {
for (const item of this.viewItems) {
item.enabled = true;
}
@ -517,24 +543,30 @@ export class Splitview {
this.saveProportions();
document.removeEventListener('mousemove', mousemove);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', end);
document.removeEventListener('mouseend', end);
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', end);
document.removeEventListener('touchcancel', end);
this._onDidSashEnd.fire(undefined);
};
document.addEventListener('mousemove', mousemove);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', end);
document.addEventListener('mouseend', end);
document.addEventListener('touchmove', onTouchMove);
document.addEventListener('touchend', end);
document.addEventListener('touchcancel', end);
};
sash.addEventListener('mousedown', onStart);
sash.addEventListener('mousedown', onMouseDown);
sash.addEventListener('touchstart', onTouchStart);
const sashItem: ISashItem = {
container: sash,
disposable: () => {
sash.removeEventListener('mousedown', onStart);
sash.removeEventListener('touchstart', onTouchStart);
this.sashContainer.removeChild(sash);
},
};
@ -562,7 +594,7 @@ export class Splitview {
const flexibleViewItems: ViewItem[] = [];
let flexibleSize = 0;
for (const item of this.views) {
for (const item of this.viewItems) {
if (item.maximumSize - item.minimumSize > 0) {
flexibleViewItems.push(item);
flexibleSize += item.size;
@ -575,12 +607,12 @@ export class Splitview {
item.size = clamp(size, item.minimumSize, item.maximumSize);
}
const indexes = range(this.views.length);
const indexes = range(this.viewItems.length);
const lowPriorityIndexes = indexes.filter(
(i) => this.views[i].priority === LayoutPriority.Low
(i) => this.viewItems[i].priority === LayoutPriority.Low
);
const highPriorityIndexes = indexes.filter(
(i) => this.views[i].priority === LayoutPriority.High
(i) => this.viewItems[i].priority === LayoutPriority.High
);
this.relayout(lowPriorityIndexes, highPriorityIndexes);
@ -592,11 +624,11 @@ export class Splitview {
skipLayout = false
): IView {
// Remove view
const viewItem = this.views.splice(index, 1)[0];
const viewItem = this.viewItems.splice(index, 1)[0];
viewItem.dispose();
// Remove sash
if (this.views.length >= 1) {
if (this.viewItems.length >= 1) {
const sashIndex = Math.max(index - 1, 0);
const sashItem = this.sashes.splice(sashIndex, 1)[0];
sashItem.disposable();
@ -616,11 +648,11 @@ export class Splitview {
}
getViewCachedVisibleSize(index: number): number | undefined {
if (index < 0 || index >= this.views.length) {
if (index < 0 || index >= this.viewItems.length) {
throw new Error('Index out of bounds');
}
const viewItem = this.views[index];
const viewItem = this.viewItems[index];
return viewItem.cachedVisibleSize;
}
@ -640,24 +672,24 @@ export class Splitview {
this.orthogonalSize = orthogonalSize;
if (!this.proportions) {
const indexes = range(this.views.length);
const indexes = range(this.viewItems.length);
const lowPriorityIndexes = indexes.filter(
(i) => this.views[i].priority === LayoutPriority.Low
(i) => this.viewItems[i].priority === LayoutPriority.Low
);
const highPriorityIndexes = indexes.filter(
(i) => this.views[i].priority === LayoutPriority.High
(i) => this.viewItems[i].priority === LayoutPriority.High
);
this.resize(
this.views.length - 1,
this.viewItems.length - 1,
size - previousSize,
undefined,
lowPriorityIndexes,
highPriorityIndexes
);
} else {
for (let i = 0; i < this.views.length; i++) {
const item = this.views[i];
for (let i = 0; i < this.viewItems.length; i++) {
const item = this.viewItems[i];
item.size = clamp(
Math.round(this.proportions[i] * size),
@ -675,10 +707,10 @@ export class Splitview {
lowPriorityIndexes?: number[],
highPriorityIndexes?: number[]
): void {
const contentSize = this.views.reduce((r, i) => r + i.size, 0);
const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
this.resize(
this.views.length - 1,
this.viewItems.length - 1,
this._size - contentSize,
undefined,
lowPriorityIndexes,
@ -690,15 +722,15 @@ export class Splitview {
}
private distributeEmptySpace(lowPriorityIndex?: number): void {
const contentSize = this.views.reduce((r, i) => r + i.size, 0);
const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
let emptyDelta = this.size - contentSize;
const indexes = range(this.views.length - 1, -1);
const indexes = range(this.viewItems.length - 1, -1);
const lowPriorityIndexes = indexes.filter(
(i) => this.views[i].priority === LayoutPriority.Low
(i) => this.viewItems[i].priority === LayoutPriority.Low
);
const highPriorityIndexes = indexes.filter(
(i) => this.views[i].priority === LayoutPriority.High
(i) => this.viewItems[i].priority === LayoutPriority.High
);
for (const index of highPriorityIndexes) {
@ -714,7 +746,7 @@ export class Splitview {
}
for (let i = 0; emptyDelta !== 0 && i < indexes.length; i++) {
const item = this.views[indexes[i]];
const item = this.viewItems[indexes[i]];
const size = clamp(
item.size + emptyDelta,
item.minimumSize,
@ -729,21 +761,21 @@ export class Splitview {
private saveProportions(): void {
if (this.proportionalLayout && this.contentSize > 0) {
this._proportions = this.views.map(
this._proportions = this.viewItems.map(
(i) => i.size / this.contentSize
);
}
}
private layoutViews(): void {
this.contentSize = this.views.reduce((r, i) => r + i.size, 0);
this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
let sum = 0;
const x: number[] = [];
this.updateSashEnablement();
for (let i = 0; i < this.views.length - 1; i++) {
sum += this.views[i].size;
for (let i = 0; i < this.viewItems.length - 1; i++) {
sum += this.viewItems[i].size;
x.push(sum);
const offset = Math.min(Math.max(0, sum - 2), this.size - 4);
@ -757,7 +789,7 @@ export class Splitview {
this.sashes[i].container.style.top = `${offset}px`;
}
}
this.views.forEach((view, i) => {
this.viewItems.forEach((view, i) => {
if (this._orientation === Orientation.HORIZONTAL) {
view.container.style.width = `${view.size}px`;
view.container.style.left = i == 0 ? '0px' : `${x[i - 1]}px`;
@ -778,7 +810,7 @@ export class Splitview {
private findFirstSnapIndex(indexes: number[]): number | undefined {
// visible views first
for (const index of indexes) {
const viewItem = this.views[index];
const viewItem = this.viewItems[index];
if (!viewItem.visible) {
continue;
@ -791,7 +823,7 @@ export class Splitview {
// then, hidden views
for (const index of indexes) {
const viewItem = this.views[index];
const viewItem = this.viewItems[index];
if (
viewItem.visible &&
@ -810,16 +842,16 @@ export class Splitview {
private updateSashEnablement(): void {
let previous = false;
const collapsesDown = this.views.map(
const collapsesDown = this.viewItems.map(
(i) => (previous = i.size - i.minimumSize > 0 || previous)
);
previous = false;
const expandsDown = this.views.map(
const expandsDown = this.viewItems.map(
(i) => (previous = i.maximumSize - i.size > 0 || previous)
);
const reverseViews = [...this.views].reverse();
const reverseViews = [...this.viewItems].reverse();
previous = false;
const collapsesUp = reverseViews
.map((i) => (previous = i.size - i.minimumSize > 0 || previous))
@ -833,7 +865,7 @@ export class Splitview {
let position = 0;
for (let index = 0; index < this.sashes.length; index++) {
const sash = this.sashes[index];
const viewItem = this.views[index];
const viewItem = this.viewItems[index];
position += viewItem.size;
const min = !(collapsesDown[index] && expandsUp[index + 1]);
@ -841,16 +873,16 @@ export class Splitview {
if (min && max) {
const upIndexes = range(index, -1);
const downIndexes = range(index + 1, this.views.length);
const downIndexes = range(index + 1, this.viewItems.length);
const snapBeforeIndex = this.findFirstSnapIndex(upIndexes);
const snapAfterIndex = this.findFirstSnapIndex(downIndexes);
const snappedBefore =
typeof snapBeforeIndex === 'number' &&
!this.views[snapBeforeIndex].visible;
!this.viewItems[snapBeforeIndex].visible;
const snappedAfter =
typeof snapAfterIndex === 'number' &&
!this.views[snapAfterIndex].visible;
!this.viewItems[snapAfterIndex].visible;
if (
snappedBefore &&
@ -887,7 +919,7 @@ export class Splitview {
private resize = (
index: number,
delta: number,
sizes: number[] = this.views.map((x) => x.size),
sizes: number[] = this.viewItems.map((x) => x.size),
lowPriorityIndexes?: number[],
highPriorityIndexes?: number[],
overloadMinDelta: number = Number.NEGATIVE_INFINITY,
@ -895,12 +927,12 @@ export class Splitview {
snapBefore?: ISashDragSnapState,
snapAfter?: ISashDragSnapState
): number => {
if (index < 0 || index > this.views.length) {
if (index < 0 || index > this.viewItems.length) {
return 0;
}
const upIndexes = range(index, -1);
const downIndexes = range(index + 1, this.views.length);
const downIndexes = range(index + 1, this.viewItems.length);
//
if (highPriorityIndexes) {
for (const i of highPriorityIndexes) {
@ -916,18 +948,18 @@ export class Splitview {
}
}
//
const upItems = upIndexes.map((i) => this.views[i]);
const upItems = upIndexes.map((i) => this.viewItems[i]);
const upSizes = upIndexes.map((i) => sizes[i]);
//
const downItems = downIndexes.map((i) => this.views[i]);
const downItems = downIndexes.map((i) => this.viewItems[i]);
const downSizes = downIndexes.map((i) => sizes[i]);
//
const minDeltaUp = upIndexes.reduce(
(_, i) => _ + this.views[i].minimumSize - sizes[i],
(_, i) => _ + this.viewItems[i].minimumSize - sizes[i],
0
);
const maxDeltaUp = upIndexes.reduce(
(_, i) => _ + this.views[i].maximumSize - sizes[i],
(_, i) => _ + this.viewItems[i].maximumSize - sizes[i],
0
);
//
@ -935,7 +967,7 @@ export class Splitview {
downIndexes.length === 0
? Number.POSITIVE_INFINITY
: downIndexes.reduce(
(_, i) => _ + sizes[i] - this.views[i].minimumSize,
(_, i) => _ + sizes[i] - this.viewItems[i].minimumSize,
0
);
@ -943,7 +975,7 @@ export class Splitview {
downIndexes.length === 0
? Number.NEGATIVE_INFINITY
: downIndexes.reduce(
(_, i) => _ + sizes[i] - this.views[i].maximumSize,
(_, i) => _ + sizes[i] - this.viewItems[i].maximumSize,
0
);
//
@ -952,14 +984,14 @@ export class Splitview {
//
let snapped = false;
if (snapBefore) {
const snapView = this.views[snapBefore.index];
const snapView = this.viewItems[snapBefore.index];
const visible = delta >= snapBefore.limitDelta;
snapped = visible !== snapView.visible;
snapView.setVisible(visible, snapBefore.size);
}
if (!snapped && snapAfter) {
const snapView = this.views[snapAfter.index];
const snapView = this.viewItems[snapAfter.index];
const visible = delta < snapAfter.limitDelta;
snapped = visible !== snapView.visible;
snapView.setVisible(visible, snapAfter.size);
@ -1047,6 +1079,10 @@ export class Splitview {
}
}
for (const viewItem of this.viewItems) {
viewItem.dispose();
}
this.element.remove();
}
}

View File

@ -1,7 +1,6 @@
import {
CompositeDisposable,
IDisposable,
IValueDisposable,
MutableDisposable,
} from '../lifecycle';
import {
@ -83,10 +82,10 @@ export class SplitviewComponent
extends Resizable
implements ISplitviewComponent
{
private _disposable = new MutableDisposable();
private _splitviewChangeDisposable = new MutableDisposable();
private _splitview!: Splitview;
private _activePanel: SplitviewPanel | undefined;
private _panels = new Map<string, IValueDisposable<SplitviewPanel>>();
private _panels = new Map<string, IDisposable>();
private _options: SplitviewComponentOptions;
private readonly _onDidLayoutfromJSON = new Emitter<void>();
@ -124,7 +123,7 @@ export class SplitviewComponent
set splitview(value: Splitview) {
this._splitview = value;
this._disposable.value = new CompositeDisposable(
this._splitviewChangeDisposable.value = new CompositeDisposable(
this._splitview.onDidSashEnd(() => {
this._onDidLayoutChange.fire(undefined);
}),
@ -170,7 +169,6 @@ export class SplitviewComponent
this.splitview = new Splitview(this.element, options);
this.addDisposables(
this._disposable,
this._onDidAddView,
this._onDidLayoutfromJSON,
this._onDidRemoveView,
@ -226,19 +224,19 @@ export class SplitviewComponent
}
removePanel(panel: SplitviewPanel, sizing?: Sizing): void {
const disposable = this._panels.get(panel.id);
const item = this._panels.get(panel.id);
if (!disposable) {
if (!item) {
throw new Error(`unknown splitview panel ${panel.id}`);
}
disposable.disposable.dispose();
disposable.value.dispose();
item.dispose();
this._panels.delete(panel.id);
const index = this.panels.findIndex((_) => _ === panel);
this.splitview.removeView(index, sizing);
const removedView = this.splitview.removeView(index, sizing);
removedView.dispose();
const panels = this.panels;
if (panels.length > 0) {
@ -250,7 +248,7 @@ export class SplitviewComponent
return this.panels.find((view) => view.id === id);
}
addPanel(options: AddSplitviewComponentOptions): ISplitviewPanel {
addPanel(options: AddSplitviewComponentOptions): SplitviewPanel {
if (this._panels.has(options.id)) {
throw new Error(`panel ${options.id} already exists`);
}
@ -308,7 +306,7 @@ export class SplitviewComponent
this.setActive(view, true);
});
this._panels.set(view.id, { disposable, value: view });
this._panels.set(view.id, disposable);
}
toJSON(): SerializedSplitview {
@ -404,23 +402,34 @@ export class SplitviewComponent
}
clear(): void {
for (const [_, value] of this._panels.entries()) {
value.disposable.dispose();
value.value.dispose();
for (const disposable of this._panels.values()) {
disposable.dispose();
}
this._panels.clear();
this.splitview.dispose();
while (this.splitview.length > 0) {
const view = this.splitview.removeView(0, Sizing.Distribute, true);
view.dispose();
}
}
dispose(): void {
for (const [_, value] of this._panels.entries()) {
value.disposable.dispose();
value.value.dispose();
for (const disposable of this._panels.values()) {
disposable.dispose();
}
this._panels.clear();
const views = this.splitview.getViews();
this._splitviewChangeDisposable.dispose();
this.splitview.dispose();
for (const view of views) {
view.dispose();
}
super.dispose();
}
}

View File

@ -7,6 +7,7 @@ import { SplitviewPanelApiImpl } from '../api/splitviewPanelApi';
import { LayoutPriority, Orientation } from './splitview';
import { FunctionOrValue } from '../types';
import { Emitter, Event } from '../events';
import { CompositeDisposable } from '../lifecycle';
export interface ISplitviewPanel
extends BasePanelViewExported<SplitviewPanelApiImpl> {

View File

@ -1,7 +1,4 @@
{
"out": "typedocs",
"entryPoints": ["./src/index.ts"],
"exclude": ["**/_test/**/*.*", "**/index.ts"],
"excludeExternals": true,
"excludePrivate": true
"extends": ["../../typedoc.base.json"],
"entryPoints": ["src/index.ts"]
}

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.2",
"version": "1.7.5",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews with ReactJS support",
"main": "./dist/cjs/index.js",
"types": "./dist/cjs/index.d.ts",
@ -56,7 +56,7 @@
"author": "https://github.com/mathuo",
"license": "MIT",
"dependencies": {
"dockview-core": "^1.7.2"
"dockview-core": "^1.7.5"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.1",
@ -71,7 +71,6 @@
"react-dom": "^18.2.0",
"rimraf": "^4.1.2",
"rollup": "^3.15.0",
"rollup-plugin-postcss": "^4.0.2",
"typedoc": "^0.23.25"
"rollup-plugin-postcss": "^4.0.2"
}
}

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

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

@ -6,9 +6,6 @@
"jsx": "react",
"rootDir": "src"
},
"paths": {
"dockview-core": "../dockview-core"
},
"include": ["src"],
"exclude": ["**/node_modules", "src/__tests__"]
}

View File

@ -1,7 +1,5 @@
{
"out": "typedocs",
"entryPoints": ["./src/index.ts"],
"exclude": ["**/_test/**/*.*", "**/index.ts"],
"excludeExternals": true,
"excludePrivate": true
"extends": ["../../typedoc.base.json"],
"entryPoints": ["src/index.ts"],
"exclude": ["**/dist/**"]
}

View File

@ -0,0 +1,17 @@
---
slug: dockview-1.7.3-release
title: Dockview 1.7.3
tags: [release]
---
# Release Notes
Please reference to docs @ [dockview.dev](https://dockview.dev).
## 🚀 Features
## 🛠 Miscs
- Fix bug custom params named 'title' conflicting with built-in tab 'title' object [#258](https://github.com/mathuo/dockview/issues/258)
## 🔥 Breaking changes

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

@ -2,7 +2,10 @@
description: Dockview Documentation
---
import { Container } from '@site/src/components/ui/container';
import {
Container,
MultiFrameworkContainer,
} from '@site/src/components/ui/container';
import Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
@ -24,7 +27,12 @@ import RenderingDockview from '@site/sandboxes/rendering-dockview/src/app';
import DockviewExternalDnd from '@site/sandboxes/externaldnd-dockview/src/app';
import DockviewResizeContainer from '@site/sandboxes/resizecontainer-dockview/src/app';
import DockviewTabheight from '@site/sandboxes/tabheight-dockview/src/app';
import { attach as attachDockviewVanilla } from '@site/sandboxes/vanilla-dockview/src/app';
import DockviewWithIFrames from '@site/sandboxes/iframe-dockview/src/app';
import { attach as attachDockviewVanilla } from '@site/sandboxes/javascript/vanilla-dockview/src/app';
import { attach as attachSimpleDockview } from '@site/sandboxes/javascript/simple-dockview/src/app';
import { attach as attachTabHeightDockview } from '@site/sandboxes/javascript/tabheight-dockview/src/app';
import { attach as attachNativeDockview } from '@site/sandboxes/javascript/fullwidthtab-dockview/src/app';
# Dockview
@ -32,12 +40,16 @@ import { attach as attachDockviewVanilla } from '@site/sandboxes/vanilla-dockvie
Dockview is an abstraction built on top of [Gridviews](./gridview) where each view is a container of many tabbed panels.
<Container sandboxId="simple-dockview">
<SimpleDockview />
</Container>
<MultiFrameworkContainer
sandboxId="simple-dockview"
react={SimpleDockview}
typescript={attachSimpleDockview}
/>
You can access the panels associated group through the `panel.group` variable.
The group will always be defined and will change if a panel is moved into another group.
<br />
> You can access the panels associated group through the `panel.group` variable.
> The group will always be defined and will change if a panel is moved into another group.
## DockviewReact Component
@ -424,6 +436,40 @@ const panel2 = api.addPanel({
});
```
### Update Panel
You can programatically update the `params` passed through to the panel through the panal api using `api.updateParameters`.
```ts
const panel = api.addPanel({
id: 'panel_1',
component: 'default',
params: {
keyA: 'valueA',
},
});
// ...
panel.api.updateParameters({
keyB: 'valueB',
});
// ...
panel.api.updateParameters({
keyA: 'anotherValueA',
});
```
To delete a parameter you should pass a value of `undefined` for the key.
```ts
panel.api.updateParameters({
keyA: undefined, // this will delete 'keyA'.
});
```
### Panel Rendering
By default `DockviewReact` only adds to the DOM those panels that are visible,
@ -608,17 +654,21 @@ to the entire width of the group. For example:
<DockviewReactComponent singleTabMode="fullwidth" {...otherProps} />
```
<Container sandboxId="fullwidthtab-dockview">
<DockviewNative />
</Container>
<MultiFrameworkContainer
sandboxId="fullwidthtab-dockview"
react={DockviewNative}
typescript={attachNativeDockview}
/>
### Tab Height
Tab height can be controlled through CSS.
<Container sandboxId="tabheight-dockview">
<DockviewTabheight />
</Container>
<MultiFrameworkContainer
sandboxId="tabheight-dockview"
react={DockviewTabheight}
typescript={attachTabHeightDockview}
/>
## Groups
@ -690,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.
@ -709,19 +782,11 @@ If you wish to interact with the drop event from one dockview instance in anothe
<NestedDockview />
</Container>
### Example
hello
### Window-like mananger with tabs
<DockviewNative2 />
hello 2
<div style={{ height: '400px', width: '100%' }}>
<App />
</div>
## VanillaJS
## Vanilla JS
> Note: This section is experimental and support for Vanilla JS is a work in progress.
@ -732,6 +797,6 @@ The core library is published as an independant package under the name `dockview
> `dockview-core` is a dependency of `dockview` and automatically installed during the installation process of `dockview` via `npm install dockview`.
<Container
sandboxId="vanilla-dockview"
sandboxId="typescript/vanilla-dockview"
injectVanillaJS={attachDockviewVanilla}
/>

View File

@ -1,6 +1,6 @@
{
"name": "dockview-docs",
"version": "1.7.2",
"version": "1.7.5",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
@ -12,8 +12,7 @@
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc",
"deploy-docs": "node scripts/package-docs.js"
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "^2.4.0",
@ -23,7 +22,7 @@
"@minoru/react-dnd-treeview": "^3.4.3",
"axios": "^1.3.3",
"clsx": "^1.2.1",
"dockview": "^1.7.2",
"dockview": "^1.7.5",
"prism-react-renderer": "^1.3.5",
"react": "^18.2.0",
"react-dnd": "^16.0.1",

View File

@ -14,7 +14,8 @@
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"react-scripts": "*"
},
"scripts": {
"start": "react-scripts start",

View File

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

View File

@ -14,7 +14,8 @@
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"react-scripts": "*"
},
"scripts": {
"start": "react-scripts start",

View File

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

View File

@ -16,7 +16,8 @@
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/uuid": "^9.0.0",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"react-scripts": "*"
},
"scripts": {
"start": "react-scripts start",

View File

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

View File

@ -14,7 +14,8 @@
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"react-scripts": "*"
},
"scripts": {
"start": "react-scripts start",

View File

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

View File

@ -14,7 +14,8 @@
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"react-scripts": "*"
},
"scripts": {
"start": "react-scripts start",

View File

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

View File

@ -14,7 +14,8 @@
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"react-scripts": "*"
},
"scripts": {
"start": "react-scripts start",

View File

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

View File

@ -16,7 +16,8 @@
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"react-scripts": "*"
},
"scripts": {
"start": "react-scripts start",

View File

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

View File

@ -14,7 +14,8 @@
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"react-scripts": "*"
},
"scripts": {
"start": "react-scripts start",

View File

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

View File

@ -14,7 +14,8 @@
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"react-scripts": "*"
},
"scripts": {
"start": "react-scripts start",

View File

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

View File

@ -0,0 +1,32 @@
{
"name": "iframe-dockview",
"description": "",
"keywords": [
"dockview"
],
"version": "1.0.0",
"main": "src/index.tsx",
"dependencies": {
"dockview": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"typescript": "^4.9.5",
"react-scripts": "*"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}

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

@ -0,0 +1,28 @@
{
"name": "javascript-fullwidthtab-dockview",
"description": "",
"keywords": [
"dockview"
],
"version": "1.0.0",
"main": "src/index.ts",
"dependencies": {
"dockview-core": "*"
},
"devDependencies": {
"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

@ -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,17 @@
.my-custom-tab {
padding: 0px 8px;
width: 100%;
display: flex;
height: 100%;
align-items: center;
background-color: var(--dv-tabs-and-actions-container-background-color);
.my-custom-tab-icon {
font-size: 16px;
&:hover {
border-radius: 2px;
background-color: var(--dv-icon-hover-background-color);
}
}
}

View File

@ -0,0 +1,171 @@
import {
IGroupPanelInitParameters,
IContentRenderer,
PanelUpdateEvent,
Parameters,
ITabRenderer,
DockviewComponent,
} from 'dockview-core';
import './app.scss';
class DefaultPanel implements IContentRenderer {
private _element: HTMLElement;
private _titleElement: HTMLElement;
private _paramsElement: HTMLElement;
get element(): HTMLElement {
return this._element;
}
constructor() {
this._element = document.createElement('div');
this._element.style.display = 'flex';
this._element.style.justifyContent = 'center';
this._element.style.alignItems = 'center';
this._element.style.color = 'white';
this._element.style.height = '100%';
this._titleElement = document.createElement('span');
this._paramsElement = document.createElement('span');
this._element.appendChild(this._titleElement);
}
init(params: IGroupPanelInitParameters): void {
this.render(params.params);
}
update(event: PanelUpdateEvent<Parameters>): void {
this.render(event.params);
}
private render(params: Record<string, any>) {
this._titleElement.textContent = params.title;
if (params.x) {
if (!this._paramsElement.parentElement) {
this._element.appendChild(this._paramsElement);
}
this._paramsElement.textContent = params.x;
} else {
this._paramsElement.parentElement?.removeChild(this._paramsElement);
}
}
}
class DefaultTab implements ITabRenderer {
private _element: HTMLElement;
private _title: HTMLElement;
get element(): HTMLElement {
return this._element;
}
constructor() {
this._element = document.createElement('div');
this._element.className = 'my-custom-tab';
this._title = document.createElement('span');
const spacer = document.createElement('span');
spacer.style.flexGrow = '1';
const btn1 = document.createElement('span');
btn1.className = 'my-custom-tab-icon material-symbols-outlined';
btn1.textContent = 'minimize';
const btn2 = document.createElement('span');
btn2.className = 'my-custom-tab-icon material-symbols-outlined';
btn2.textContent = 'maximize';
const btn3 = document.createElement('span');
btn3.className = 'my-custom-tab-icon material-symbols-outlined';
btn3.textContent = 'close';
this._element.appendChild(this._title);
this._element.appendChild(spacer);
this._element.appendChild(btn1);
this._element.appendChild(btn2);
this._element.appendChild(btn3);
}
init(params: IGroupPanelInitParameters): void {
this.render(params.params);
}
update(event: PanelUpdateEvent<Parameters>): void {
this.render(event.params);
}
private render(params: Record<string, any>) {
this._title = params.title;
}
}
export function attach(parent: HTMLElement): {
dispose: () => void;
} {
const element = document.createElement('div');
element.className = 'dockview-theme-abyss';
element.style.height = '100%';
element.style.width = '100%';
const dockview = new DockviewComponent({
components: {
default: DefaultPanel,
},
tabComponents: {
default: DefaultTab,
},
singleTabMode: 'fullwidth',
parentElement: element,
});
parent.appendChild(element);
const { clientWidth, clientHeight } = parent;
dockview.layout(clientWidth, clientHeight);
const panel1 = dockview.addPanel({
id: 'panel_1',
component: 'default',
tabComponent: 'default',
params: {
title: 'Window 1',
},
});
panel1.group.locked = true;
const panel2 = dockview.addPanel({
id: 'panel_2',
component: 'default',
tabComponent: 'default',
params: {
title: 'Window 2',
},
position: {
direction: 'right',
},
});
panel2.group.locked = true;
const panel3 = dockview.addPanel({
id: 'panel_3',
component: 'default',
tabComponent: 'default',
params: {
title: 'Window 3',
},
position: {
direction: 'below',
},
});
panel3.group.locked = true;
return {
dispose: () => {
dockview.dispose();
element.remove();
},
};
}

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

@ -1,5 +1,5 @@
{
"name": "vanilla-dockview",
"name": "javascript-simple-dockview",
"description": "",
"keywords": [
"dockview"
@ -10,9 +10,15 @@
"dockview-core": "*"
},
"devDependencies": {
"typescript": "^4.9.5"
"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"
},
"scripts": {},
"browserslist": [
">0.2%",
"not dead",

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,120 @@
import {
DockviewComponent,
IContentRenderer,
IGroupPanelInitParameters,
PanelUpdateEvent,
Parameters,
} from 'dockview-core';
class DefaultPanel implements IContentRenderer {
private _element: HTMLElement;
get element(): HTMLElement {
return this._element;
}
constructor() {
this._element = document.createElement('div');
this._element.style.padding = '20px';
this._element.style.color = 'white';
}
init(params: IGroupPanelInitParameters): void {
this._element.textContent = params.params.title;
}
update(event: PanelUpdateEvent<Parameters>): void {
this._element.textContent = event.params.title;
}
}
export function attach(parent: HTMLElement): {
dispose: () => void;
} {
const element = document.createElement('div');
element.className = 'dockview-theme-abyss';
element.style.height = '100%';
element.style.width = '100%';
const dockview = new DockviewComponent({
components: {
default: DefaultPanel,
},
parentElement: element,
});
parent.appendChild(element);
const { clientWidth, clientHeight } = parent;
dockview.layout(clientWidth, clientHeight);
const panel = dockview.addPanel({
id: 'panel_1',
component: 'default',
params: {
title: 'Panel 1',
},
});
panel.group.locked = true;
panel.group.header.hidden = true;
dockview.addPanel({
id: 'panel_2',
component: 'default',
params: {
title: 'Panel 2',
},
});
dockview.addPanel({
id: 'panel_3',
component: 'default',
params: {
title: 'Panel 3',
},
});
dockview.addPanel({
id: 'panel_4',
component: 'default',
params: {
title: 'Panel 4',
},
position: { referencePanel: 'panel_1', direction: 'right' },
});
const panel5 = dockview.addPanel({
id: 'panel_5',
component: 'default',
params: {
title: 'Panel 5',
},
position: { referencePanel: 'panel_3', direction: 'right' },
});
dockview.addPanel({
id: 'panel_6',
component: 'default',
params: {
title: 'Panel 6',
},
position: { referencePanel: 'panel_5', direction: 'below' },
});
dockview.addPanel({
id: 'panel_7',
component: 'default',
params: {
title: 'Panel 7',
},
position: { referencePanel: 'panel_6', direction: 'right' },
});
return {
dispose: () => {
dockview.dispose();
element.remove();
},
};
}

View File

@ -0,0 +1,10 @@
import './styles.css';
import 'dockview-core/dist/styles/dockview.css';
import { attach } from './app';
const rootElement = document.getElementById('root');
if (rootElement) {
attach(rootElement);
}

View File

@ -0,0 +1,16 @@
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

@ -0,0 +1,28 @@
{
"name": "javascript-tabheight-dockview",
"description": "",
"keywords": [
"dockview"
],
"version": "1.0.0",
"main": "src/index.ts",
"dependencies": {
"dockview-core": "*"
},
"devDependencies": {
"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"
]
}

Some files were not shown because too many files have changed in this diff Show More