Merge branch 'master' of https://github.com/mathuo/dockview into 172-zero-dependency-vanilla-js

This commit is contained in:
mathuo 2023-02-27 16:05:54 +08:00
commit d09b2e8ff4
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
90 changed files with 10406 additions and 6730 deletions

View File

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

View File

@ -32,34 +32,34 @@
},
"homepage": "https://github.com/mathuo/dockview#readme",
"devDependencies": {
"@testing-library/dom": "^8.13.0",
"@types/jest": "^27.5.0",
"@typescript-eslint/eslint-plugin": "^5.22.0",
"@typescript-eslint/parser": "^5.22.0",
"@testing-library/dom": "^8.20.0",
"@types/jest": "^29.4.0",
"@typescript-eslint/eslint-plugin": "^5.52.0",
"@typescript-eslint/parser": "^5.52.0",
"codecov": "^3.8.3",
"cross-env": "^7.0.3",
"css-loader": "^6.7.1",
"eslint": "^8.15.0",
"fs-extra": "^10.1.0",
"css-loader": "^6.7.3",
"eslint": "^8.34.0",
"fs-extra": "^11.1.0",
"gulp": "^4.0.2",
"gulp-concat": "^2.6.1",
"gulp-dart-sass": "^1.0.2",
"jest": "^28.1.0",
"jest-environment-jsdom": "^28.1.0",
"jest": "^29.4.3",
"jest-environment-jsdom": "^29.4.3",
"jest-sonar-reporter": "^2.0.0",
"jsdom": "^19.0.0",
"lerna": "^4.0.0",
"jsdom": "^21.1.0",
"lerna": "^6.5.1",
"merge2": "^1.4.1",
"rimraf": "^3.0.2",
"sass": "^1.51.0",
"sass-loader": "^12.6.0",
"rimraf": "^4.1.2",
"sass": "^1.58.1",
"sass-loader": "^13.2.0",
"style-loader": "^3.3.1",
"ts-jest": "^28.0.2",
"ts-loader": "^9.3.0",
"tslib": "^2.4.0",
"typescript": "^4.6.4",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.9.0"
"ts-jest": "^29.0.5",
"ts-loader": "^9.4.2",
"tslib": "^2.5.0",
"typescript": "^4.9.5",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "dockview",
"version": "1.5.2",
"version": "1.6.0",
"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,22 +56,18 @@
"author": "https://github.com/mathuo",
"license": "MIT",
"devDependencies": {
"@rollup/plugin-typescript": "^8.3.2",
"@testing-library/react": "^13.2.0",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.3",
"@rollup/plugin-typescript": "^11.0.0",
"@testing-library/react": "^13.4.0",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"cross-env": "^7.0.3",
"postcss": "^8.4.13",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"rimraf": "^3.0.2",
"rollup": "^2.72.1",
"postcss": "^8.4.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "^4.1.2",
"rollup": "^3.15.0",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-terser": "^7.0.2",
"typedoc": "^0.22.15"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
"typedoc": "^0.23.25"
}
}

View File

@ -1,9 +1,9 @@
/* eslint-disable */
import { join } from 'path';
import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser';
import postcss from 'rollup-plugin-postcss';
const { join } = require('path');
const typescript = require('@rollup/plugin-typescript');
const { terser } = require('rollup-plugin-terser');
const postcss = require('rollup-plugin-postcss');
const { name, version, homepage, license } = require('./package.json');
const reactMain = join(__dirname, './scripts/rollupEntryTarget-react.ts');
@ -108,7 +108,7 @@ function createBundle(format, options) {
};
}
export default [
module.exports = [
// amd
createBundle('amd', {
withStyles: false,

View File

@ -1,42 +1,30 @@
import { DockviewPanelApiImpl, TitleEvent } from '../../api/dockviewPanelApi';
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { DockviewPanelApiImpl, TitleEvent } from '../../api/groupPanelApi';
import { IDockviewPanel } from '../../groupview/groupPanel';
import { DockviewPanel, IDockviewPanel } from '../../dockview/dockviewPanel';
import { GroupPanel } from '../../groupview/groupviewPanel';
describe('groupPanelApi', () => {
test('title', () => {
const groupPanel: Partial<IDockviewPanel> = {
id: 'test_id',
title: 'test_title',
};
const accessor: Partial<DockviewComponent> = {};
const groupViewPanel = new GroupPanel(
<DockviewComponent>accessor,
'',
{}
);
const cut = new DockviewPanelApiImpl(
<IDockviewPanel>groupPanel,
<GroupPanel>groupViewPanel
);
let events: TitleEvent[] = [];
const disposable = cut.onDidTitleChange((event) => {
events.push(event);
const panelMock = jest.fn<DockviewPanel, []>(() => {
return {
update: jest.fn(),
} as any;
});
const groupMock = jest.fn<GroupPanel, []>(() => {
return {} as any;
});
expect(events.length).toBe(0);
expect(cut.title).toBe('test_title');
const panel = new panelMock();
const group = new groupMock();
cut.setTitle('test_title_2');
expect(events.length).toBe(1);
expect(events[0]).toEqual({ title: 'test_title_2' });
expect(cut.title).toBe('test_title'); // title should remain unchanged
const cut = new DockviewPanelApiImpl(panel, group);
disposable.dispose();
cut.setTitle('test_title');
expect(panel.update).toBeCalledTimes(1);
expect(panel.update).toBeCalledWith({
params: { title: 'test_title' },
});
});
test('onDidGroupChange', () => {
@ -44,7 +32,11 @@ describe('groupPanelApi', () => {
id: 'test_id',
};
const accessor: Partial<DockviewComponent> = {};
const accessor: Partial<DockviewComponent> = {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
const groupViewPanel = new GroupPanel(
<DockviewComponent>accessor,
'',

View File

@ -3,7 +3,7 @@ import { DragHandler } from '../../dnd/abstractDragHandler';
import { IDisposable } from '../../lifecycle';
describe('abstractDragHandler', () => {
test('that className dragged is added to element after dragstart event', () => {
test('that className dv-dragged is added to element after dragstart event', () => {
jest.useFakeTimers();
const element = document.createElement('div');
@ -26,13 +26,13 @@ describe('abstractDragHandler', () => {
}
})(element);
expect(element.classList.contains('dragged')).toBeFalsy();
expect(element.classList.contains('dv-dragged')).toBeFalsy();
fireEvent.dragStart(element);
expect(element.classList.contains('dragged')).toBeTruthy();
expect(element.classList.contains('dv-dragged')).toBeTruthy();
jest.runAllTimers();
expect(element.classList.contains('dragged')).toBeFalsy();
expect(element.classList.contains('dv-dragged')).toBeFalsy();
handler.dispose();
});

View File

@ -1,16 +1,23 @@
import { Droptarget, Position } from '../../dnd/droptarget';
import {
calculateQuadrantAsPercentage,
calculateQuadrantAsPixels,
directionToPosition,
Droptarget,
Position,
positionToDirection,
} from '../../dnd/droptarget';
import { fireEvent } from '@testing-library/dom';
function createOffsetDragOverEvent(params: {
offsetX: number;
offsetY: number;
clientX: number;
clientY: number;
}): Event {
const event = new Event('dragover', {
bubbles: true,
cancelable: true,
});
Object.defineProperty(event, 'offsetX', { get: () => params.offsetX });
Object.defineProperty(event, 'offsetY', { get: () => params.offsetY });
Object.defineProperty(event, 'clientX', { get: () => params.clientX });
Object.defineProperty(event, 'clientY', { get: () => params.clientY });
return event;
}
@ -27,12 +34,34 @@ describe('droptarget', () => {
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 200);
});
test('directionToPosition', () => {
expect(directionToPosition('above')).toBe('top');
expect(directionToPosition('below')).toBe('bottom');
expect(directionToPosition('left')).toBe('left');
expect(directionToPosition('right')).toBe('right');
expect(directionToPosition('within')).toBe('center');
expect(() => directionToPosition('bad_input' as any)).toThrow(
"invalid direction 'bad_input'"
);
});
test('positionToDirection', () => {
expect(positionToDirection('top')).toBe('above');
expect(positionToDirection('bottom')).toBe('below');
expect(positionToDirection('left')).toBe('left');
expect(positionToDirection('right')).toBe('right');
expect(positionToDirection('center')).toBe('within');
expect(() => positionToDirection('bad_input' as any)).toThrow(
"invalid position 'bad_input'"
);
});
test('non-directional', () => {
let position: Position | undefined = undefined;
droptarget = new Droptarget(element, {
canDisplayOverlay: () => true,
validOverlays: 'none',
acceptedTargetZones: ['center'],
});
droptarget.onDrop((event) => {
@ -46,7 +75,7 @@ describe('droptarget', () => {
'.drop-target-dropzone'
) as HTMLElement;
fireEvent.drop(target);
expect(position).toBe(Position.Center);
expect(position).toBe('center');
});
test('drop', () => {
@ -54,7 +83,7 @@ describe('droptarget', () => {
droptarget = new Droptarget(element, {
canDisplayOverlay: () => true,
validOverlays: 'all',
acceptedTargetZones: ['top', 'left', 'right', 'bottom', 'center'],
});
droptarget.onDrop((event) => {
@ -73,18 +102,21 @@ describe('droptarget', () => {
fireEvent(
target,
createOffsetDragOverEvent({ offsetX: 19, offsetY: 0 })
createOffsetDragOverEvent({
clientX: 19,
clientY: 0,
})
);
expect(position).toBeUndefined();
fireEvent.drop(target);
expect(position).toBe(Position.Left);
expect(position).toBe('left');
});
test('default', () => {
droptarget = new Droptarget(element, {
canDisplayOverlay: () => true,
validOverlays: 'all',
acceptedTargetZones: ['top', 'left', 'right', 'bottom', 'center'],
});
expect(droptarget.state).toBeUndefined();
@ -106,57 +138,204 @@ describe('droptarget', () => {
fireEvent(
target,
createOffsetDragOverEvent({ offsetX: 19, offsetY: 0 })
createOffsetDragOverEvent({ clientX: 19, clientY: 0 })
);
viewQuery = element.querySelectorAll(
'.drop-target > .drop-target-dropzone > .drop-target-selection.left'
'.drop-target > .drop-target-dropzone > .drop-target-selection'
);
expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe(Position.Left);
expect(droptarget.state).toBe('left');
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('translateX(-25%) scaleX(0.5)');
fireEvent(
target,
createOffsetDragOverEvent({ offsetX: 40, offsetY: 19 })
createOffsetDragOverEvent({ clientX: 40, clientY: 19 })
);
viewQuery = element.querySelectorAll(
'.drop-target > .drop-target-dropzone > .drop-target-selection.top'
'.drop-target > .drop-target-dropzone > .drop-target-selection'
);
expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe(Position.Top);
expect(droptarget.state).toBe('top');
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('translateY(-25%) scaleY(0.5)');
fireEvent(
target,
createOffsetDragOverEvent({ offsetX: 160, offsetY: 81 })
createOffsetDragOverEvent({ clientX: 160, clientY: 81 })
);
viewQuery = element.querySelectorAll(
'.drop-target > .drop-target-dropzone > .drop-target-selection.bottom'
'.drop-target > .drop-target-dropzone > .drop-target-selection'
);
expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe(Position.Bottom);
expect(droptarget.state).toBe('bottom');
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('translateY(25%) scaleY(0.5)');
fireEvent(
target,
createOffsetDragOverEvent({ offsetX: 161, offsetY: 0 })
createOffsetDragOverEvent({ clientX: 161, clientY: 0 })
);
viewQuery = element.querySelectorAll(
'.drop-target > .drop-target-dropzone > .drop-target-selection.right'
'.drop-target > .drop-target-dropzone > .drop-target-selection'
);
expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe(Position.Right);
expect(droptarget.state).toBe('right');
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('translateX(25%) scaleX(0.5)');
fireEvent(
target,
createOffsetDragOverEvent({ offsetX: 100, offsetY: 50 })
createOffsetDragOverEvent({ clientX: 100, clientY: 50 })
);
expect(droptarget.state).toBe(Position.Center);
expect(droptarget.state).toBe('center');
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('');
fireEvent.dragLeave(target);
expect(droptarget.state).toBeUndefined();
expect(droptarget.state).toBe('center');
viewQuery = element.querySelectorAll('.drop-target');
expect(viewQuery.length).toBe(0);
});
describe('calculateQuadrantAsPercentage', () => {
test('variety of cases', () => {
const inputs: Array<{
directions: Position[];
x: number;
y: number;
result: Position | null;
}> = [
{ directions: ['left', 'right'], x: 19, y: 50, result: 'left' },
{
directions: ['left', 'right'],
x: 81,
y: 50,
result: 'right',
},
{
directions: ['top', 'bottom'],
x: 50,
y: 19,
result: 'top',
},
{
directions: ['top', 'bottom'],
x: 50,
y: 81,
result: 'bottom',
},
{
directions: ['left', 'right', 'top', 'bottom', 'center'],
x: 50,
y: 50,
result: 'center',
},
{
directions: ['left', 'right', 'top', 'bottom'],
x: 50,
y: 50,
result: null,
},
];
for (const input of inputs) {
expect(
calculateQuadrantAsPercentage(
new Set(input.directions),
input.x,
input.y,
100,
100,
20
)
).toBe(input.result);
}
});
});
describe('calculateQuadrantAsPixels', () => {
test('variety of cases', () => {
const inputs: Array<{
directions: Position[];
x: number;
y: number;
result: Position | null;
}> = [
{ directions: ['left', 'right'], x: 19, y: 50, result: 'left' },
{
directions: ['left', 'right'],
x: 81,
y: 50,
result: 'right',
},
{
directions: ['top', 'bottom'],
x: 50,
y: 19,
result: 'top',
},
{
directions: ['top', 'bottom'],
x: 50,
y: 81,
result: 'bottom',
},
{
directions: ['left', 'right', 'top', 'bottom', 'center'],
x: 50,
y: 50,
result: 'center',
},
{
directions: ['left', 'right', 'top', 'bottom'],
x: 50,
y: 50,
result: null,
},
];
for (const input of inputs) {
expect(
calculateQuadrantAsPixels(
new Set(input.directions),
input.x,
input.y,
100,
100,
20
)
).toBe(input.result);
}
});
});
});

View File

@ -7,22 +7,21 @@ import {
import { PanelUpdateEvent } from '../../panel/types';
import { Orientation } from '../../splitview/core/splitview';
import { ReactPanelDeserialzier } from '../../react/deserializer';
import { Position } from '../../dnd/droptarget';
import { GroupPanel } from '../../groupview/groupviewPanel';
import { CompositeDisposable } from '../../lifecycle';
import {
GroupPanelUpdateEvent,
GroupviewPanelState,
IDockviewPanel,
IGroupPanelInitParameters,
} from '../../groupview/groupPanel';
} from '../../groupview/types';
import { IGroupPanelView } from '../../dockview/defaultGroupPanelView';
import { DefaultTab } from '../../dockview/components/tab/defaultTab';
import { Emitter } from '../../events';
import { IDockviewPanel } from '../../dockview/dockviewPanel';
import {
DockviewPanelApi,
DockviewPanelApiImpl,
} from '../../api/groupPanelApi';
import { DefaultTab } from '../../dockview/components/tab/defaultTab';
import { Emitter } from '../../events';
} from '../../api/dockviewPanelApi';
class PanelContentPartTest implements IContentRenderer {
element: HTMLElement = document.createElement('div');
@ -32,7 +31,7 @@ class PanelContentPartTest implements IContentRenderer {
isDisposed: boolean = false;
constructor(public readonly id: string, component: string) {
constructor(public readonly id: string, public readonly component: string) {
this.element.classList.add(`testpanel-${id}`);
}
@ -53,7 +52,7 @@ class PanelContentPartTest implements IContentRenderer {
}
toJSON(): object {
return { id: this.id };
return { id: this.component };
}
focus(): void {
@ -253,9 +252,9 @@ describe('dockviewComponent', () => {
const panel4 = dockview.getGroupPanel('panel4');
const group1 = panel1!.group;
dockview.moveGroupOrPanel(group1, group1.id, 'panel1', Position.Right);
dockview.moveGroupOrPanel(group1, group1.id, 'panel1', 'right');
const group2 = panel1!.group;
dockview.moveGroupOrPanel(group2, group1.id, 'panel3', Position.Center);
dockview.moveGroupOrPanel(group2, group1.id, 'panel3', 'center');
expect(dockview.activeGroup).toBe(group2);
expect(dockview.activeGroup!.model.activePanel).toBe(panel3);
@ -302,12 +301,12 @@ describe('dockviewComponent', () => {
component: 'default',
});
const panel1 = dockview.getGroupPanel('panel1');
const panel2 = dockview.getGroupPanel('panel2');
const panel1 = dockview.getGroupPanel('panel1')!;
const panel2 = dockview.getGroupPanel('panel2')!;
const group1 = panel1.group;
dockview.moveGroupOrPanel(group1, group1.id, 'panel1', Position.Right);
dockview.moveGroupOrPanel(group1, group1.id, 'panel1', 'right');
const group2 = panel1.group;
dockview.moveGroupOrPanel(group2, group1.id, 'panel3', Position.Center);
dockview.moveGroupOrPanel(group2, group1.id, 'panel3', 'center');
expect(dockview.size).toBe(2);
expect(dockview.totalPanels).toBe(4);
@ -345,10 +344,10 @@ describe('dockviewComponent', () => {
component: 'default',
});
const panel1 = dockview.getGroupPanel('panel1');
const panel2 = dockview.getGroupPanel('panel2');
const panel3 = dockview.getGroupPanel('panel3');
const panel4 = dockview.getGroupPanel('panel4');
const panel1 = dockview.getGroupPanel('panel1')!;
const panel2 = dockview.getGroupPanel('panel2')!;
const panel3 = dockview.getGroupPanel('panel3')!;
const panel4 = dockview.getGroupPanel('panel4')!;
expect(panel1.api.isActive).toBeFalsy();
expect(panel2.api.isActive).toBeFalsy();
@ -370,9 +369,9 @@ describe('dockviewComponent', () => {
expect(panel4.api.isActive).toBeFalsy();
const group1 = panel1.group;
dockview.moveGroupOrPanel(group1, group1.id, 'panel1', Position.Right);
dockview.moveGroupOrPanel(group1, group1.id, 'panel1', 'right');
const group2 = panel1.group;
dockview.moveGroupOrPanel(group2, group1.id, 'panel3', Position.Center);
dockview.moveGroupOrPanel(group2, group1.id, 'panel3', 'center');
expect(dockview.size).toBe(2);
expect(panel1.group).toBe(panel3.group);
@ -425,9 +424,8 @@ describe('dockviewComponent', () => {
expect(dockview.size).toBe(1);
expect(dockview.totalPanels).toBe(2);
const panel1 = dockview.getGroupPanel('panel1');
const panel2 = dockview.getGroupPanel('panel2');
const panel1 = dockview.getGroupPanel('panel1')!;
const panel2 = dockview.getGroupPanel('panel2')!;
expect(panel1.group).toBe(panel2.group);
const group = panel1.group;
@ -440,7 +438,7 @@ describe('dockviewComponent', () => {
expect(group.model.indexOf(panel1)).toBe(0);
expect(group.model.indexOf(panel2)).toBe(1);
dockview.moveGroupOrPanel(group, group.id, 'panel1', Position.Right);
dockview.moveGroupOrPanel(group, group.id, 'panel1', 'right');
expect(dockview.size).toBe(2);
expect(dockview.totalPanels).toBe(2);
@ -460,7 +458,7 @@ describe('dockviewComponent', () => {
await panel2.api.close();
expect(dockview.size).toBe(1); // watermark
expect(dockview.size).toBe(0);
expect(dockview.totalPanels).toBe(0);
});
@ -489,8 +487,8 @@ describe('dockviewComponent', () => {
expect(viewQuery.length).toBe(1);
const group = dockview.getGroupPanel('panel1').group;
dockview.moveGroupOrPanel(group, group.id, 'panel1', Position.Right);
const group = dockview.getGroupPanel('panel1')!.group;
dockview.moveGroupOrPanel(group, group.id, 'panel1', 'right');
viewQuery = container.querySelectorAll(
'.branch-node > .split-view-container > .view-container > .view'
@ -975,7 +973,7 @@ describe('dockviewComponent', () => {
panel2.group!,
panel5.group!.id,
panel5.id,
Position.Center
'center'
);
expect(events).toEqual([
{ type: 'REMOVE_PANEL', panel: panel5 },
@ -994,7 +992,7 @@ describe('dockviewComponent', () => {
panel2.group!,
panel4.group!.id,
panel4.id,
Position.Center
'center'
);
expect(events).toEqual([
@ -1207,55 +1205,6 @@ describe('dockviewComponent', () => {
expect(dockview.totalPanels).toBe(0);
});
test('last group is retained for watermark', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(container, {
components: { default: PanelContentPartTest },
});
dockview.layout(500, 1000);
const panel1 = dockview.addPanel({
id: 'panel1',
component: 'default',
tabComponent: 'default',
});
expect(dockview.size).toBe(1);
expect(dockview.totalPanels).toBe(1);
const group = panel1.group;
dockview.removePanel(panel1);
expect(group.model.hasWatermark).toBeTruthy();
expect(dockview.size).toBe(1);
expect(dockview.totalPanels).toBe(0);
const panel2 = dockview.addPanel({
id: 'panel2',
component: 'default',
tabComponent: 'default',
});
expect(group.model.hasWatermark).toBeFalsy();
const panel3 = dockview.addPanel({
id: 'panel3',
component: 'default',
tabComponent: 'default',
});
expect(dockview.size).toBe(1);
expect(dockview.totalPanels).toBe(2);
panel2.api.close();
expect(group.model.hasWatermark).toBeFalsy();
panel3.api.close();
expect(group.model.hasWatermark).toBeTruthy();
});
test('panel is disposed of when removed', () => {
const container = document.createElement('div');
@ -1314,7 +1263,7 @@ describe('dockviewComponent', () => {
panel1.group,
panel2.group.id,
'panel2',
Position.Left
'left'
);
expect(panel1Spy).not.toHaveBeenCalled();
@ -1355,7 +1304,7 @@ describe('dockviewComponent', () => {
panel1.group,
panel2.group.id,
'panel2',
Position.Center
'center'
);
expect(panel1Spy).not.toHaveBeenCalled();
@ -1394,7 +1343,7 @@ describe('dockviewComponent', () => {
panel1.group,
panel1.group.id,
'panel1',
Position.Center,
'center',
0
);
@ -1515,6 +1464,53 @@ describe('dockviewComponent', () => {
expect(panel2Spy).toBeCalledTimes(1);
});
test('move entire group into another group', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(container, {
components: { default: PanelContentPartTest },
});
dockview.layout(500, 1000);
const panel1 = dockview.addPanel({
id: 'panel1',
component: 'default',
tabComponent: 'default',
});
const panel2 = dockview.addPanel({
id: 'panel2',
component: 'default',
tabComponent: 'default',
position: {
referencePanel: panel1,
},
});
const panel3 = dockview.addPanel({
id: 'panel3',
component: 'default',
tabComponent: 'default',
position: {
referencePanel: panel1,
direction: 'right',
},
});
const panel1Spy = jest.spyOn(panel1.group, 'dispose');
expect(dockview.groups.length).toBe(2);
dockview.moveGroupOrPanel(
panel3.group,
panel1.group.id,
undefined,
'center'
);
expect(dockview.groups.length).toBe(1);
expect(panel1Spy).toBeCalledTimes(1);
});
test('fromJSON events should still fire', () => {
jest.useFakeTimers();
@ -1635,8 +1631,6 @@ describe('dockviewComponent', () => {
jest.runAllTimers();
console.log(activePanel.map((_) => _?.id).join(' '));
expect(addGroup.length).toBe(4);
expect(removeGroup.length).toBe(0);
expect(activeGroup.length).toBe(1);
@ -1973,4 +1967,466 @@ describe('dockviewComponent', () => {
// load a layout with a default tab identifier when react default is present
// load a layout with invialid panel identifier
test('orthogonal realigment #1', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(container, {
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
orientation: Orientation.HORIZONTAL,
});
dockview.deserializer = new ReactPanelDeserialzier(dockview);
expect(dockview.orientation).toBe(Orientation.HORIZONTAL);
dockview.fromJSON({
activeGroup: 'group-1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 500,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.VERTICAL,
},
panels: {
panel1: {
id: 'panel1',
view: { content: { id: 'default' } },
title: 'panel1',
},
},
});
expect(dockview.orientation).toBe(Orientation.VERTICAL);
dockview.addPanel({
id: 'panel2',
component: 'default',
position: {
direction: 'left',
},
});
expect(dockview.orientation).toBe(Orientation.HORIZONTAL);
expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({
activeGroup: '1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel2'],
id: '1',
activeView: 'panel2',
},
size: 500,
},
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 1000,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.HORIZONTAL,
},
panels: {
panel1: {
id: 'panel1',
view: { content: { id: 'default' } },
title: 'panel1',
},
panel2: {
id: 'panel2',
view: { content: { id: 'default' } },
title: 'panel2',
},
},
options: {},
});
});
test('orthogonal realigment #2', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(container, {
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
orientation: Orientation.HORIZONTAL,
});
dockview.deserializer = new ReactPanelDeserialzier(dockview);
expect(dockview.orientation).toBe(Orientation.HORIZONTAL);
dockview.fromJSON({
activeGroup: 'group-1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 500,
},
{
type: 'leaf',
data: {
views: ['panel2'],
id: 'group-2',
activeView: 'panel2',
},
size: 500,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.VERTICAL,
},
panels: {
panel1: {
id: 'panel1',
view: { content: { id: 'default' } },
title: 'panel1',
},
panel2: {
id: 'panel2',
view: { content: { id: 'default' } },
title: 'panel2',
},
},
});
expect(dockview.orientation).toBe(Orientation.VERTICAL);
dockview.addPanel({
id: 'panel3',
component: 'default',
position: {
direction: 'left',
},
});
expect(dockview.orientation).toBe(Orientation.HORIZONTAL);
expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({
activeGroup: '1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel3'],
id: '1',
activeView: 'panel3',
},
size: 500,
},
{
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 500,
},
{
type: 'leaf',
data: {
views: ['panel2'],
id: 'group-2',
activeView: 'panel2',
},
size: 500,
},
],
size: 500,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.HORIZONTAL,
},
panels: {
panel1: {
id: 'panel1',
view: { content: { id: 'default' } },
title: 'panel1',
},
panel2: {
id: 'panel2',
view: { content: { id: 'default' } },
title: 'panel2',
},
panel3: {
id: 'panel3',
view: { content: { id: 'default' } },
title: 'panel3',
},
},
options: {},
});
});
test('orthogonal realigment #3', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(container, {
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
orientation: Orientation.HORIZONTAL,
});
dockview.deserializer = new ReactPanelDeserialzier(dockview);
expect(dockview.orientation).toBe(Orientation.HORIZONTAL);
dockview.fromJSON({
activeGroup: 'group-1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 500,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.VERTICAL,
},
panels: {
panel1: {
id: 'panel1',
view: { content: { id: 'default' } },
title: 'panel1',
},
},
});
expect(dockview.orientation).toBe(Orientation.VERTICAL);
dockview.addPanel({
id: 'panel2',
component: 'default',
position: {
direction: 'above',
},
});
dockview.addPanel({
id: 'panel3',
component: 'default',
position: {
direction: 'below',
},
});
expect(dockview.orientation).toBe(Orientation.VERTICAL);
expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({
activeGroup: '2',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel2'],
id: '1',
activeView: 'panel2',
},
size: 333,
},
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 333,
},
{
type: 'leaf',
data: {
views: ['panel3'],
id: '2',
activeView: 'panel3',
},
size: 334,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.VERTICAL,
},
panels: {
panel1: {
id: 'panel1',
view: { content: { id: 'default' } },
title: 'panel1',
},
panel2: {
id: 'panel2',
view: { content: { id: 'default' } },
title: 'panel2',
},
panel3: {
id: 'panel3',
view: { content: { id: 'default' } },
title: 'panel3',
},
},
options: {},
});
});
test('that a empty component has no groups', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(container, {
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
orientation: Orientation.HORIZONTAL,
});
dockview.deserializer = new ReactPanelDeserialzier(dockview);
expect(dockview.groups.length).toBe(0);
});
test('that deserializing an empty layout has zero groups and a watermark', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(container, {
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
orientation: Orientation.HORIZONTAL,
});
dockview.deserializer = new ReactPanelDeserialzier(dockview);
expect(dockview.groups.length).toBe(0);
expect(
dockview.element.querySelectorAll('.dv-watermark-container').length
).toBe(1);
dockview.fromJSON({
grid: {
orientation: Orientation.HORIZONTAL,
root: {
type: 'branch',
data: [],
},
height: 100,
width: 100,
},
panels: {},
});
expect(dockview.groups.length).toBe(0);
expect(
dockview.element.querySelectorAll('.dv-watermark-container').length
).toBe(1);
});
test('empty', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(container, {
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
orientation: Orientation.HORIZONTAL,
});
dockview.deserializer = new ReactPanelDeserialzier(dockview);
expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({
grid: {
height: 0,
width: 0,
orientation: Orientation.HORIZONTAL,
root: {
data: [],
type: 'branch',
size: 0,
},
},
options: {},
panels: {},
});
});
});

View File

@ -1,10 +1,10 @@
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { DockviewApi } from '../../api/component.api';
import { IGroupPanelView } from '../../dockview/defaultGroupPanelView';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { DockviewPanel } from '../../dockview/dockviewPanel';
import { GroupPanel } from '../../groupview/groupviewPanel';
describe('dockviewGroupPanel', () => {
describe('dockviewPanel', () => {
test('update title', () => {
const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
return {
@ -20,7 +20,7 @@ describe('dockviewGroupPanel', () => {
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = new groupMock();
const cut = new DockviewGroupPanel('fake-id', accessor, api, group);
const cut = new DockviewPanel('fake-id', accessor, api, group);
let latestTitle: string | undefined = undefined;
@ -55,7 +55,7 @@ describe('dockviewGroupPanel', () => {
const accessor = new accessorMock();
const group = new groupMock();
const cut = new DockviewGroupPanel('fake-id', accessor, api, group);
const cut = new DockviewPanel('fake-id', accessor, api, group);
const viewMock = jest.fn<IGroupPanelView, []>(() => {
return {
@ -86,7 +86,7 @@ describe('dockviewGroupPanel', () => {
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = new groupMock();
const cut = new DockviewGroupPanel('fake-id', accessor, api, group);
const cut = new DockviewPanel('fake-id', accessor, api, group);
expect(cut.params).toEqual(undefined);
@ -94,4 +94,29 @@ describe('dockviewGroupPanel', () => {
expect(cut.params).toEqual({ variableA: 'A', variableB: 'B' });
});
test('setSize propagates to underlying group', () => {
const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
return {} as any;
});
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const groupMock = jest.fn<GroupPanel, []>(() => {
return {
api: {
setSize: jest.fn(),
},
} as any;
});
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = new groupMock();
const cut = new DockviewPanel('fake-id', accessor, api, group);
cut.api.setSize({ height: 123, width: 456 });
expect(group.api.setSize).toBeCalledWith({ height: 123, width: 456 });
expect(group.api.setSize).toBeCalledTimes(1);
});
});

View File

@ -1,3 +1,4 @@
import { GridviewPanelApiImpl } from '../../api/gridviewPanelApi';
import { GridviewComponent } from '../../gridview/gridviewComponent';
import { GridviewPanel } from '../../gridview/gridviewPanel';
import { CompositeDisposable } from '../../lifecycle';
@ -6,7 +7,9 @@ import { Orientation } from '../../splitview/core/splitview';
class TestGridview extends GridviewPanel {
constructor(id: string, componentName: string) {
super(id, componentName);
super(id, componentName, new GridviewPanelApiImpl(id));
this.api.initialize(this);
this.element.id = id;
}
@ -65,7 +68,7 @@ describe('gridview', () => {
expect(gridview.size).toBe(1);
const panel1 = gridview.getPanel('panel1');
const panel1 = gridview.getPanel('panel1')!;
gridview.removePanel(panel1);
@ -101,9 +104,9 @@ describe('gridview', () => {
component: 'default',
});
const panel1 = gridview.getPanel('panel1');
const panel2 = gridview.getPanel('panel2');
const panel3 = gridview.getPanel('panel3');
const panel1 = gridview.getPanel('panel1')!;
const panel2 = gridview.getPanel('panel2')!;
const panel3 = gridview.getPanel('panel3')!;
expect(panel1.api.isActive).toBeFalsy();
expect(panel2.api.isActive).toBeFalsy();
@ -192,9 +195,9 @@ describe('gridview', () => {
});
gridview.layout(800, 400, true);
const panel1 = gridview.getPanel('panel_1');
const panel2 = gridview.getPanel('panel_2');
const panel3 = gridview.getPanel('panel_3');
const panel1 = gridview.getPanel('panel_1')!;
const panel2 = gridview.getPanel('panel_2')!;
const panel3 = gridview.getPanel('panel_3')!;
expect(panel1?.api.isVisible).toBeTruthy();
expect(panel1?.api.id).toBe('panel_1');
@ -330,7 +333,7 @@ describe('gridview', () => {
component: 'default',
});
const panel1 = gridview.getPanel('panel_1');
const panel1 = gridview.getPanel('panel_1')!;
expect(events).toEqual([
{
@ -349,7 +352,7 @@ describe('gridview', () => {
component: 'default',
});
const panel2 = gridview.getPanel('panel_2');
const panel2 = gridview.getPanel('panel_2')!;
expect(events).toEqual([
{
@ -368,7 +371,7 @@ describe('gridview', () => {
component: 'default',
});
const panel3 = gridview.getPanel('panel_3');
const panel3 = gridview.getPanel('panel_3')!;
expect(events).toEqual([
{
@ -1685,8 +1688,8 @@ describe('gridview', () => {
component: 'default',
});
const panel1 = gridview.getPanel('panel1');
const panel2 = gridview.getPanel('panel2');
const panel1 = gridview.getPanel('panel1')!;
const panel2 = gridview.getPanel('panel2')!;
const panel1Spy = jest.spyOn(panel1, 'dispose');
const panel2Spy = jest.spyOn(panel2, 'dispose');
@ -1714,8 +1717,8 @@ describe('gridview', () => {
component: 'default',
});
const panel1 = gridview.getPanel('panel1');
const panel2 = gridview.getPanel('panel2');
const panel1 = gridview.getPanel('panel1')!;
const panel2 = gridview.getPanel('panel2')!;
const panel1Spy = jest.spyOn(panel1, 'dispose');
const panel2Spy = jest.spyOn(panel2, 'dispose');
@ -1743,8 +1746,8 @@ describe('gridview', () => {
component: 'default',
});
const panel1 = gridview.getPanel('panel1');
const panel2 = gridview.getPanel('panel2');
const panel1 = gridview.getPanel('panel1')!;
const panel2 = gridview.getPanel('panel2')!;
const panel1Spy = jest.spyOn(panel1, 'dispose');
const panel2Spy = jest.spyOn(panel2, 'dispose');

View File

@ -4,7 +4,11 @@ import { GroupPanel } from '../../groupview/groupviewPanel';
describe('gridviewPanel', () => {
test('get panel', () => {
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
return {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
} as any;
});
const accessor = new accessorMock();

View File

@ -1,9 +1,8 @@
import { DockviewComponent } from '../../dockview/dockviewComponent';
import {
GroupviewPanelState,
IDockviewPanel,
IGroupPanelInitParameters,
} from '../../groupview/groupPanel';
} from '../../groupview/types';
import {
GroupPanelPartInitParameters,
IContentRenderer,
@ -12,7 +11,6 @@ import {
} from '../../groupview/types';
import { PanelUpdateEvent } from '../../panel/types';
import { GroupOptions, Groupview } from '../../groupview/groupview';
import { DockviewPanelApi } from '../../api/groupPanelApi';
import {
DefaultGroupPanelView,
IGroupPanelView,
@ -21,6 +19,8 @@ import { GroupPanel } from '../../groupview/groupviewPanel';
import { fireEvent } from '@testing-library/dom';
import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer';
import { CompositeDisposable } from '../../lifecycle';
import { DockviewPanelApi } from '../../api/dockviewPanelApi';
import { IDockviewPanel } from '../../dockview/dockviewPanel';
enum GroupChangeKind2 {
ADD_PANEL,
@ -225,6 +225,8 @@ describe('groupview', () => {
id: 'dockview-1',
removePanel: removePanelMock,
removeGroup: removeGroupMock,
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
}) as DockviewComponent;
options = {
@ -616,6 +618,8 @@ describe('groupview', () => {
showDndOverlay: jest.fn(),
},
getPanel: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
};
});
const accessor = new accessorMock() as DockviewComponent;
@ -671,6 +675,8 @@ describe('groupview', () => {
},
getPanel: jest.fn(),
doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
};
});
const accessor = new accessorMock() as DockviewComponent;
@ -724,7 +730,7 @@ describe('groupview', () => {
).toBe(0);
});
test('that should allow drop when not dropping on self for same component id', () => {
test('that should not allow drop when dropping on self for same component id', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
@ -733,6 +739,8 @@ describe('groupview', () => {
},
getPanel: jest.fn(),
doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
};
});
const accessor = new accessorMock() as DockviewComponent;
@ -784,7 +792,7 @@ describe('groupview', () => {
expect(
element.getElementsByClassName('drop-target-dropzone').length
).toBe(1);
).toBe(0);
});
test('that should not allow drop when not dropping for different component id', () => {
@ -796,6 +804,8 @@ describe('groupview', () => {
},
getPanel: jest.fn(),
doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
};
});
const accessor = new accessorMock() as DockviewComponent;

View File

@ -8,8 +8,8 @@ import {
import { CompositeDisposable } from '../../../lifecycle';
import { PanelUpdateEvent } from '../../../panel/types';
import { IGroupPanelView } from '../../../dockview/defaultGroupPanelView';
import { IDockviewPanel } from '../../../groupview/groupPanel';
import { GroupPanel } from '../../../groupview/groupviewPanel';
import { IDockviewPanel } from '../../../dockview/dockviewPanel';
class TestContentRenderer
extends CompositeDisposable

View File

@ -12,7 +12,11 @@ import { TestPanel } from '../groupview.spec';
describe('tabsContainer', () => {
test('that an external event does not render a drop target and calls through to the group mode', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {};
return {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
});
const groupviewMock = jest.fn<Partial<Groupview>, []>(() => {
return {
@ -31,7 +35,7 @@ describe('tabsContainer', () => {
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as GroupPanel;
const cut = new TabsContainer(accessor, groupPanel, {});
const cut = new TabsContainer(accessor, groupPanel);
const emptySpace = cut.element
.getElementsByClassName('void-container')
@ -62,6 +66,9 @@ describe('tabsContainer', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
});
const groupviewMock = jest.fn<Partial<Groupview>, []>(() => {
@ -76,13 +83,14 @@ describe('tabsContainer', () => {
return {
id: 'testgroupid',
model: groupView,
panels: [],
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as GroupPanel;
const cut = new TabsContainer(accessor, groupPanel, {});
const cut = new TabsContainer(accessor, groupPanel);
const emptySpace = cut.element
.getElementsByClassName('void-container')
@ -120,10 +128,13 @@ describe('tabsContainer', () => {
).toBe(1);
});
test('that dropping the last tab should render no drop target', () => {
test('that dropping over the empty space should render a drop target', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
});
const groupviewMock = jest.fn<Partial<Groupview>, []>(() => {
@ -138,13 +149,14 @@ describe('tabsContainer', () => {
return {
id: 'testgroupid',
model: groupView,
panels: [],
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as GroupPanel;
const cut = new TabsContainer(accessor, groupPanel, {});
const cut = new TabsContainer(accessor, groupPanel);
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
@ -176,13 +188,16 @@ describe('tabsContainer', () => {
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(0);
).toBe(1);
});
test('that dropping the first tab should render a drop target', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
});
const groupviewMock = jest.fn<Partial<Groupview>, []>(() => {
@ -197,13 +212,14 @@ describe('tabsContainer', () => {
return {
id: 'testgroupid',
model: groupView,
panels: [],
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as GroupPanel;
const cut = new TabsContainer(accessor, groupPanel, {});
const cut = new TabsContainer(accessor, groupPanel);
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
@ -242,6 +258,9 @@ describe('tabsContainer', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
});
const groupviewMock = jest.fn<Partial<Groupview>, []>(() => {
@ -262,7 +281,7 @@ describe('tabsContainer', () => {
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as GroupPanel;
const cut = new TabsContainer(accessor, groupPanel, {});
const cut = new TabsContainer(accessor, groupPanel);
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));

View File

@ -15,7 +15,7 @@ import {
SerializedGridview,
} from '../gridview/gridviewComponent';
import { IGridviewPanel } from '../gridview/gridviewPanel';
import { IDockviewPanel } from '../groupview/groupPanel';
import {
AddPaneviewComponentOptions,
SerializedPaneview,
@ -33,6 +33,7 @@ import { ISplitviewPanel } from '../splitview/splitviewPanel';
import { GroupPanel, IGroupviewPanel } from '../groupview/groupviewPanel';
import { Emitter, Event } from '../events';
import { PaneviewDropEvent } from '../react';
import { IDockviewPanel } from '../dockview/dockviewPanel';
export interface CommonApi<T = any> {
readonly height: number;
@ -325,6 +326,10 @@ export class GridviewApi implements CommonApi<SerializedGridview> {
}
export class DockviewApi implements CommonApi<SerializedDockview> {
get id(): string {
return this.component.id;
}
get width(): number {
return this.component.width;
}
@ -435,8 +440,8 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.addPanel(options);
}
addEmptyGroup(options?: AddGroupOptions): void {
this.component.addEmptyGroup(options);
addGroup(options?: AddGroupOptions): IGroupviewPanel {
return this.component.addGroup(options);
}
moveToNext(options?: MovementOptions): void {

View File

@ -1,8 +1,8 @@
import { Emitter, Event } from '../events';
import { GridviewPanelApiImpl, GridviewPanelApi } from './gridviewPanelApi';
import { IDockviewPanel } from '../groupview/groupPanel';
import { GroupPanel } from '../groupview/groupviewPanel';
import { MutableDisposable } from '../lifecycle';
import { IDockviewPanel } from '../dockview/dockviewPanel';
export interface TitleEvent {
readonly title: string;
@ -37,13 +37,13 @@ export class DockviewPanelApiImpl
private readonly _onDidGroupChange = new Emitter<void>();
readonly onDidGroupChange = this._onDidGroupChange.event;
private disposable = new MutableDisposable();
private readonly disposable = new MutableDisposable();
get title() {
get title(): string {
return this.panel.title;
}
get isGroupActive() {
get isGroupActive(): boolean {
return !!this.group?.isActive;
}
@ -71,6 +71,9 @@ export class DockviewPanelApiImpl
constructor(private panel: IDockviewPanel, group: GroupPanel) {
super(panel.id);
this.initialize(panel);
this._group = group;
this.addDisposables(
@ -81,14 +84,11 @@ export class DockviewPanelApiImpl
);
}
public setTitle(title: string) {
this._onDidTitleChange.fire({ title });
public setTitle(title: string): void {
this.panel.update({ params: { title } });
}
public close(): void {
if (!this.group) {
throw new Error(`panel ${this.id} has no group`);
}
return this.group.model.closePanel(this.panel);
this.group.model.closePanel(this.panel);
}
}

View File

@ -1,4 +1,5 @@
import { Emitter, Event } from '../events';
import { IPanel } from '../panel/types';
import { FunctionOrValue } from '../types';
import { PanelApiImpl, PanelApi } from './panelApi';
@ -31,24 +32,21 @@ export class GridviewPanelApiImpl
extends PanelApiImpl
implements GridviewPanelApi
{
readonly _onDidConstraintsChangeInternal =
private readonly _onDidConstraintsChangeInternal =
new Emitter<GridConstraintChangeEvent2>();
readonly onDidConstraintsChangeInternal: Event<GridConstraintChangeEvent2> =
this._onDidConstraintsChangeInternal.event;
//
readonly _onDidConstraintsChange = new Emitter<GridConstraintChangeEvent>({
replay: true,
});
readonly onDidConstraintsChange: Event<GridConstraintChangeEvent> =
this._onDidConstraintsChange.event;
//
readonly _onDidSizeChange = new Emitter<SizeEvent>();
private readonly _onDidSizeChange = new Emitter<SizeEvent>();
readonly onDidSizeChange: Event<SizeEvent> = this._onDidSizeChange.event;
//
constructor(id: string) {
constructor(id: string, panel?: IPanel) {
super(id);
this.addDisposables(
@ -56,13 +54,17 @@ export class GridviewPanelApiImpl
this._onDidConstraintsChange,
this._onDidSizeChange
);
if (panel) {
this.initialize(panel);
}
}
public setConstraints(value: GridConstraintChangeEvent) {
public setConstraints(value: GridConstraintChangeEvent): void {
this._onDidConstraintsChangeInternal.fire(value);
}
public setSize(event: SizeEvent) {
public setSize(event: SizeEvent): void {
this._onDidSizeChange.fire(event);
}
}

View File

@ -1,5 +1,6 @@
import { Emitter, Event } from '../events';
import { CompositeDisposable } from '../lifecycle';
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { IPanel, Parameters } from '../panel/types';
export interface FocusEvent {
readonly isFocused: boolean;
@ -25,6 +26,7 @@ export interface PanelApi {
readonly onDidActiveChange: Event<ActiveEvent>;
setVisible(isVisible: boolean): void;
setActive(): void;
updateParameters(parameters: Parameters): void;
/**
* The id of the panel that would have been assigned when the panel was created
*/
@ -61,6 +63,8 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
private _width = 0;
private _height = 0;
private readonly panelUpdatesDisposable = new MutableDisposable();
readonly _onDidDimensionChange = new Emitter<PanelDimensionChangeEvent>({
replay: true,
});
@ -94,6 +98,10 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
readonly _onActiveChange = new Emitter<void>();
readonly onActiveChange: Event<void> = this._onActiveChange.event;
//
readonly _onUpdateParameters = new Emitter<Parameters>();
readonly onUpdateParameters: Event<Parameters> =
this._onUpdateParameters.event;
//
get isFocused() {
return this._isFocused;
@ -118,6 +126,7 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
super();
this.addDisposables(
this.panelUpdatesDisposable,
this._onDidDimensionChange,
this._onDidChangeFocus,
this._onDidVisibilityChange,
@ -125,6 +134,7 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
this._onFocusEvent,
this._onActiveChange,
this._onVisibilityChange,
this._onUpdateParameters,
this.onDidFocusChange((event) => {
this._isFocused = event.isFocused;
}),
@ -141,6 +151,18 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
);
}
public initialize(panel: IPanel): void {
this.panelUpdatesDisposable.value = this._onUpdateParameters.event(
(parameters) => {
panel.update({
params: {
params: parameters,
},
});
}
);
}
setVisible(isVisible: boolean) {
this._onVisibilityChange.fire({ isVisible });
}
@ -149,6 +171,10 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
this._onActiveChange.fire();
}
updateParameters(parameters: Parameters): void {
this._onUpdateParameters.fire(parameters);
}
dispose() {
super.dispose();
}

View File

@ -21,11 +21,17 @@ export { PaneviewComponentOptions } from './paneview/options';
export * from './gridview/gridviewPanel';
export * from './splitview/splitviewPanel';
export * from './paneview/paneviewPanel';
export * from './groupview/groupPanel';
export * from './groupview/types';
export * from './react'; // TODO: should be conditional on whether user wants the React wrappers
export { Event } from './events';
export { IDisposable } from './lifecycle';
export { Position, Droptarget } from './dnd/droptarget';
export {
Position,
positionToDirection,
directionToPosition,
} from './dnd/droptarget';
export {
FocusEvent,
PanelDimensionChangeEvent,
@ -38,7 +44,7 @@ export {
GridviewPanelApi,
GridConstraintChangeEvent,
} from './api/gridviewPanelApi';
export { TitleEvent, DockviewPanelApi } from './api/groupPanelApi';
export { TitleEvent, DockviewPanelApi } from './api/dockviewPanelApi';
export {
PanelSizeEvent,
PanelConstraintChangeEvent,

View File

@ -14,12 +14,12 @@ export abstract class DragHandler extends CompositeDisposable {
private iframes: HTMLElement[] = [];
constructor(private readonly el: HTMLElement) {
constructor(protected readonly el: HTMLElement) {
super();
this.configure();
}
abstract getData(): IDisposable;
abstract getData(dataTransfer?: DataTransfer | null): IDisposable;
private configure() {
this.addDisposables(
@ -34,10 +34,10 @@ export abstract class DragHandler extends CompositeDisposable {
iframe.style.pointerEvents = 'none';
}
this.el.classList.add('dragged');
setTimeout(() => this.el.classList.remove('dragged'), 0);
this.el.classList.add('dv-dragged');
setTimeout(() => this.el.classList.remove('dv-dragged'), 0);
this.disposable.value = this.getData();
this.disposable.value = this.getData(event.dataTransfer);
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';

View File

@ -8,7 +8,7 @@ export class PanelTransfer extends TransferObject {
constructor(
public readonly viewId: string,
public readonly groupId: string,
public readonly panelId: string
public readonly panelId: string | null
) {
super();
}

View File

@ -6,16 +6,11 @@ export interface IDragAndDropObserverCallbacks {
onDragLeave: (e: DragEvent) => void;
onDrop: (e: DragEvent) => void;
onDragEnd: (e: DragEvent) => void;
onDragOver?: (e: DragEvent) => void;
}
export class DragAndDropObserver extends CompositeDisposable {
// A helper to fix issues with repeated DRAG_ENTER / DRAG_LEAVE
// calls see https://github.com/microsoft/vscode/issues/14470
// when the element has child elements where the events are fired
// repeadedly.
private counter = 0;
private target: EventTarget | null = null;
constructor(
private element: HTMLElement,
@ -28,28 +23,37 @@ export class DragAndDropObserver extends CompositeDisposable {
private registerListeners(): void {
this.addDisposables(
addDisposableListener(this.element, 'dragenter', (e: DragEvent) => {
this.counter++;
this.callbacks.onDragEnter(e);
})
addDisposableListener(
this.element,
'dragenter',
(e: DragEvent) => {
this.target = e.target;
this.callbacks.onDragEnter(e);
},
true
)
);
this.addDisposables(
addDisposableListener(this.element, 'dragover', (e: DragEvent) => {
e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
addDisposableListener(
this.element,
'dragover',
(e: DragEvent) => {
e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
if (this.callbacks.onDragOver) {
this.callbacks.onDragOver(e);
}
})
if (this.callbacks.onDragOver) {
this.callbacks.onDragOver(e);
}
},
true
)
);
this.addDisposables(
addDisposableListener(this.element, 'dragleave', (e: DragEvent) => {
this.counter--;
if (this.target === e.target) {
this.target = null;
if (this.counter === 0) {
this.callbacks.onDragLeave(e);
}
})
@ -57,14 +61,13 @@ export class DragAndDropObserver extends CompositeDisposable {
this.addDisposables(
addDisposableListener(this.element, 'dragend', (e: DragEvent) => {
this.counter = 0;
this.target = null;
this.callbacks.onDragEnd(e);
})
);
this.addDisposables(
addDisposableListener(this.element, 'drop', (e: DragEvent) => {
this.counter = 0;
this.callbacks.onDrop(e);
})
);

View File

@ -19,22 +19,6 @@
will-change: transform;
pointer-events: none;
&.left {
transform: translateX(-25%) scaleX(0.5)
}
&.right {
transform: translateX(25%) scaleX(0.5)
}
&.top {
transform: translateY(-25%) scaleY(0.5);
}
&.bottom {
transform: translateY(25%) scaleY(0.5);
}
&.small-top {
border-top: 1px solid var(--dv-drag-over-border-color);
}

View File

@ -2,33 +2,58 @@ import { toggleClass } from '../dom';
import { Emitter, Event } from '../events';
import { CompositeDisposable } from '../lifecycle';
import { DragAndDropObserver } from './dnd';
import { clamp } from '../math';
import { Direction } from '../gridview/baseComponentGridview';
import { isBooleanValue } from '../types';
export enum Position {
Top = 'Top',
Left = 'Left',
Bottom = 'Bottom',
Right = 'Right',
Center = 'Center',
function numberOrFallback(maybeNumber: any, fallback: number): number {
return typeof maybeNumber === 'number' ? maybeNumber : fallback;
}
export type Quadrant = 'top' | 'bottom' | 'left' | 'right';
export function directionToPosition(direction: Direction): Position {
switch (direction) {
case 'above':
return 'top';
case 'below':
return 'bottom';
case 'left':
return 'left';
case 'right':
return 'right';
case 'within':
return 'center';
default:
throw new Error(`invalid direction '${direction}'`);
}
}
export function positionToDirection(position: Position): Direction {
switch (position) {
case 'top':
return 'above';
case 'bottom':
return 'below';
case 'left':
return 'left';
case 'right':
return 'right';
case 'center':
return 'within';
default:
throw new Error(`invalid position '${position}'`);
}
}
export interface DroptargetEvent {
position: Position;
nativeEvent: DragEvent;
readonly position: Position;
readonly nativeEvent: DragEvent;
}
export type DropTargetDirections = 'vertical' | 'horizontal' | 'all' | 'none';
function isBooleanValue(
canDisplayOverlay: CanDisplayOverlay
): canDisplayOverlay is boolean {
return typeof canDisplayOverlay === 'boolean';
}
export type Position = 'top' | 'bottom' | 'left' | 'right' | 'center';
export type CanDisplayOverlay =
| boolean
| ((dragEvent: DragEvent, state: Quadrant | null) => boolean);
| ((dragEvent: DragEvent, state: Position) => boolean);
export class Droptarget extends CompositeDisposable {
private target: HTMLElement | undefined;
@ -38,27 +63,31 @@ export class Droptarget extends CompositeDisposable {
private readonly _onDrop = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
get state() {
get state(): Position | undefined {
return this._state;
}
set validOverlays(value: DropTargetDirections) {
this.options.validOverlays = value;
}
set canDisplayOverlay(value: CanDisplayOverlay) {
this.options.canDisplayOverlay = value;
}
constructor(
private readonly element: HTMLElement,
private readonly options: {
canDisplayOverlay: CanDisplayOverlay;
validOverlays: DropTargetDirections;
acceptedTargetZones: Position[];
overlayModel?: {
size?: { value: number; type: 'pixels' | 'percentage' };
activationSize?: {
value: number;
type: 'pixels' | 'percentage';
};
};
}
) {
super();
// use a set to take advantage of #<set>.has
const acceptedTargetZonesSet = new Set(
this.options.acceptedTargetZones
);
this.addDisposables(
this._onDrop,
new DragAndDropObserver(this.element, {
@ -71,17 +100,26 @@ export class Droptarget extends CompositeDisposable {
return; // avoid div!0
}
const x = e.offsetX;
const y = e.offsetY;
const xp = (100 * x) / width;
const yp = (100 * y) / height;
const rect = (
e.currentTarget as HTMLElement
).getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const quadrant = this.calculateQuadrant(
this.options.validOverlays,
xp,
yp
acceptedTargetZonesSet,
x,
y,
width,
height
);
if (quadrant === null) {
// no drop target should be displayed
this.removeDropTarget();
return;
}
if (isBooleanValue(this.options.canDisplayOverlay)) {
if (!this.options.canDisplayOverlay) {
return;
@ -95,14 +133,14 @@ export class Droptarget extends CompositeDisposable {
this.target.className = 'drop-target-dropzone';
this.overlay = document.createElement('div');
this.overlay.className = 'drop-target-selection';
this._state = Position.Center;
this._state = 'center';
this.target.appendChild(this.overlay);
this.element.classList.add('drop-target');
this.element.append(this.target);
}
if (this.options.validOverlays === 'none') {
if (this.options.acceptedTargetZones.length === 0) {
return;
}
@ -110,10 +148,7 @@ export class Droptarget extends CompositeDisposable {
return;
}
const isSmallX = width < 100;
const isSmallY = height < 100;
this.toggleClasses(quadrant, isSmallX, isSmallY);
this.toggleClasses(quadrant, width, height);
this.setState(quadrant);
},
@ -139,28 +174,69 @@ export class Droptarget extends CompositeDisposable {
);
}
public dispose() {
public dispose(): void {
this.removeDropTarget();
}
private toggleClasses(
quadrant: Quadrant | null,
isSmallX: boolean,
isSmallY: boolean
) {
quadrant: Position,
width: number,
height: number
): void {
if (!this.overlay) {
return;
}
const isSmallX = width < 100;
const isSmallY = height < 100;
const isLeft = quadrant === 'left';
const isRight = quadrant === 'right';
const isTop = quadrant === 'top';
const isBottom = quadrant === 'bottom';
toggleClass(this.overlay, 'right', !isSmallX && isRight);
toggleClass(this.overlay, 'left', !isSmallX && isLeft);
toggleClass(this.overlay, 'top', !isSmallY && isTop);
toggleClass(this.overlay, 'bottom', !isSmallY && isBottom);
const rightClass = !isSmallX && isRight;
const leftClass = !isSmallX && isLeft;
const topClass = !isSmallY && isTop;
const bottomClass = !isSmallY && isBottom;
let size = 0.5;
if (this.options.overlayModel?.size?.type === 'percentage') {
size = clamp(this.options.overlayModel.size.value, 0, 100) / 100;
}
if (this.options.overlayModel?.size?.type === 'pixels') {
if (rightClass || leftClass) {
size =
clamp(0, this.options.overlayModel.size.value, width) /
width;
}
if (topClass || bottomClass) {
size =
clamp(0, this.options.overlayModel.size.value, height) /
height;
}
}
const translate = (1 - size) / 2;
const scale = size;
let transform: string;
if (rightClass) {
transform = `translateX(${100 * translate}%) scaleX(${scale})`;
} else if (leftClass) {
transform = `translateX(-${100 * translate}%) scaleX(${scale})`;
} else if (topClass) {
transform = `translateY(-${100 * translate}%) scaleY(${scale})`;
} else if (bottomClass) {
transform = `translateY(${100 * translate}%) scaleY(${scale})`;
} else {
transform = '';
}
this.overlay.style.transform = transform;
toggleClass(this.overlay, 'small-right', isSmallX && isRight);
toggleClass(this.overlay, 'small-left', isSmallX && isLeft);
@ -168,68 +244,129 @@ export class Droptarget extends CompositeDisposable {
toggleClass(this.overlay, 'small-bottom', isSmallY && isBottom);
}
private setState(quadrant: Quadrant | null) {
private setState(quadrant: Position): void {
switch (quadrant) {
case 'top':
this._state = Position.Top;
this._state = 'top';
break;
case 'left':
this._state = Position.Left;
this._state = 'left';
break;
case 'bottom':
this._state = Position.Bottom;
this._state = 'bottom';
break;
case 'right':
this._state = Position.Right;
this._state = 'right';
break;
default:
this._state = Position.Center;
case 'center':
this._state = 'center';
break;
}
}
private calculateQuadrant(
overlayType: DropTargetDirections,
xp: number,
yp: number
): Quadrant | null {
switch (overlayType) {
case 'all':
if (xp < 20) {
return 'left';
}
if (xp > 80) {
return 'right';
}
if (yp < 20) {
return 'top';
}
if (yp > 80) {
return 'bottom';
}
break;
case 'vertical':
if (yp < 50) {
return 'top';
}
return 'bottom';
overlayType: Set<Position>,
x: number,
y: number,
width: number,
height: number
): Position | null {
const isPercentage =
this.options.overlayModel?.activationSize === undefined ||
this.options.overlayModel?.activationSize?.type === 'percentage';
case 'horizontal':
if (xp < 50) {
return 'left';
}
return 'right';
const value = numberOrFallback(
this.options?.overlayModel?.activationSize?.value,
20
);
if (isPercentage) {
return calculateQuadrantAsPercentage(
overlayType,
x,
y,
width,
height,
value
);
}
return null;
return calculateQuadrantAsPixels(
overlayType,
x,
y,
width,
height,
value
);
}
private removeDropTarget() {
private removeDropTarget(): void {
if (this.target) {
this._state = undefined;
this.element.removeChild(this.target);
this.target = undefined;
this.overlay = undefined;
this.element.classList.remove('drop-target');
}
}
}
export function calculateQuadrantAsPercentage(
overlayType: Set<Position>,
x: number,
y: number,
width: number,
height: number,
threshold: number
): Position | null {
const xp = (100 * x) / width;
const yp = (100 * y) / height;
if (overlayType.has('left') && xp < threshold) {
return 'left';
}
if (overlayType.has('right') && xp > 100 - threshold) {
return 'right';
}
if (overlayType.has('top') && yp < threshold) {
return 'top';
}
if (overlayType.has('bottom') && yp > 100 - threshold) {
return 'bottom';
}
if (!overlayType.has('center')) {
return null;
}
return 'center';
}
export function calculateQuadrantAsPixels(
overlayType: Set<Position>,
x: number,
y: number,
width: number,
height: number,
threshold: number
): Position | null {
if (overlayType.has('left') && x < threshold) {
return 'left';
}
if (overlayType.has('right') && x > width - threshold) {
return 'right';
}
if (overlayType.has('top') && y < threshold) {
return 'top';
}
if (overlayType.has('bottom') && y > height - threshold) {
return 'bottom';
}
if (!overlayType.has('center')) {
return null;
}
return 'center';
}

View File

@ -0,0 +1,16 @@
import { addClasses } from '../dom';
export function addGhostImage(
dataTransfer: DataTransfer,
ghostElement: HTMLElement
) {
// class dockview provides to force ghost image to be drawn on a different layer and prevent weird rendering issues
addClasses(ghostElement, 'dv-dragged');
document.body.appendChild(ghostElement);
dataTransfer.setDragImage(ghostElement, 0, 0);
setTimeout(() => {
ghostElement.remove();
}, 0);
}

View File

@ -0,0 +1,60 @@
import { GroupPanel } from '../groupview/groupviewPanel';
import { IDisposable } from '../lifecycle';
import { DragHandler } from './abstractDragHandler';
import { LocalSelectionTransfer, PanelTransfer } from './dataTransfer';
import { addGhostImage } from './ghost';
export class GroupDragHandler extends DragHandler {
private readonly panelTransfer =
LocalSelectionTransfer.getInstance<PanelTransfer>();
constructor(
element: HTMLElement,
private readonly accessorId: string,
private readonly group: GroupPanel
) {
super(element);
}
getData(dataTransfer: DataTransfer | null): IDisposable {
this.panelTransfer.setData(
[new PanelTransfer(this.accessorId, this.group.id, null)],
PanelTransfer.prototype
);
const style = window.getComputedStyle(this.el);
const bgColor = style.getPropertyValue(
'--dv-activegroup-visiblepanel-tab-background-color'
);
const color = style.getPropertyValue(
'--dv-activegroup-visiblepanel-tab-color'
);
if (dataTransfer) {
const ghostElement = document.createElement('div');
ghostElement.style.backgroundColor = bgColor;
ghostElement.style.color = color;
ghostElement.style.padding = '2px 8px';
ghostElement.style.height = '24px';
ghostElement.style.fontSize = '11px';
ghostElement.style.lineHeight = '20px';
ghostElement.style.borderRadius = '12px';
ghostElement.style.position = 'absolute';
ghostElement.textContent = `Multiple Panels (${this.group.size})`;
addGhostImage(dataTransfer, ghostElement);
}
return {
dispose: () => {
this.panelTransfer.clearData(PanelTransfer.prototype);
},
};
}
public dispose(): void {
//
}
}

View File

@ -1,4 +1,4 @@
.dragged {
.dv-dragged {
transform: translate3d(
0px,
0px,
@ -9,7 +9,7 @@
.tab {
flex-shrink: 0;
&.dragging {
&.dv-tab-dragging {
.tab-action {
background-color: var(--dv-activegroup-visiblepanel-tab-color);
}

View File

@ -79,7 +79,8 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer {
public init(params: GroupPanelPartInitParameters) {
this.params = params;
this._content.textContent = params.title;
this._content.textContent =
typeof params.title === 'string' ? params.title : this.id;
addDisposableListener(this.action, 'click', (ev) => {
ev.preventDefault(); //
@ -106,7 +107,10 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer {
private render() {
if (this._content.textContent !== this.params.title) {
this._content.textContent = this.params.title;
this._content.textContent =
typeof this.params.title === 'string'
? this.params.title
: this.id;
}
}
}

View File

@ -6,7 +6,7 @@ import {
} from '../groupview/types';
import { GroupPanel } from '../groupview/groupviewPanel';
import { IDisposable } from '../lifecycle';
import { GroupPanelUpdateEvent } from '../groupview/groupPanel';
import { GroupPanelUpdateEvent } from '../groupview/types';
export interface IGroupPanelView extends IDisposable {
readonly content: IContentRenderer;

View File

@ -1,5 +1,6 @@
import { GroupviewPanelState, IDockviewPanel } from '../groupview/groupPanel';
import { GroupviewPanelState } from '../groupview/types';
import { GroupPanel } from '../groupview/groupviewPanel';
import { IDockviewPanel } from './dockviewPanel';
export interface IPanelDeserializer {
fromJSON(panelData: GroupviewPanelState, group: GroupPanel): IDockviewPanel;

View File

@ -1,13 +1,14 @@
.custom-dragging {
height: 24px;
line-height: 24px;
font-size: 11px;
width: 100px;
background-color: dodgerblue;
color: ghostwhite;
border-radius: 11px;
.dv-dockview {
position: relative;
background-color: var(--dv-group-view-background-color);
.dv-watermark-container {
position: absolute;
padding-left: 10px;
top: 0px;
left: 0px;
height: 100%;
width: 100%;
}
}
.groupview {
@ -50,7 +51,7 @@
* therefore we also set some stylings for the dragging event
**/
.tab {
&.dragging {
&.dv-tab-dragging {
background-color: var(
--dv-activegroup-visiblepanel-tab-background-color
);

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,35 @@
import { DockviewApi } from '../api/component.api';
import { DockviewPanelApiImpl } from '../api/groupPanelApi';
import {
DockviewPanelApi,
DockviewPanelApiImpl,
} from '../api/dockviewPanelApi';
import {
GroupPanelUpdateEvent,
GroupviewPanelState,
IDockviewPanel,
IGroupPanelInitParameters,
} from '../groupview/groupPanel';
} from '../groupview/types';
import { GroupPanel } from '../groupview/groupviewPanel';
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { Parameters } from '../panel/types';
import { CompositeDisposable, IDisposable } from '../lifecycle';
import { IPanel, Parameters } from '../panel/types';
import { IGroupPanelView } from './defaultGroupPanelView';
import { DockviewComponent } from './dockviewComponent';
export class DockviewGroupPanel
export interface IDockviewPanel extends IDisposable, IPanel {
readonly view?: IGroupPanelView;
readonly group: GroupPanel;
readonly api: DockviewPanelApi;
readonly title: string;
readonly params: Record<string, any> | undefined;
updateParentGroup(group: GroupPanel, isGroupActive: boolean): void;
init(params: IGroupPanelInitParameters): void;
toJSON(): GroupviewPanelState;
update(event: GroupPanelUpdateEvent): void;
}
export class DockviewPanel
extends CompositeDisposable
implements IDockviewPanel
{
private readonly mutableDisposable = new MutableDisposable();
readonly api: DockviewPanelApiImpl;
private _group: GroupPanel;
private _params?: Parameters;
@ -26,11 +38,11 @@ export class DockviewGroupPanel
private _title: string;
get params() {
get params(): Parameters | undefined {
return this._params;
}
get title() {
get title(): string {
return this._title;
}
@ -38,7 +50,7 @@ export class DockviewGroupPanel
return this._group;
}
get view() {
get view(): IGroupPanelView | undefined {
return this._view;
}
@ -57,6 +69,11 @@ export class DockviewGroupPanel
this.addDisposables(
this.api.onActiveChange(() => {
accessor.setActivePanel(this);
}),
this.api.onDidSizeChange((event) => {
// forward the resize event to the group since if you want to resize a panel
// you are actually just resizing the panels parent which is the group
this.group.api.setSize(event);
})
);
}
@ -65,7 +82,9 @@ export class DockviewGroupPanel
this._params = params.params;
this._view = params.view;
this.setTitle(params.title);
if (typeof params.title === 'string') {
this.setTitle(params.title);
}
this.view?.init({
...params,
@ -74,7 +93,7 @@ export class DockviewGroupPanel
});
}
focus() {
focus(): void {
this.api._onFocusEvent.fire();
}
@ -90,7 +109,7 @@ export class DockviewGroupPanel
};
}
setTitle(title: string) {
setTitle(title: string): void {
const didTitleChange = title !== this._params?.title;
if (didTitleChange) {
@ -129,7 +148,7 @@ export class DockviewGroupPanel
});
}
public updateParentGroup(group: GroupPanel, isGroupActive: boolean) {
public updateParentGroup(group: GroupPanel, isGroupActive: boolean): void {
this._group = group;
this.api.group = group;
@ -148,7 +167,7 @@ export class DockviewGroupPanel
);
}
public layout(width: number, height: number) {
public layout(width: number, height: number): void {
// the obtain the correct dimensions of the content panel we must deduct the tab height
this.api._onDidDimensionChange.fire({
width,
@ -158,9 +177,8 @@ export class DockviewGroupPanel
this.view?.layout(width, height);
}
public dispose() {
public dispose(): void {
this.api.dispose();
this.mutableDisposable.dispose();
this.view?.dispose();
}

View File

@ -1,7 +1,6 @@
import { DockviewApi } from '../api/component.api';
import { Direction } from '../gridview/baseComponentGridview';
import { IGridView } from '../gridview/gridview';
import { IDockviewPanel } from '../groupview/groupPanel';
import {
IContentRenderer,
ITabRenderer,
@ -14,6 +13,8 @@ import { FrameworkFactory } from '../types';
import { DockviewDropTargets } from '../groupview/dnd';
import { PanelTransfer } from '../dnd/dataTransfer';
import { IDisposable } from '../lifecycle';
import { Position } from '../dnd/droptarget';
import { IDockviewPanel } from './dockviewPanel';
export interface IGroupControlRenderer extends IDisposable {
readonly element: HTMLElement;
@ -59,7 +60,8 @@ export interface ViewFactoryData {
export interface DockviewDndOverlayEvent {
nativeEvent: DragEvent;
target: DockviewDropTargets;
group: GroupPanel;
position: Position;
group?: GroupPanel;
getData: () => PanelTransfer | undefined;
}
@ -73,6 +75,7 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions {
defaultTabComponent?: string;
showDndOverlay?: (event: DockviewDndOverlayEvent) => boolean;
createGroupControlElement?: (group: GroupPanel) => IGroupControlRenderer;
singleTabMode?: 'fullwidth' | 'default';
}
export interface PanelOptions {
@ -83,19 +86,81 @@ export interface PanelOptions {
title?: string;
}
type RelativePanel = {
direction?: Direction;
referencePanel: string | IDockviewPanel;
};
type RelativeGroup = {
direction?: Direction;
referenceGroup: string | GroupPanel;
};
type AbsolutePosition = {
direction: Omit<Direction, 'within'>;
};
export type AddPanelPositionOptions =
| RelativePanel
| RelativeGroup
| AbsolutePosition;
export function isPanelOptionsWithPanel(
data: AddPanelPositionOptions
): data is RelativePanel {
if ((data as RelativePanel).referencePanel) {
return true;
}
return false;
}
export function isPanelOptionsWithGroup(
data: AddPanelPositionOptions
): data is RelativeGroup {
if ((data as RelativeGroup).referenceGroup) {
return true;
}
return false;
}
export interface AddPanelOptions
extends Omit<PanelOptions, 'component' | 'tabComponent'> {
component: string;
tabComponent?: string;
position?: {
direction?: Direction;
referencePanel?: string;
};
position?: AddPanelPositionOptions;
}
export interface AddGroupOptions {
direction?: 'left' | 'right' | 'above' | 'below';
referencePanel: string;
type AddGroupOptionsWithPanel = {
referencePanel: string | IDockviewPanel;
direction?: Omit<Direction, 'within'>;
};
type AddGroupOptionsWithGroup = {
referenceGroup: string | GroupPanel;
direction?: Omit<Direction, 'within'>;
};
export type AddGroupOptions =
| AddGroupOptionsWithGroup
| AddGroupOptionsWithPanel
| AbsolutePosition;
export function isGroupOptionsWithPanel(
data: AddGroupOptions
): data is AddGroupOptionsWithPanel {
if ((data as AddGroupOptionsWithPanel).referencePanel) {
return true;
}
return false;
}
export function isGroupOptionsWithGroup(
data: AddGroupOptions
): data is AddGroupOptionsWithGroup {
if ((data as AddGroupOptionsWithGroup).referenceGroup) {
return true;
}
return false;
}
export interface MovementOptions2 {

View File

@ -15,19 +15,19 @@ const nextLayoutId = sequentialNumberGenerator();
export type Direction = 'left' | 'right' | 'above' | 'below' | 'within';
export function toTarget(direction: Direction) {
export function toTarget(direction: Direction): Position {
switch (direction) {
case 'left':
return Position.Left;
return 'left';
case 'right':
return Position.Right;
return 'right';
case 'above':
return Position.Top;
return 'top';
case 'below':
return Position.Bottom;
return 'bottom';
case 'within':
default:
return Position.Center;
return 'center';
}
}

View File

@ -6,7 +6,7 @@ import {
PanelInitParameters,
IPanel,
} from '../panel/types';
import { PanelApiImpl } from '../api/panelApi';
import { PanelApi, PanelApiImpl } from '../api/panelApi';
export interface BasePanelViewState {
id: string;
@ -14,7 +14,7 @@ export interface BasePanelViewState {
params?: Record<string, any>;
}
export interface BasePanelViewExported<T extends PanelApiImpl> {
export interface BasePanelViewExported<T extends PanelApi> {
readonly id: string;
readonly api: T;
readonly width: number;

View File

@ -9,13 +9,13 @@ import {
Orientation,
Sizing,
} from '../splitview/core/splitview';
import { Position } from '../dnd/droptarget';
import { tail } from '../array';
import { LeafNode } from './leafNode';
import { BranchNode } from './branchNode';
import { Node } from './types';
import { Emitter, Event } from '../events';
import { IDisposable, MutableDisposable } from '../lifecycle';
import { Position } from '../dnd/droptarget';
function findLeaf(candiateNode: Node, last: boolean): LeafNode {
if (candiateNode instanceof LeafNode) {
@ -132,22 +132,19 @@ export function getRelativeLocation(
const [rest, _index] = tail(location);
let index = _index;
if (direction === Position.Right || direction === Position.Bottom) {
if (direction === 'right' || direction === 'bottom') {
index += 1;
}
return [...rest, index];
} else {
const index =
direction === Position.Right || direction === Position.Bottom
? 1
: 0;
const index = direction === 'right' || direction === 'bottom' ? 1 : 0;
return [...location, index];
}
}
export function getDirectionOrientation(direction: Position): Orientation {
return direction === Position.Top || direction === Position.Bottom
return direction === 'top' || direction === 'bottom'
? Orientation.VERTICAL
: Orientation.HORIZONTAL;
}
@ -276,6 +273,10 @@ export class Gridview implements IDisposable {
readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> =
this._onDidChange.event;
public get length(): number {
return this._root ? this._root.children.length : 0;
}
public serialize() {
const root = serializeBranchNode(this.getView(), this.orientation);
@ -410,6 +411,43 @@ export class Gridview implements IDisposable {
});
}
/**
* If the root is orientated as a VERTICAL node then nest the existing root within a new HORIZIONTAL root node
* If the root is orientated as a HORIZONTAL node then nest the existing root within a new VERITCAL root node
*/
public insertOrthogonalSplitviewAtRoot(): void {
if (!this._root) {
return;
}
const oldRoot = this.root;
oldRoot.element.remove();
this._root = new BranchNode(
orthogonal(oldRoot.orientation),
this.proportionalLayout,
this.styles,
this.root.orthogonalSize,
this.root.size
);
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
oldRoot.dispose();
this._root.addChild(childReference, Sizing.Distribute, 0);
} else {
this._root.addChild(oldRoot, Sizing.Distribute, 0);
}
this.element.appendChild(this._root.element);
this.disposable.value = this._root.onDidChange((e) => {
this._onDidChange.fire(e);
});
}
public next(location: number[]) {
return this.progmaticSelect(location);
}

View File

@ -3,7 +3,6 @@ import {
SerializedGridObject,
getGridLocation,
} from './gridview';
import { Position } from '../dnd/droptarget';
import { tail, sequenceEquals } from '../array';
import { CompositeDisposable } from '../lifecycle';
import { IPanelDeserializer } from '../dockview/deserializer';
@ -25,6 +24,7 @@ import { BaseComponentOptions } from '../panel/types';
import { Orientation, Sizing } from '../splitview/core/splitview';
import { createComponent } from '../panel/componentFactory';
import { Emitter, Event } from '../events';
import { Position } from '../dnd/droptarget';
export interface SerializedGridview {
grid: {
@ -265,7 +265,7 @@ export class GridviewComponent
}
const target = toTarget(options.direction);
if (target === Position.Center) {
if (target === 'center') {
throw new Error(`${target} not supported as an option`);
} else {
const location = getGridLocation(referenceGroup.element);
@ -294,7 +294,7 @@ export class GridviewComponent
}
const target = toTarget(options.position.direction);
if (target === Position.Center) {
if (target === 'center') {
throw new Error(`${target} not supported as an option`);
} else {
const location = getGridLocation(referenceGroup.element);

View File

@ -9,7 +9,10 @@ import {
BasePanelViewExported,
BasePanelViewState,
} from './basePanelView';
import { GridviewPanelApiImpl } from '../api/gridviewPanelApi';
import {
GridviewPanelApi,
GridviewPanelApiImpl,
} from '../api/gridviewPanelApi';
import { LayoutPriority } from '../splitview/core/splitview';
import { Emitter, Event } from '../events';
import { IViewSize } from './gridview';
@ -26,7 +29,7 @@ export interface GridviewInitParameters extends PanelInitParameters {
}
export interface IGridviewPanel
extends BasePanelViewExported<GridviewPanelApiImpl> {
extends BasePanelViewExported<GridviewPanelApi> {
readonly minimumWidth: number;
readonly maximumWidth: number;
readonly minimumHeight: number;
@ -123,13 +126,11 @@ export abstract class GridviewPanel
return this.api.isActive;
}
constructor(
id: string,
component: string,
api = new GridviewPanelApiImpl(id)
) {
constructor(id: string, component: string, api: GridviewPanelApiImpl) {
super(id, component, api);
this.api.initialize(this); // TODO: required to by-pass 'super before this' requirement
this.addDisposables(
this._onDidChange,
this.api.onVisibilityChange((event) => {

View File

@ -2,4 +2,5 @@ export enum DockviewDropTargets {
Tab,
Panel,
TabContainer,
Edge,
}

View File

@ -1,41 +0,0 @@
import { DockviewPanelApi } from '../api/groupPanelApi';
import { IDisposable } from '../lifecycle';
import { HeaderPartInitParameters } from './types';
import {
IPanel,
PanelInitParameters,
PanelUpdateEvent,
Parameters,
} from '../panel/types';
import { GroupPanel } from './groupviewPanel';
import { IGroupPanelView } from '../dockview/defaultGroupPanelView';
export interface IGroupPanelInitParameters
extends PanelInitParameters,
HeaderPartInitParameters {
view: IGroupPanelView;
}
export type GroupPanelUpdateEvent = PanelUpdateEvent<{
params?: Parameters;
title?: string;
}>;
export interface IDockviewPanel extends IDisposable, IPanel {
readonly view?: IGroupPanelView;
readonly group: GroupPanel;
readonly api: DockviewPanelApi;
readonly title: string;
readonly params: Record<string, any> | undefined;
updateParentGroup(group: GroupPanel, isGroupActive: boolean): void;
init(params: IGroupPanelInitParameters): void;
toJSON(): GroupviewPanelState;
update(event: GroupPanelUpdateEvent): void;
}
export interface GroupviewPanelState {
id: string;
view?: any;
title: string;
params?: { [key: string]: any };
}

View File

@ -8,12 +8,12 @@ import { IGridPanelView } from '../gridview/baseComponentGridview';
import { IViewSize } from '../gridview/gridview';
import { CompositeDisposable, IDisposable } from '../lifecycle';
import { PanelInitParameters, PanelUpdateEvent } from '../panel/types';
import { IDockviewPanel } from './groupPanel';
import { ContentContainer, IContentContainer } from './panel/content';
import { ITabsContainer, TabsContainer } from './titlebar/tabsContainer';
import { IWatermarkRenderer } from './types';
import { GroupPanel } from './groupviewPanel';
import { DockviewDropTargets } from './dnd';
import { IDockviewPanel } from '../dockview/dockviewPanel';
import { IGroupControlRenderer } from '../core';
export interface DndService {
@ -38,7 +38,7 @@ export interface IGroupItem {
interface GroupMoveEvent {
groupId: string;
itemId: string;
itemId?: string;
target: Position;
index?: number;
}
@ -110,7 +110,11 @@ export interface IGroupview extends IDisposable, IGridPanelView {
panel?: IDockviewPanel;
suppressRoll?: boolean;
}): void;
canDisplayOverlay(event: DragEvent, target: DockviewDropTargets): boolean;
canDisplayOverlay(
event: DragEvent,
position: Position,
target: DockviewDropTargets
): boolean;
}
export class Groupview extends CompositeDisposable implements IGroupview {
@ -167,6 +171,8 @@ export class Groupview extends CompositeDisposable implements IGroupview {
set locked(value: boolean) {
this._locked = value;
toggleClass(this.container, 'locked-groupview', value);
}
get isActive(): boolean {
@ -226,35 +232,48 @@ export class Groupview extends CompositeDisposable implements IGroupview {
private accessor: DockviewComponent,
public id: string,
private readonly options: GroupOptions,
private readonly parent: GroupPanel
private readonly groupPanel: GroupPanel
) {
super();
this.container.classList.add('groupview');
this.tabsContainer = new TabsContainer(this.accessor, this.parent, {
tabHeight: options.tabHeight,
});
this.tabsContainer = new TabsContainer(this.accessor, this.groupPanel);
this.contentContainer = new ContentContainer();
this.dropTarget = new Droptarget(this.contentContainer.element, {
validOverlays: 'all',
canDisplayOverlay: (event, quadrant) => {
if (this.locked && !quadrant) {
acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'],
canDisplayOverlay: (event, position) => {
if (this.locked && position === 'center') {
return false;
}
const data = getPanelData();
if (data && data.viewId === this.accessor.id) {
if (data.groupId === this.id) {
if (position === 'center') {
// don't allow to drop on self for center position
return false;
}
if (data.panelId === null) {
// don't allow group move to drop anywhere on self
return false;
}
}
const groupHasOnePanelAndIsActiveDragElement =
this._panels.length === 1 && data.groupId === this.id;
return !groupHasOnePanelAndIsActiveDragElement;
}
return this.canDisplayOverlay(event, DockviewDropTargets.Panel);
return this.canDisplayOverlay(
event,
position,
DockviewDropTargets.Panel
);
},
});
@ -274,10 +293,10 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this._onDidRemovePanel,
this._onDidActivePanelChange,
this.tabsContainer.onDrop((event) => {
this.handleDropEvent(event.event, Position.Center, event.index);
this.handleDropEvent(event.event, 'center', event.index);
}),
this.contentContainer.onDidFocus(() => {
this.accessor.doSetGroupActive(this.parent, true);
this.accessor.doSetGroupActive(this.groupPanel, true);
}),
this.contentContainer.onDidBlur(() => {
// noop
@ -306,12 +325,12 @@ export class Groupview extends CompositeDisposable implements IGroupview {
if (this.accessor.options.createGroupControlElement) {
this._control = this.accessor.options.createGroupControlElement(
this.parent
this.groupPanel
);
this.addDisposables(this._control);
this._control.init({
containerApi: new DockviewApi(this.accessor),
api: this.parent.api,
api: this.groupPanel.api,
});
this.tabsContainer.setActionElement(this._control.element);
}
@ -431,11 +450,11 @@ export class Groupview extends CompositeDisposable implements IGroupview {
const skipSetGroupActive = !!options.skipSetGroupActive;
// ensure the group is updated before we fire any events
panel.updateParentGroup(this.parent, true);
panel.updateParentGroup(this.groupPanel, true);
if (this._activePanel === panel) {
if (!skipSetGroupActive) {
this.accessor.doSetGroupActive(this.parent);
this.accessor.doSetGroupActive(this.groupPanel);
}
return;
}
@ -447,7 +466,10 @@ export class Groupview extends CompositeDisposable implements IGroupview {
}
if (!skipSetGroupActive) {
this.accessor.doSetGroupActive(this.parent, !!options.skipFocus);
this.accessor.doSetGroupActive(
this.groupPanel,
!!options.skipFocus
);
}
this.updateContainer();
@ -476,7 +498,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.doClose(panel);
}
} else {
this.accessor.removeGroup(this.parent);
this.accessor.removeGroup(this.groupPanel);
}
}
@ -633,7 +655,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
toggleClass(this.container, 'empty', this.isEmpty);
this.panels.forEach((panel) =>
panel.updateParentGroup(this.parent, this.isActive)
panel.updateParentGroup(this.groupPanel, this.isActive)
);
if (this.isEmpty && !this.watermark) {
@ -648,14 +670,14 @@ export class Groupview extends CompositeDisposable implements IGroupview {
addDisposableListener(this.watermark.element, 'click', () => {
if (!this.isActive) {
this.accessor.doSetGroupActive(this.parent);
this.accessor.doSetGroupActive(this.groupPanel);
}
});
this.tabsContainer.hide();
this.contentContainer.element.appendChild(this.watermark.element);
this.watermark.updateParentGroup(this.parent, true);
this.watermark.updateParentGroup(this.groupPanel, true);
}
if (!this.isEmpty && this.watermark) {
this.watermark.element.remove();
@ -665,13 +687,18 @@ export class Groupview extends CompositeDisposable implements IGroupview {
}
}
canDisplayOverlay(event: DragEvent, target: DockviewDropTargets): boolean {
canDisplayOverlay(
event: DragEvent,
position: Position,
target: DockviewDropTargets
): boolean {
// custom overlay handler
if (this.accessor.options.showDndOverlay) {
return this.accessor.options.showDndOverlay({
nativeEvent: event,
target,
group: this.accessor.getPanel(this.id)!,
position,
getData: getPanelData,
});
}
@ -685,7 +712,19 @@ export class Groupview extends CompositeDisposable implements IGroupview {
): void {
const data = getPanelData();
if (data) {
if (data && data.viewId === this.accessor.id) {
if (data.panelId === null) {
// this is a group move dnd event
const { groupId } = data;
this._onMove.fire({
target: position,
groupId: groupId,
index,
});
return;
}
const fromSameGroup =
this.tabsContainer.indexOf(data.panelId) !== -1;

View File

@ -6,7 +6,7 @@ import {
} from '../api/gridviewPanelApi';
import { Groupview, GroupOptions, IHeader } from './groupview';
import { GridviewPanel, IGridviewPanel } from '../gridview/gridviewPanel';
import { IDockviewPanel } from './groupPanel';
import { IDockviewPanel } from '../dockview/dockviewPanel';
export interface IGroupviewPanel extends IGridviewPanel {
model: Groupview;

View File

@ -5,7 +5,7 @@ import {
} from '../../lifecycle';
import { Emitter, Event } from '../../events';
import { trackFocus } from '../../dom';
import { IDockviewPanel } from '../groupPanel';
import { IDockviewPanel } from '../../dockview/dockviewPanel';
export interface IContentContainer extends IDisposable {
onDidFocus: Event<void>;

View File

@ -8,28 +8,16 @@ import {
import { toggleClass } from '../dom';
import { IDockviewComponent } from '../dockview/dockviewComponent';
import { ITabRenderer } from './types';
import { IDockviewPanel } from './groupPanel';
import { GroupPanel } from './groupviewPanel';
import { DroptargetEvent, Droptarget } from '../dnd/droptarget';
import { DockviewDropTargets } from './dnd';
import { DragHandler } from '../dnd/abstractDragHandler';
export enum MouseEventKind {
CLICK = 'CLICK',
}
export interface LayoutMouseEvent {
readonly kind: MouseEventKind;
readonly event: MouseEvent;
readonly panel?: IDockviewPanel;
readonly tab?: boolean;
}
export interface ITab {
readonly panelId: string;
readonly element: HTMLElement;
setContent: (element: ITabRenderer) => void;
onChanged: Event<LayoutMouseEvent>;
onChanged: Event<MouseEvent>;
onDrop: Event<DroptargetEvent>;
setActive(isActive: boolean): void;
}
@ -39,13 +27,13 @@ export class Tab extends CompositeDisposable implements ITab {
private readonly droptarget: Droptarget;
private content?: ITabRenderer;
private readonly _onChanged = new Emitter<LayoutMouseEvent>();
readonly onChanged: Event<LayoutMouseEvent> = this._onChanged.event;
private readonly _onChanged = new Emitter<MouseEvent>();
readonly onChanged: Event<MouseEvent> = this._onChanged.event;
private readonly _onDropped = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDropped.event;
public get element() {
public get element(): HTMLElement {
return this._element;
}
@ -104,20 +92,34 @@ export class Tab extends CompositeDisposable implements ITab {
*/
event.stopPropagation();
this._onChanged.fire({ kind: MouseEventKind.CLICK, event });
this._onChanged.fire(event);
})
);
this.droptarget = new Droptarget(this._element, {
validOverlays: 'none',
canDisplayOverlay: (event) => {
acceptedTargetZones: ['center'],
canDisplayOverlay: (event, position) => {
if (this.group.locked) {
return false;
}
const data = getPanelData();
if (data && this.accessor.id === data.viewId) {
if (
data.panelId === null &&
data.groupId === this.group.id
) {
// don't allow group move to drop on self
return false;
}
return this.panelId !== data.panelId;
}
return this.group.model.canDisplayOverlay(
event,
position,
DockviewDropTargets.Tab
);
},
@ -130,12 +132,12 @@ export class Tab extends CompositeDisposable implements ITab {
);
}
public setActive(isActive: boolean) {
public setActive(isActive: boolean): void {
toggleClass(this.element, 'active-tab', isActive);
toggleClass(this.element, 'inactive-tab', !isActive);
}
public setContent(part: ITabRenderer) {
public setContent(part: ITabRenderer): void {
if (this.content) {
this._element.removeChild(this.content.element);
}
@ -143,7 +145,7 @@ export class Tab extends CompositeDisposable implements ITab {
this._element.appendChild(this.content.element);
}
public dispose() {
public dispose(): void {
super.dispose();
this.droptarget.dispose();
}

View File

@ -10,9 +10,24 @@
display: none;
}
&.dv-single-tab.dv-full-width-single-tab {
.tabs-container {
flex-grow: 1;
.tab {
flex-grow: 1;
}
}
.void-container {
flex-grow: 0;
}
}
.void-container {
display: flex;
flex-grow: 1;
cursor: grab;
}
.tabs-container {

View File

@ -4,14 +4,12 @@ import {
IValueDisposable,
} from '../../lifecycle';
import { addDisposableListener, Emitter, Event } from '../../events';
import { ITab, MouseEventKind, Tab } from '../tab';
import { last } from '../../array';
import { IDockviewPanel } from '../groupPanel';
import { ITab, Tab } from '../tab';
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { getPanelData } from '../../dnd/dataTransfer';
import { GroupPanel } from '../groupviewPanel';
import { Droptarget } from '../../dnd/droptarget';
import { DockviewDropTargets } from '../dnd';
import { VoidContainer } from './voidContainer';
import { toggleClass } from '../../dom';
import { IDockviewPanel } from '../../dockview/dockviewPanel';
export interface TabDropIndexEvent {
event: DragEvent;
@ -44,10 +42,8 @@ export class TabsContainer
{
private readonly _element: HTMLElement;
private readonly tabContainer: HTMLElement;
private readonly voidContainer: HTMLElement;
private readonly actionContainer: HTMLElement;
private readonly voidDropTarget: Droptarget;
private readonly voidContainer: VoidContainer;
private tabs: IValueDisposable<ITab>[] = [];
private selectedIndex = -1;
@ -138,9 +134,8 @@ export class TabsContainer
}
constructor(
private accessor: DockviewComponent,
private group: GroupPanel,
options: { tabHeight?: number }
private readonly accessor: DockviewComponent,
private readonly group: GroupPanel
) {
super();
@ -149,7 +144,34 @@ export class TabsContainer
this._element = document.createElement('div');
this._element.className = 'tabs-and-actions-container';
this.height = options.tabHeight;
this.height = accessor.options.tabHeight;
toggleClass(
this._element,
'dv-full-width-single-tab',
this.accessor.options.singleTabMode === 'fullwidth'
);
this.addDisposables(
this.accessor.onDidAddPanel((e) => {
if (e.api.group === this.group) {
toggleClass(
this._element,
'dv-single-tab',
this.size === 1
);
}
}),
this.accessor.onDidRemovePanel((e) => {
if (e.api.group === this.group) {
toggleClass(
this._element,
'dv-single-tab',
this.size === 1
);
}
})
);
this.actionContainer = document.createElement('div');
this.actionContainer.className = 'action-container';
@ -157,38 +179,20 @@ export class TabsContainer
this.tabContainer = document.createElement('div');
this.tabContainer.className = 'tabs-container';
this.voidContainer = document.createElement('div');
this.voidContainer.className = 'void-container';
this.voidContainer = new VoidContainer(this.accessor, this.group);
this._element.appendChild(this.tabContainer);
this._element.appendChild(this.voidContainer);
this._element.appendChild(this.voidContainer.element);
this._element.appendChild(this.actionContainer);
this.voidDropTarget = new Droptarget(this.voidContainer, {
validOverlays: 'none',
canDisplayOverlay: (event) => {
const data = getPanelData();
if (data && this.accessor.id === data.viewId) {
// don't show the overlay if the tab being dragged is the last panel of this group
return last(this.tabs)?.value.panelId !== data.panelId;
}
return group.model.canDisplayOverlay(
event,
DockviewDropTargets.Panel
);
},
});
this.addDisposables(
this.voidDropTarget.onDrop((event) => {
this.voidContainer,
this.voidContainer.onDrop((event) => {
this._onDrop.fire({
event: event.nativeEvent,
index: this.tabs.length,
});
}),
this.voidDropTarget,
addDisposableListener(this.tabContainer, 'mousedown', (event) => {
if (event.defaultPrevented) {
return;
@ -265,17 +269,15 @@ export class TabsContainer
panel.id === this.group.model.activePanel?.id &&
this.group.model.isContentFocused;
const isLeftClick = event.event.button === 0;
const isLeftClick = event.button === 0;
if (!isLeftClick || event.event.defaultPrevented) {
if (!isLeftClick || event.defaultPrevented) {
return;
}
if (event.kind === MouseEventKind.CLICK) {
this.group.model.openPanel(panel, {
skipFocus: alreadyFocused,
});
}
this.group.model.openPanel(panel, {
skipFocus: alreadyFocused,
});
}),
tabToAdd.onDrop((event) => {
this._onDrop.fire({

View File

@ -0,0 +1,77 @@
import { last } from '../../array';
import { getPanelData } from '../../dnd/dataTransfer';
import { Droptarget, DroptargetEvent } from '../../dnd/droptarget';
import { GroupDragHandler } from '../../dnd/groupDragHandler';
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { addDisposableListener, Emitter, Event } from '../../events';
import { CompositeDisposable } from '../../lifecycle';
import { DockviewDropTargets } from '../dnd';
import { GroupPanel } from '../groupviewPanel';
export class VoidContainer extends CompositeDisposable {
private readonly _element: HTMLElement;
private readonly voidDropTarget: Droptarget;
private readonly _onDrop = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
get element() {
return this._element;
}
constructor(
private readonly accessor: DockviewComponent,
private readonly group: GroupPanel
) {
super();
this._element = document.createElement('div');
this._element.className = 'void-container';
this._element.tabIndex = 0;
this._element.draggable = true;
this.addDisposables(
this._onDrop,
addDisposableListener(this._element, 'click', () => {
this.accessor.doSetGroupActive(this.group);
})
);
const handler = new GroupDragHandler(this._element, accessor.id, group);
this.voidDropTarget = new Droptarget(this._element, {
acceptedTargetZones: ['center'],
canDisplayOverlay: (event, position) => {
const data = getPanelData();
if (data && this.accessor.id === data.viewId) {
if (
data.panelId === null &&
data.groupId === this.group.id
) {
// don't allow group move to drop on self
return false;
}
// don't show the overlay if the tab being dragged is the last panel of this group
return last(this.group.panels)?.id !== data.panelId;
}
return group.model.canDisplayOverlay(
event,
position,
DockviewDropTargets.Panel
);
},
});
this.addDisposables(
handler,
this.voidDropTarget.onDrop((event) => {
this._onDrop.fire(event);
}),
this.voidDropTarget
);
}
}

View File

@ -1,9 +1,15 @@
import { IDockviewComponent } from '../dockview/dockviewComponent';
import { DockviewPanelApi } from '../api/groupPanelApi';
import { PanelInitParameters, IPanel } from '../panel/types';
import { DockviewPanelApi } from '../api/dockviewPanelApi';
import {
PanelInitParameters,
IPanel,
PanelUpdateEvent,
Parameters,
} from '../panel/types';
import { DockviewApi } from '../api/component.api';
import { GroupPanel } from './groupviewPanel';
import { Event } from '../events';
import { IGroupPanelView } from '../dockview/defaultGroupPanelView';
export interface IRenderable {
id: string;
@ -13,7 +19,7 @@ export interface IRenderable {
}
export interface HeaderPartInitParameters {
title: string;
title?: string;
}
export interface GroupPanelPartInitParameters
@ -67,3 +73,21 @@ export interface PanelContentPartConstructor {
export interface WatermarkConstructor {
new (): IWatermarkRenderer;
}
export interface IGroupPanelInitParameters
extends PanelInitParameters,
HeaderPartInitParameters {
view: IGroupPanelView;
}
export type GroupPanelUpdateEvent = PanelUpdateEvent<{
params?: Parameters;
title?: string;
}>;
export interface GroupviewPanelState {
id: string;
view?: any;
title?: string;
params?: { [key: string]: any };
}

View File

@ -4,7 +4,7 @@ import {
LocalSelectionTransfer,
PaneTransfer,
} from '../dnd/dataTransfer';
import { Droptarget, DroptargetEvent, Position } from '../dnd/droptarget';
import { Droptarget, DroptargetEvent } from '../dnd/droptarget';
import { Emitter } from '../events';
import { IDisposable } from '../lifecycle';
import { Orientation } from '../splitview/core/splitview';
@ -70,7 +70,10 @@ export abstract class DraggablePaneviewPanel extends PaneviewPanel {
})(this.header);
this.target = new Droptarget(this.element, {
validOverlays: 'vertical',
acceptedTargetZones: ['top', 'bottom'],
overlayModel: {
activationSize: { type: 'percentage', value: 50 },
},
canDisplayOverlay: (event) => {
const data = getPaneData();
@ -139,16 +142,10 @@ export abstract class DraggablePaneviewPanel extends PaneviewPanel {
const fromIndex = allPanels.indexOf(existingPanel);
let toIndex = containerApi.panels.indexOf(this);
if (
event.position === Position.Left ||
event.position === Position.Top
) {
if (event.position === 'left' || event.position === 'top') {
toIndex = Math.max(0, toIndex - 1);
}
if (
event.position === Position.Right ||
event.position === Position.Bottom
) {
if (event.position === 'right' || event.position === 'bottom') {
if (fromIndex > toIndex) {
toIndex++;
}

View File

@ -164,6 +164,8 @@ export abstract class PaneviewPanel
) {
super(id, component, new PaneviewPanelApiImpl(id));
this.api.pane = this; // TODO cannot use 'this' before 'super'
this.api.initialize(this);
this._isExpanded = isExpanded;
this._headerVisible = isHeaderVisible;

View File

@ -1,6 +1,6 @@
import { DockviewComponent } from '../dockview/dockviewComponent';
import { GroupviewPanelState, IDockviewPanel } from '../groupview/groupPanel';
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { GroupviewPanelState } from '../groupview/types';
import { DockviewPanel, IDockviewPanel } from '../dockview/dockviewPanel';
import { IPanelDeserializer } from '../dockview/deserializer';
import { createComponent } from '../panel/componentFactory';
import { DockviewApi } from '../api/component.api';
@ -56,7 +56,7 @@ export class ReactPanelDeserialzier implements IPanelDeserializer {
tab,
});
const panel = new DockviewGroupPanel(
const panel = new DockviewPanel(
panelId,
this.layout,
new DockviewApi(this.layout),

View File

@ -11,7 +11,7 @@ import {
GroupPanelFrameworkComponentFactory,
IGroupControlRenderer,
} from '../../dockview/options';
import { DockviewPanelApi } from '../../api/groupPanelApi';
import { DockviewPanelApi } from '../../api/dockviewPanelApi';
import { ReactPortalStore, usePortalsLifecycle } from '../react';
import { DockviewApi } from '../../api/component.api';
import { IWatermarkPanelProps, ReactWatermarkPart } from './reactWatermarkPart';
@ -57,10 +57,10 @@ export interface DockviewReadyEvent {
}
export interface IDockviewReactProps {
onReady: (event: DockviewReadyEvent) => void;
components: PanelCollection<IDockviewPanelProps>;
tabComponents?: PanelCollection<IDockviewPanelHeaderProps>;
watermarkComponent?: React.FunctionComponent<IWatermarkPanelProps>;
onReady: (event: DockviewReadyEvent) => void;
tabHeight?: number;
onDidDrop?: (event: DockviewDropEvent) => void;
showDndOverlay?: (event: DockviewDndOverlayEvent) => boolean;
@ -69,6 +69,7 @@ export interface IDockviewReactProps {
disableAutoResizing?: boolean;
defaultTabComponent?: React.FunctionComponent<IDockviewPanelHeaderProps>;
groupControlComponent?: React.FunctionComponent<IDockviewGroupControlProps>;
singleTabMode?: 'fullwidth' | 'default';
}
export const DockviewReact = React.forwardRef(
@ -161,6 +162,7 @@ export const DockviewReact = React.forwardRef(
props.groupControlComponent,
{ addPortal }
),
singleTabMode: props.singleTabMode,
});
domRef.current?.appendChild(dockview.element);

View File

@ -3,12 +3,8 @@ import { ReactPart, ReactPortalStore } from '../react';
import { PanelUpdateEvent } from '../../panel/types';
import { GroupPanel, GroupviewPanelApi } from '../../groupview/groupviewPanel';
import { DockviewApi } from '../../api/component.api';
import {
CompositeDisposable,
IDisposable,
MutableDisposable,
} from '../../lifecycle';
import { IDockviewPanel } from '../../groupview/groupPanel';
import { CompositeDisposable, MutableDisposable } from '../../lifecycle';
import { IDockviewPanel } from '../../dockview/dockviewPanel';
export interface IDockviewGroupControlProps {
api: GroupviewPanelApi;

View File

@ -1,4 +1,5 @@
import { GridviewApi } from '../../api/component.api';
import { GridviewPanelApiImpl } from '../../api/gridviewPanelApi';
import {
GridviewPanel,
GridviewInitParameters,
@ -14,7 +15,7 @@ export class ReactGridPanelView extends GridviewPanel {
private readonly reactComponent: React.FunctionComponent<IGridviewPanelProps>,
private readonly reactPortalStore: ReactPortalStore
) {
super(id, component);
super(id, component, new GridviewPanelApiImpl(id));
}
getComponent(): IFrameworkPart {

View File

@ -85,6 +85,8 @@ export abstract class SplitviewPanel
constructor(id: string, componentName: string) {
super(id, componentName, new SplitviewPanelApiImpl(id));
this.api.initialize(this);
this.addDisposables(
this._onDidChange,
this.api.onVisibilityChange((event) => {

View File

@ -7,3 +7,7 @@ export interface FrameworkFactory<T> {
}
export type FunctionOrValue<T> = (() => T) | T;
export function isBooleanValue(value: any): value is boolean {
return typeof value === 'boolean';
}

View File

@ -0,0 +1,37 @@
---
slug: dockview-1.6.0-release
title: Dockview 1.6.0
tags: [release]
---
import Link from '@docusaurus/Link';
# Release Notes
Please reference to docs @ [dockview.dev](https://dockview.dev).
If you feel anything is missing or unclear please let me know.
## 🚀 Features
- Allow drag events to edge of dockview containers [#177](https://github.com/mathuo/dockview/pull/177)
- group dnd [#171](https://github.com/mathuo/dockview/pull/171)
- full width tabs [#171](https://github.com/mathuo/dockview/pull/177)
- addPanel improvements
- update parameters via panel.api.updateParameters
- allow dnd on empty groups [#168](https://github.com/mathuo/dockview/pull/168)
- Change watermark logic [#194](https://github.com/mathuo/dockview/pull/194)
## 🛠 Miscs
- Fix dockview panel.api.setSize to work as expected [#184](https://github.com/mathuo/dockview/pull/184)
- Fix dockview setTitle [#190](https://github.com/mathuo/dockview/pull/190)
- Fix group dnd logic to filter for same dockview instance [#185](https://github.com/mathuo/dockview/pull/193)
- Update dependencies including the dev dependencies for dockview and all dependencies for the docs website.
[#180](https://github.com/mathuo/dockview/pull/180)
- A variety of internal changes including file name changes
- Improve internal dnd control logic to handle a wider variety of cases
- Various doc enhancements @ [dockview.dev](https://dockview.dev)
## 🔥 Breaking changes
- addEmptyGroup renamed to addGroup

View File

@ -13,7 +13,15 @@ import { ContextMenuDockview } from '@site/src/components/dockview/contextMenu';
import { NestedDockview } from '@site/src/components/dockview/nested';
import { CustomHeadersDockview } from '@site/src/components/dockview/customHeaders';
import { ResizeDockview } from '@site/src/components/dockview/resize';
import { DockviewGroupControl } from '@site/src/components/dockview/groupControl';
import { DockviewWatermark } from '@site/src/components/dockview/watermark';
import { DockviewPersistance } from '@site/src/components/dockview/persistance';
import {
DockviewNative,
DockviewNative2,
} from '@site/src/components/dockview/native';
import Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
# Dockview
@ -43,17 +51,21 @@ You can create a Dockview through the use of the `ReactDockview` component.
import { ReactDockview } from 'dockview';
```
| Property | Type | Optional | Default | Description |
| ------------------- | ------------------------------------ | -------- | ------- | ------------------------------------------------------------ |
| onReady | (event: SplitviewReadyEvent) => void | No | | |
| components | object | No | | |
| tabComponents | object | Yes | | |
| watermarkComponent | object | Yes | | |
| hideBorders | boolean | Yes | false | |
| className | string | Yes | '' | |
| disableAutoResizing | boolean | Yes | false | See <Link to="../basics/#auto-resizing">Auto Resizing</Link> |
| onDidDrop | Event | Yes | false | |
| showDndOverlay | Event | Yes | false | |
| Property | Type | Optional | Default | Description |
| --------------------- | ------------------------------------ | -------- | --------- | ------------------------------------------------------------ |
| onReady | (event: SplitviewReadyEvent) => void | No | | |
| components | object | No | | |
| tabComponents | object | Yes | | |
| watermarkComponent | object | Yes | | |
| hideBorders | boolean | Yes | false | |
| className | string | Yes | '' | |
| disableAutoResizing | boolean | Yes | false | See <Link to="../basics/#auto-resizing">Auto Resizing</Link> |
| onDidDrop | Event | Yes | false | |
| showDndOverlay | Event | Yes | false | |
| defaultTabComponent | object | Yes | | |
| groupControlComponent | object | Yes | | |
| tabHeight | number | Yes | | |
| singleTabMode | 'fullwidth' \| 'default' | Yes | 'default' | |
## Dockview API
@ -101,13 +113,13 @@ const onReady = (event: DockviewReadyEvent) => {
| | | |
| addPanel | `addPanel(options: AddPanelOptions): IDockviewPanel` | |
| getPanel | `(id: string) \| IDockviewPanel \| undefined` | |
| addEmptyGroup | `(options? AddGroupOptions): void` | |
| addGroup | `(options? AddGroupOptions): void` | |
| closeAllGroups | `(): void` | |
| removeGroup | `(group: GroupPanel): void` | |
| getGroup | `(id: string): GroupPanel \| undefined` | |
| | | |
| getTabHeight | `(): number \| undefined` | |
| setTabHeight | `(hegiht: number \| undefined): void` | |
| setTabHeight | `(height: number \| undefined): void` | |
| updateOptions | `(options:SplitviewComponentUpdateOptions): void` | |
| focus | `(): void` | |
| layout | `(width: number, height:number): void` | <Link to="../basics/#auto-resizing">Auto Resizing</Link> |
@ -125,192 +137,155 @@ const MyComponent = (props: IDockviewPanelProps<{ title: string }>) => {
};
```
| Property | Type | Description |
| ---------------------- | ----------------------------------------------------------- | --------------- |
| id | `string` | Panel id |
| isFocused | `boolean` | Is panel focsed |
| isActive | `boolean` | Is panel active |
| width | `number` | Panel width |
| height | `number` | Panel height |
| onDidDimensionsChange | `Event<PanelDimensionChangeEvent>` | |
| onDidFocusChange | `Event<FocusEvent>` | |
| onDidVisibilityChange | `Event<VisibilityEvent>` | |
| onDidActiveChange | `Event<ActiveEvent>` | |
| setActive | `(): void` | |
| | | |
| onDidConstraintsChange | `onDidConstraintsChange: Event<PanelConstraintChangeEvent>` | |
| setConstraints | `(value: PanelConstraintChangeEvent2): void;` | |
| setSize | `(event: SizeEvent): void` | |
| | | |
| group | `GroupPanel | undefined` |
| isGroupActive | `boolean` | |
| title | `string` | |
| suppressClosable | `boolean` | |
| close | `(): void` | |
| setTitle | `(title: string): void` | |
| Property | Type | Description |
| ---------------------- | ----------------------------------------------------------- | ---------------- |
| id | `string` | Panel id |
| isFocused | `boolean` | Is panel focused |
| isActive | `boolean` | Is panel active |
| width | `number` | Panel width |
| height | `number` | Panel height |
| onDidDimensionsChange | `Event<PanelDimensionChangeEvent>` | |
| onDidFocusChange | `Event<FocusEvent>` | |
| onDidVisibilityChange | `Event<VisibilityEvent>` | |
| onDidActiveChange | `Event<ActiveEvent>` | |
| setActive | `(): void` | |
| | | |
| onDidConstraintsChange | `onDidConstraintsChange: Event<PanelConstraintChangeEvent>` | |
| setConstraints | `(value: PanelConstraintChangeEvent2): void;` | |
| setSize | `(event: SizeEvent): void` | |
| | | |
| group | `GroupPanel | undefined` |
| isGroupActive | `boolean` | |
| title | `string` | |
| suppressClosable | `boolean` | |
| close | `(): void` | |
| setTitle | `(title: string): void` | |
## Advanced Features
## Layout Persistance
### Resizing via API
Layouts are loaded and saved via to `fromJSON` and `toJSON` methods on the Dockview api.
The api also exposes an event `onDidLayoutChange` you can listen on to determine when the layout has changed.
Below are some snippets showing how you might load from and save to localStorage.
Each Dockview is comprised of a number of groups, each of which have a number of panels.
Logically most people would want to resize a panel but practically this really translates to resizing the group to which the panel belongs.
```tsx title="Saving the layout state to localStorage"
React.useEffect(() => {
if (!api) {
return;
}
From the api you can access the panels group object (`props.group`) which exposes it's own api object (`props.groups.api`).
This api is largly similar to the <Link to="./gridview/#gridview-panel-api">Gridview API</Link>.
const disposable = api.onDidLayoutChange(() => {
const layout = api.toJSON();
To resize an individual panel you could create a snippet similar to below.
```tsx
const onResizePanel = () => {
props.api.group.api.setSize({
height: 100,
localStorage.setItem(
'dockview_persistance_layout',
JSON.stringify(layout)
);
});
};
```
```tsx
const onResizePanel = () => {
props.api.group.api.setSize({
width: 100,
});
};
```
Here is a working example of resizing panels via these API methods.
<ResizeDockview />
### Locked group
Locking a group will disable all drop events for this group ensuring a user can not add additional panels to the group.
You can still add groups to a locked panel programatically using the API.
```tsx
panel.group.locked = true;
```
### Group header
You may wish to hide the header section of a group. This can achieved through setting the `hidden` variable on `panel.group.header`.
```tsx
panel.group.header.hidden = true;
```
### Custom Tab Headers
You can provide custom renderers for your tab headers.
A default implementation of `DockviewDefaultTab` is provided should you only wish to attach minor
changes and events that do not alter the default behaviour, for example to add a custom context menu event
handler.
You are also free to define a custom renderer entirely from scratch and not make use of the `DockviewDefaultTab` component.
```tsx title="Attaching a custom context menu event handlers to a custom header"
import { IDockviewPanelHeaderProps, DockviewDefaultTab } from 'dockview';
const MyCustomheader = (props: IDockviewPanelHeaderProps) => {
const onContextMenu = (event: React.MouseEvent) => {
event.preventDefault();
alert('context menu');
return () => {
disposable.dispose();
};
return <DockviewDefaultTab onContextMenu={onContextMenu} {...props} />;
}, [api]);
```
```tsx title="Loading a layout from localStorage"
const onReady = (event: DockviewReadyEvent) => {
const layoutString = localStorage.getItem('dockview_persistance_layout');
let success = false;
if (layoutString) {
try {
const layout = JSON.parse(layoutString);
event.api.fromJSON(layout);
success = true;
} catch (err) {
//
}
}
if (!success) {
// do something if there is no layout or there was a loading error
}
};
```
To use a custom renderer you can must register a collection of tab components
Here is an example using the above code loading from and saving to localStorage.
If you refresh the page you should notice your layout is loaded as you left it.
<DockviewPersistance />
## Resizing
Each Dockview contains of a number of groups and each group has a number of panels.
Logically a user may want to resize a panel, but this translates to resizing the group which contains that panel.
You can set the size of a panel using `props.api.setSize(...)`.
You can also set the size of the group associated with the panel using `props.api.group.api.setSize(...)` although this isn't recommended
due to the clunky syntax.
```tsx
const tabComponents = {
myCustomHeader: MyCustomHeader,
};
// it's mandatory to provide either a height or a width, providing both is optional
props.api.setSize({
height: 100,
width: 200,
});
return <DockviewReact tabComponents={tabComponents} ... />;
```
```tsx
api.addPanel({
id: 'panel_1',
component: 'default',
tabComponent: 'myCustomHeader', // <--
title: 'Panel 1',
// you could also resize the panels group, although not recommended it achieved the same result
props.api.group.api.setSize({
height: 100,
width: 200,
});
```
You can also override the default tab renderer which will be used when no `tabComponent` is provided to the `addPanel` function.
You can see an example invoking both approaches below.
```tsx
<DockviewReact defaultTabComponent={MyCustomHeader} ... />;
```
<ResizeDockview />
As a simple example the below attachs a custom event handler for the context menu on all tabs as a default tab renderer
## Watermark
<CustomHeadersDockview />
When the dockview is empty you may want to display some fallback content, this is refered to as the `watermark`.
By default there the watermark has no content but you can provide as a prop to `DockviewReact` a `watermarkComponent`
which will be rendered when there are no panels or groups.
### Rendering
<DockviewWatermark />
Although `DockviewReact` will only add those tabs that are visible to the DOM all associated React Components for each tab including those that
are not initially visible will be created.
This will mean that any hooks in those components will run and if you running expensive operations in the tabs you may end up doing a lot of initial
work for what are hidden tabs.
## Drag And Drop
This is the default behaviour to ensure the greatest flexibility for the user but you can create a Higher-Order component wrapping your components that
will ensure the component is only created if the tab is visible as below:
### Built-in behaviours
```tsx
import { PanelApi } from 'dockview';
import * as React from 'react';
Dockview supports a wide variety of built-in Drag and Drop possibilities.
Below are some examples of the operations you can perform.
function RenderWhenVisible<
T extends { api: Pick<PanelApi, 'isVisible' | 'onDidVisibilityChange'> }
>(component: React.FunctionComponent<T>) {
const HigherOrderComponent = (props: T) => {
const [visible, setVisible] = React.useState<boolean>(
props.api.isVisible
);
<img style={{ width: '60%' }} src={useBaseUrl('/img/add_to_tab.svg')} />
React.useEffect(() => {
const disposable = props.api.onDidVisibilityChange((event) =>
setVisible(event.isVisible)
);
> Drag a tab onto another tab to place it inbetween existing tabs.
return () => {
disposable.dispose();
};
}, [props.api]);
<img style={{ width: '60%' }} src={useBaseUrl('/img/add_to_empty_space.svg')} />
if (!visible) {
return null;
}
> Drag a tab to the right of the last tab to place it after the existing tabs.
return React.createElement(component, props);
};
return HigherOrderComponent;
}
```
<img style={{ width: '60%' }} src={useBaseUrl('/img/add_to_group.svg')} />
```tsx
const component = RenderWhenVisible(MyComponent);
```
> Drag a group onto an existing group to merge the two groups.
Through toggling the checkbox you can see that when you only render those panels which are visible the underling React component is destroyed when it becomes hidden and re-created when it becomes visible.
<Checkbox />
<div
style={{
height: '300px',
backgroundColor: 'rgb(30,30,30)',
color: 'white',
margin: '20px 0px',
}}
>
<RenderingDockview renderVisibleOnly={false} />
<div style={{ display: 'flex', justifyContent: 'space-around' }}>
<img style={{ width: '40%' }} src={useBaseUrl('/img/drop_positions.svg')} />
<img
style={{ width: '40%' }}
src={useBaseUrl('/img/magnet_drop_positions.svg')}
/>
</div>
### Drag And Drop
> Drag into the left/right/top/bottom target zone of a panel to create a new group in the selected direction.
The component exposes some method to help determine whether external drag events should be interacted with or not.
> Drag into the center of a panel to add to that group.
> Drag to the edge of the dockview component to create a new group on the selected edge.
### Extended behaviours
For interaction with the Drag events directly the component exposes some method to help determine whether external drag events should be interacted with or not.
```tsx
/**
@ -352,16 +327,280 @@ return (
<DndDockview />
### Events
## Panels
<EventsDockview />
### Add Panel
### Nested Dockviews
Using the dockview API you can access the `addPanel` method which returns an instance of the created panel.
The minimum method signature is:
You can safely create multiple dockview instances within one page and nest dockviews within other dockviews.
If you wish to interact with the drop event from one dockview instance in another dockview instance you can implement the `showDndOverlay` and `onDidDrop` props on `DockviewReact`.
```ts
const panel = api.addPanel({
id: 'my_unique_panel_id',
component: 'my_component',
});
```
<NestedDockview />
where `id` is the unique id of the panel and `component` is the implenentation which
will be used to render the panel. You will have registered this using the `components` prop of the `DockviewReactComponent` component.
You can optionally provide a `tabComponent` parameters to the `addPanel` method which will render the tab using a custom renderer.
You will have registered this using the `tabComponents` prop of the `DockviewReactComponent` component.
```ts
const panel = api.addPanel({
id: 'my_unique_panel_id',
component: 'my_component',
tabComponent: 'my_tab_component',
});
```
You can pass properties to the panel using the `params` key.
You can update these properties through the panels `api` object and its `updateParameters` method.
```ts
const panel = api.addPanel({
id: 'my_unique_panel_id',
component: 'my_component',
params: {
myCustomKey: 'my_custom_value',
},
});
panel.api.updateParameters({
myCustomKey: 'my_custom_value',
myOtherCustomKey: 'my_other_custom_key',
});
```
> Note `updateParameters` does not accept partial parameter updates, you should call it with the entire set of parameters
> you want the panel to receive.
Finally `addPanel` accepts a `position` object which tells dockview where to place the panel.
- This object optionally accepts either a `referencePanel` or `referenceGroup` which can be the associated id as a string
or the panel/group object reference.
- This object accepts a `direction` property which dictates where,
relative to the provided reference the new panel will be placed.
> If neither a `referencePanel` or `referenceGroup` then the provided `direction` will be treated as absolute.
> If no `direction` is provided the library will place the new panel in a pre-determined position.
```ts
const panel = api.addPanel({
id: 'panel_1',
component: 'default',
});
const panel2 = api.addPanel({
id: 'panel_2',
component: 'default',
position: {
referencePanel: panel1,
direction: 'right',
},
});
```
### Panel Rendering
By default `DockviewReact` only adds to the DOM those panels that are visible,
if a panel is not the active tab and not shown the contents of the hidden panel will be removed from the DOM.
However the React Components associated with each panel are only created once and will always exist for as long as the panel exists, hidden or not.
> For example this means that any hooks in those components will run whether the panel is visible or not which may lead to excessive background work depending
> on the panels implementation.
This is the default behaviour to ensure the greatest flexibility for the user but through the panels `props.api` you can listen to the visiblity state of the panel
and write additional logic to optimize your application.
For example if you wanted to unmount the React Components when the panel is not visible you could create a Higher-Order-Component that listens to the panels
visiblity state and only renders the panel when visible.
```tsx title="Only rendering the React Component when the panel is visible, otherwise rendering a null React Component"
import { IDockviewPanelProps } from 'dockview';
import * as React from 'react';
function RenderWhenVisible(
component: React.FunctionComponent<IDockviewPanelProps>
) {
const HigherOrderComponent = (props: IDockviewPanelProps) => {
const [visible, setVisible] = React.useState<boolean>(
props.api.isVisible
);
React.useEffect(() => {
const disposable = props.api.onDidVisibilityChange((event) =>
setVisible(event.isVisible)
);
return () => {
disposable.dispose();
};
}, [props.api]);
if (!visible) {
return null;
}
return React.createElement(component, props);
};
return HigherOrderComponent;
}
```
```tsx
const components = { default: RenderWhenVisible(MyComponent) };
```
Toggling the checkbox you can see that when you only render those panels which are visible the underling React component is destroyed when it becomes hidden and re-created when it becomes visible.
<Checkbox />
<div
style={{
height: '300px',
backgroundColor: 'rgb(30,30,30)',
color: 'white',
margin: '20px 0px',
}}
>
<RenderingDockview renderVisibleOnly={false} />
</div>
## Headers
### Custom Tab Headers
You can provide custom renderers for your tab headers for maximum customization.
A default implementation of `DockviewDefaultTab` is provided should you only wish to attach minor
changes and events that do not alter the default behaviour, for example to add a custom context menu event
handler.
```tsx title="Attaching a custom context menu event handlers to a custom header"
import { IDockviewPanelHeaderProps, DockviewDefaultTab } from 'dockview';
const MyCustomheader = (props: IDockviewPanelHeaderProps) => {
const onContextMenu = (event: React.MouseEvent) => {
event.preventDefault();
alert('context menu');
};
return <DockviewDefaultTab onContextMenu={onContextMenu} {...props} />;
};
```
You are also free to define a custom renderer entirely from scratch and not make use of the `DockviewDefaultTab` component.
To use a custom renderer you can must register a collection of tab components.
```tsx
const tabComponents = {
myCustomHeader: MyCustomHeader,
};
return <DockviewReact tabComponents={tabComponents} ... />;
```
```tsx
api.addPanel({
id: 'panel_1',
component: 'default',
tabComponent: 'myCustomHeader', // <-- your registered renderers
title: 'Panel 1',
});
```
You can also override the default tab renderer which will be used when no `tabComponent` is provided to the `addPanel` function.
```tsx
<DockviewReact defaultTabComponent={MyCustomHeader} ... />;
```
As a simple example the below attachs a custom event handler for the context menu on all tabs as a default tab renderer
The below example uses a custom tab renderer to reigster a popover when the user right clicked on a tab.
This still makes use of the `DockviewDefaultTab` since it's only a minor change.
<CustomHeadersDockview />
### Default Tab Title
If you are using the default tab renderer you can set the title of a tab when creating it
```tsx
api.addPanel({
id: 'panel_1',
component: 'my_component',
title: 'my_custom_title', // <-- special param for title
});
```
You can update the title through the panel api which can be accessed via `props.api` if you are inside the panel
component or via `api.getPanel('panel1').api` if you are accessing from outside of the panel component.
```tsx
api.updateTitle('my_new_custom_title');
```
> Note this only works when using the default tab implementation.
### Custom Tab Title
If you are using a custom tab implementation you should pass variables through as a parameter and render them
through your tab components implementation.
```tsx title="Add a panel with custom parameters"
api.addPanel({
id: 'panel_2',
component: 'my_component',
tabComponent: 'my_tab',
params: {
myTitle: 'Window 2', // <-- passing a variable to use as a title
},
});
```
```tsx title="Accessing custom parameters from a custom tab renderer"
const tabComponents = {
default: (props: IDockviewPanelHeaderProps<{ myTitle: string }>) => {
const title = props.params.myTitle; // <-- accessing my custom varaible
return <div>{/** tab implementation as chosen by developer */}</div>;
},
};
```
### Hidden Headers
You may wish to hide the header section of a group. This can achieved through the `hidden` variable on `panel.group.header`.
```tsx
panel.group.header.hidden = true;
```
### Full width tabs
`DockviewReactComponent` accepts the prop `singleTabMode`. If set `singleTabMode=fullwidth` then when there is only one tab in a group this tab will expand
to the entire width of the group. For example:
> This can be conmbined with <Link to="./dockview/#locked-group">Locked Groups</Link> to create an application that feels more like a Window Manager
> rather than a collection of groups and tabs.
```tsx
<DockviewReactComponent singleTabMode="fullwidth" {...otherProps} />
```
<DockviewNative />
## Groups
### Locked group
Locking a group will disable all drop events for this group ensuring no additional panels can be added to the group through drop events.
You can still add groups to a locked panel programatically using the API though.
```tsx
panel.group.locked = true;
```
### Group Controls Panel
@ -375,3 +614,50 @@ const Component: React.FunctionComponent<IDockviewGroupControlProps> = () => {
return <DockviewReact {...props} groupControlComponent={Component} />;
```
As a simple example the below uses the `groupControlComponent` to render a small control that indicates whether the group
is active and which panel is active in that group.
```tsx
const GroupControlComponent = (props: IDockviewGroupControlProps) => {
const isGroupActive = props.isGroupActive;
const activePanel = props.activePanel;
return (
<div className="dockview-groupcontrol-demo">
<span
className="dockview-groupcontrol-demo-group-active"
style={{
background: isGroupActive ? 'green' : 'red',
}}
>
{isGroupActive ? 'Group Active' : 'Group Inactive'}
</span>
<span className="dockview-groupcontrol-demo-active-panel">{`activePanel: ${
activePanel?.id || 'null'
}`}</span>
</div>
);
};
```
<DockviewGroupControl />
## Events
<EventsDockview />
## Advanced Examples
### Nested Dockviews
You can safely create multiple dockview instances within one page and nest dockviews within other dockviews.
If you wish to interact with the drop event from one dockview instance in another dockview instance you can implement the `showDndOverlay` and `onDidDrop` props on `DockviewReact`.
<NestedDockview />
### Example
hello
<DockviewNative2 />

View File

@ -1,6 +1,6 @@
{
"name": "dockview-docs",
"version": "1.5.2",
"version": "1.6.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
@ -16,30 +16,30 @@
"deploy-docs": "node scripts/package-docs.js"
},
"dependencies": {
"@docusaurus/core": "2.0.0-beta.20",
"@docusaurus/preset-classic": "2.0.0-beta.20",
"@docusaurus/core": "2.3.1",
"@docusaurus/preset-classic": "2.3.1",
"@mdx-js/react": "^1.6.22",
"axios": "^0.27.2",
"clsx": "^1.1.1",
"dockview": "^1.5.2",
"prism-react-renderer": "^1.3.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"recoil": "^0.7.3-alpha.2",
"uuid": "^8.3.2",
"axios": "^1.3.3",
"clsx": "^1.2.1",
"dockview": "^1.6.0",
"prism-react-renderer": "^1.3.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"recoil": "^0.7.6",
"uuid": "^9.0.0",
"xml2js": "^0.4.23"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-beta.20",
"@tsconfig/docusaurus": "^1.0.5",
"@types/react": "^17.0.35",
"@types/react-dom": "^17.0.11",
"@types/uuid": "^8.3.4",
"docusaurus-plugin-sass": "^0.2.2",
"fs-extra": "^10.1.0",
"@docusaurus/module-type-aliases": "2.3.1",
"@tsconfig/docusaurus": "^1.0.6",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/uuid": "^9.0.0",
"docusaurus-plugin-sass": "^0.2.3",
"fs-extra": "^11.1.0",
"install": "^0.13.0",
"sass": "^1.52.1",
"typescript": "^4.6.4"
"sass": "^1.58.1",
"typescript": "^4.9.5"
},
"resolutions": {
"react": "17.0.2",

View File

@ -11,7 +11,7 @@ type FeatureItem = {
const FeatureList: FeatureItem[] = [
{
title: '',
Svg: require('@site/static/img/dockview_grid_2.svg').default,
Svg: require('@site/static/img/dockview_grid_3.svg').default,
description: (
<>
<div className="feature-banner">

View File

@ -84,7 +84,7 @@ export const CustomHeadersDockview = () => {
position: { referencePanel: 'panel_7', direction: 'within' },
});
event.api.addEmptyGroup();
event.api.addGroup();
};
return (

View File

@ -209,9 +209,8 @@ const Icon = (props: {
};
const Button = () => {
const [position, setPosition] = React.useState<
{ x: number; y: number } | undefined
>(undefined);
const [position, setPosition] =
React.useState<{ x: number; y: number } | undefined>(undefined);
const close = () => setPosition(undefined);
@ -316,19 +315,19 @@ export const DockviewDemo = () => {
position: { referencePanel: 'panel_7', direction: 'within' },
});
event.api.addEmptyGroup();
event.api.addGroup();
event.api.getPanel('panel_1').api.setActive();
setInterval(() => {
event.api.getPanel('panel_1').update({
params: {
params: {
title: Date.now().toString(),
},
},
});
}, 1000);
// setInterval(() => {
// event.api.getPanel('panel_1').update({
// params: {
// params: {
// title: Date.now().toString(),
// },
// },
// });
// }, 1000);
};
return (

View File

@ -4,6 +4,7 @@ import {
DockviewReact,
DockviewReadyEvent,
IDockviewPanelProps,
positionToDirection,
} from 'dockview';
import * as React from 'react';
@ -54,14 +55,12 @@ export const DndDockview = (props: { renderVisibleOnly: boolean }) => {
};
const onDidDrop = (event: DockviewDropEvent) => {
const { group } = event;
event.api.addPanel({
id: 'test',
component: 'default',
position: {
referencePanel: group.activePanel.id,
direction: 'within',
direction: positionToDirection(event.position),
referenceGroup: event.group,
},
});
};

View File

@ -0,0 +1,17 @@
.dockview-groupcontrol-demo {
height: 100%;
display: flex;
align-items: center;
color: white;
background-color: black;
padding-left: 8px;
.dockview-groupcontrol-demo-group-active {
padding: 0px 8px;
}
.dockview-groupcontrol-demo-active-panel {
color: yellow;
padding: 0px 8px;
}
}

View File

@ -0,0 +1,102 @@
import {
DockviewReact,
DockviewReadyEvent,
IDockviewGroupControlProps,
IDockviewPanelProps,
} from 'dockview';
import * as React from 'react';
import './groupControl.scss';
const components = {
default: (props: IDockviewPanelProps<{ title: string; x?: number }>) => {
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: 'white',
height: '100%',
}}
>
<span>{`${props.params.title}`}</span>
{props.params.x && <span>{` ${props.params.x}`}</span>}
</div>
);
},
};
const GroupControlComponent = (props: IDockviewGroupControlProps) => {
const isGroupActive = props.isGroupActive;
const activePanel = props.activePanel;
return (
<div className="dockview-groupcontrol-demo">
<span
className="dockview-groupcontrol-demo-group-active"
style={{
background: isGroupActive ? 'green' : 'red',
}}
>
{isGroupActive ? 'Group Active' : 'Group Inactive'}
</span>
<span className="dockview-groupcontrol-demo-active-panel">{`activePanel: ${
activePanel?.id || 'null'
}`}</span>
</div>
);
};
export const DockviewGroupControl = () => {
const onReady = (event: DockviewReadyEvent) => {
const panel1 = event.api.addPanel({
id: 'panel_1',
component: 'default',
tabComponent: 'default',
params: {
title: 'Window 1',
},
});
const panel2 = event.api.addPanel({
id: 'panel_2',
component: 'default',
tabComponent: 'default',
params: {
title: 'Window 2',
},
position: {
direction: 'right',
},
});
const panel3 = event.api.addPanel({
id: 'panel_3',
component: 'default',
tabComponent: 'default',
params: {
title: 'Window 3',
},
position: {
direction: 'below',
},
});
};
return (
<div
style={{
height: '500px',
display: 'flex',
flexDirection: 'column',
}}
>
<DockviewReact
onReady={onReady}
components={components}
groupControlComponent={GroupControlComponent}
className="dockview-theme-abyss"
/>
</div>
);
};

View File

@ -0,0 +1,34 @@
.nested-dockview {
position: relative;
::after {
content: '';
position: absolute;
top: 0px;
left: 0px;
height: 1px;
width: 100%;
background-color: var(--dv-separator-border);
}
}
.header-title {
padding: 0px 8px;
}
.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,201 @@
import {
DockviewReact,
DockviewReadyEvent,
IDockviewPanelProps,
Position,
Direction,
IDockviewPanelHeaderProps,
} from 'dockview';
import * as React from 'react';
import './native.scss';
const components = {
default: (props: IDockviewPanelProps<{ title: string; x?: number }>) => {
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: 'white',
height: '100%',
}}
>
<span>{`${props.params.title}`}</span>
{props.params.x && <span>{` ${props.params.x}`}</span>}
</div>
);
},
isolatedApp: (
props: IDockviewPanelProps<{ title: string; x?: number }>
) => {
const onReady = (event: DockviewReadyEvent) => {
const panel1 = event.api.addPanel({
id: 'panel_1',
component: 'default',
params: {
title: 'Tab 1',
},
});
const panel2 = event.api.addPanel({
id: 'panel_2',
component: 'default',
params: {
title: 'Tab 2',
},
});
const panel3 = event.api.addPanel({
id: 'panel_3',
component: 'default',
params: {
title: 'Tab 3',
},
});
};
return (
<DockviewReact
onReady={onReady}
components={components}
tabComponents={tabComponents}
className="dockview-theme-abyss"
/>
);
},
};
const tabComponents = {
default: (props: IDockviewPanelHeaderProps<{ title: string }>) => {
return (
<div className="my-custom-tab">
<span>{props.params.title}</span>
<span style={{ flexGrow: 1 }} />
<span className="my-custom-tab-icon material-symbols-outlined">
chrome_minimize
</span>
<span className="my-custom-tab-icon material-symbols-outlined">
chrome_maximize
</span>
<span className="my-custom-tab-icon material-symbols-outlined">
close
</span>
</div>
);
},
};
export const DockviewNative = () => {
const onReady = (event: DockviewReadyEvent) => {
const panel1 = event.api.addPanel({
id: 'panel_1',
component: 'default',
tabComponent: 'default',
params: {
title: 'Window 1',
},
});
panel1.group.locked = true;
const panel2 = event.api.addPanel({
id: 'panel_2',
component: 'default',
tabComponent: 'default',
params: {
title: 'Window 2',
},
position: {
direction: 'right',
},
});
panel2.group.locked = true;
const panel3 = event.api.addPanel({
id: 'panel_3',
component: 'default',
tabComponent: 'default',
params: {
title: 'Window 3',
},
position: {
direction: 'below',
},
});
panel3.group.locked = true;
};
return (
<div
style={{
height: '500px',
display: 'flex',
flexDirection: 'column',
}}
>
<DockviewReact
onReady={onReady}
components={components}
tabComponents={tabComponents}
className="dockview-theme-abyss"
singleTabMode="fullwidth"
/>
</div>
);
};
export const DockviewNative2 = () => {
const onReady = (event: DockviewReadyEvent) => {
const panel1 = event.api.addPanel({
id: 'panel_1',
component: 'isolatedApp',
tabComponent: 'default',
params: {
title: 'Window 1',
},
});
panel1.group.locked = true;
const panel2 = event.api.addPanel({
id: 'panel_2',
component: 'isolatedApp',
tabComponent: 'default',
params: {
title: 'Window 2',
},
position: {
direction: 'right',
},
});
panel2.group.locked = true;
const panel3 = event.api.addPanel({
id: 'panel_3',
component: 'isolatedApp',
tabComponent: 'default',
params: {
title: 'Window 3',
},
position: {
direction: 'below',
},
});
panel3.group.locked = true;
};
return (
<div
style={{
height: '500px',
display: 'flex',
flexDirection: 'column',
}}
>
<DockviewReact
onReady={onReady}
components={components}
tabComponents={tabComponents}
className="dockview-theme-abyss"
singleTabMode="fullwidth"
/>
</div>
);
};

View File

@ -0,0 +1,126 @@
import {
DockviewApi,
DockviewReact,
DockviewReadyEvent,
IDockviewPanelProps,
IWatermarkPanelProps,
Orientation,
} from 'dockview';
import * as React from 'react';
import './nested.scss';
const components = {
default: (props: IDockviewPanelProps<{ title: string }>) => {
return (
<div
style={{
height: '100%',
padding: '20px',
background: 'var(--dv-group-view-background-color)',
}}
>
{props.params.title}
</div>
);
},
};
const counter = (() => {
let i = 0;
return {
next: () => ++i,
};
})();
function loadDefaultLayout(api: DockviewApi) {
api.addPanel({
id: 'panel_1',
component: 'default',
});
api.addPanel({
id: 'panel_2',
component: 'default',
});
api.addPanel({
id: 'panel_3',
component: 'default',
});
}
export const DockviewPersistance = () => {
const [api, setApi] = React.useState<DockviewApi>();
const clearLayout = () => {
localStorage.removeItem('dockview_persistance_layout');
if (api) {
api.clear();
loadDefaultLayout(api);
}
};
const onReady = (event: DockviewReadyEvent) => {
const layoutString = localStorage.getItem(
'dockview_persistance_layout'
);
let success = false;
if (layoutString) {
try {
const layout = JSON.parse(layoutString);
event.api.fromJSON(layout);
success = true;
} catch (err) {
//
}
}
if (!success) {
loadDefaultLayout(event.api);
}
setApi(event.api);
};
React.useEffect(() => {
if (!api) {
return;
}
api.onDidLayoutChange(() => {
const layout = api.toJSON();
localStorage.setItem(
'dockview_persistance_layout',
JSON.stringify(layout)
);
});
}, [api]);
return (
<div
style={{
height: '500px',
display: 'flex',
flexDirection: 'column',
}}
>
<div>
<button onClick={clearLayout}>Reset Layout</button>
</div>
<DockviewReact
onReady={onReady}
components={components}
watermarkComponent={Watermark}
className="dockview-theme-abyss"
/>
</div>
);
};
const Watermark = () => {
return <div style={{ color: 'white', padding: '8px' }}>watermark</div>;
};

View File

@ -2,7 +2,6 @@ import {
DockviewReact,
DockviewReadyEvent,
IDockviewPanelProps,
PanelApi,
} from 'dockview';
import * as React from 'react';
@ -13,10 +12,10 @@ const renderVisibleComponentsOnlyAtom = atom<boolean>({
default: false,
});
function RenderWhenVisible<
T extends { api: Pick<PanelApi, 'isVisible' | 'onDidVisibilityChange'> }
>(component: React.FunctionComponent<T>) {
const HigherOrderComponent = (props: T) => {
function RenderWhenVisible(
component: React.FunctionComponent<IDockviewPanelProps>
) {
const HigherOrderComponent = (props: IDockviewPanelProps) => {
const [visible, setVisible] = React.useState<boolean>(
props.api.isVisible
);

View File

@ -23,13 +23,24 @@ const Default = (props: IDockviewPanelProps) => {
step={1}
/>
<button
style={{ width: '100px' }}
onClick={() => {
props.api.group.api.setSize({
width,
});
}}
>
Set
Resize Group
</button>
<button
style={{ width: '100px' }}
onClick={() => {
props.api.setSize({
width,
});
}}
>
Resize panel
</button>
</div>
<div className="resize-control">
@ -42,13 +53,24 @@ const Default = (props: IDockviewPanelProps) => {
step={1}
/>
<button
style={{ width: '100px' }}
onClick={() => {
props.api.group.api.setSize({
height,
});
}}
>
Set
Resize Group
</button>
<button
style={{ width: '100px' }}
onClick={() => {
props.api.setSize({
height,
});
}}
>
Resize Panel
</button>
</div>
</div>

View File

@ -0,0 +1,134 @@
import {
DockviewApi,
DockviewReact,
DockviewReadyEvent,
IDockviewPanelProps,
IWatermarkPanelProps,
Orientation,
} from 'dockview';
import * as React from 'react';
import './nested.scss';
const components = {
default: (props: IDockviewPanelProps<{ title: string }>) => {
return (
<div
style={{
height: '100%',
padding: '20px',
background: 'var(--dv-group-view-background-color)',
}}
>
{props.params.title}
</div>
);
},
};
const counter = (() => {
let i = 0;
return {
next: () => ++i,
};
})();
const Watermark = (props: IWatermarkPanelProps) => {
const isGroup = props.containerApi.groups.length > 0;
const addPanel = () => {
props.containerApi.addPanel({
id: counter.next().toString(),
component: 'default',
});
};
return (
<div
style={{
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: 'white',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
}}
>
<span>
This is a custom watermark. You can put whatever React
component you want here
</span>
<span>
<button onClick={addPanel}>Add New Panel</button>
</span>
{isGroup && (
<span>
<button
onClick={() => {
props.close();
}}
>
Close Group
</button>
</span>
)}
</div>
</div>
);
};
export const DockviewWatermark = () => {
const [api, setApi] = React.useState<DockviewApi>();
const onReady = (event: DockviewReadyEvent) => {
// event.api.addPanel({
// id: 'panel_1',
// component: 'default',
// });
event.api.fromJSON({
grid: {
orientation: Orientation.HORIZONTAL,
root: { type: 'branch', data: [] },
height: 100,
width: 100,
},
panels: {},
});
setApi(event.api);
};
const onClick = () => {
if (!api) {
return;
}
api.addGroup();
};
return (
<div
style={{
height: '500px',
display: 'flex',
flexDirection: 'column',
}}
>
<div>
<button onClick={onClick}>Add Empty Group</button>
</div>
<DockviewReact
onReady={onReady}
components={components}
watermarkComponent={Watermark}
className="dockview-theme-abyss nested-dockview"
/>
</div>
);
};

View File

@ -0,0 +1,20 @@
<svg width="156" height="18" viewBox="0 0 156 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="4" width="156" height="14" fill="#1C1C2A"/>
<rect y="4" width="30" height="14" fill="#10192C"/>
<rect x="31" y="4" width="30" height="14" fill="#10192C"/>
<rect x="62" y="4" width="30" height="14" fill="#000C18"/>
<rect x="30" y="4" width="1" height="14" fill="#2B2B4A"/>
<rect x="61" y="4" width="1" height="14" fill="#2B2B4A"/>
<rect x="92" y="4" width="1" height="14" fill="#2B2B4A"/>
<rect x="66" y="9" width="7" height="4" rx="2" fill="white"/>
<rect x="76" y="9" width="12" height="4" rx="2" fill="white"/>
<rect x="33" y="9" width="15" height="4" rx="2" fill="#777777"/>
<rect x="2" y="9" width="6" height="4" rx="2" fill="#777777"/>
<rect x="10" y="9" width="18" height="4" rx="2" fill="#777777"/>
<rect x="93" y="4" width="63" height="14" fill="#E1E1E1" fill-opacity="0.25"/>
<rect x="111.5" y="0.5" width="29" height="13" fill="#000C18" stroke="#2B2B4A"/>
<rect x="115" y="5" width="7" height="4" rx="2" fill="white"/>
<rect x="126" y="5" width="12" height="4" rx="2" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M128.344 13.2654C128.52 13.1426 128.51 12.8799 128.327 12.77L124.18 10.2905C123.961 10.1595 123.691 10.3492 123.739 10.5997L124.651 15.344C124.691 15.5542 124.935 15.653 125.11 15.5302L125.772 15.067C125.867 15.0002 125.914 14.8836 125.892 14.7693L125.602 13.2612C125.554 13.0106 125.825 12.821 126.044 12.952L127.362 13.7401C127.462 13.7999 127.588 13.7954 127.683 13.7286L128.344 13.2654Z" fill="white"/>
<rect x="127.5" y="14.5" width="4" height="2" stroke="white" stroke-dasharray="0.25 0.25"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,25 @@
<svg width="156" height="18" viewBox="0 0 156 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="4" width="156" height="14" fill="#1C1C2A"/>
<rect y="4" width="30" height="14" fill="#10192C"/>
<rect x="31" y="4" width="30" height="14" fill="#10192C"/>
<rect x="62" y="4" width="30" height="14" fill="#000C18"/>
<rect x="30" y="4" width="1" height="14" fill="#2B2B4A"/>
<rect x="61" y="4" width="1" height="14" fill="#2B2B4A"/>
<rect x="92" y="4" width="1" height="14" fill="#2B2B4A"/>
<rect x="66" y="9" width="7" height="4" rx="2" fill="#777777"/>
<rect x="76" y="9" width="12" height="4" rx="2" fill="#777777"/>
<rect x="33" y="9" width="15" height="4" rx="2" fill="#282828"/>
<rect x="2" y="9" width="6" height="4" rx="2" fill="#282828"/>
<rect x="10" y="9" width="18" height="4" rx="2" fill="#282828"/>
<rect x="59" width="91" height="10" fill="#2B2B4A"/>
<rect x="60" y="1" width="89.1429" height="8" fill="#1C1C2A"/>
<rect x="60" y="1" width="17.1429" height="8" fill="#10192C"/>
<rect x="77.7142" y="1" width="17.1429" height="8" fill="#10192C"/>
<rect x="77.1428" y="1" width="0.571429" height="8" fill="#2B2B4A"/>
<rect x="94.8572" y="1" width="0.571429" height="8" fill="#2B2B4A"/>
<rect x="78.8572" y="3.85718" width="8.57143" height="2.28571" rx="1.14286" fill="white"/>
<rect x="89.1428" y="3.85718" width="2.85714" height="2.28571" rx="1.14286" fill="white"/>
<rect x="61.1428" y="3.85718" width="10.8571" height="2.28571" rx="1.14286" fill="#777777"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M117.344 8.26544C117.52 8.14263 117.51 7.87986 117.327 7.76998L113.18 5.29049C112.961 5.15953 112.691 5.34916 112.739 5.59974L113.651 10.344C113.691 10.5542 113.935 10.653 114.11 10.5302L114.772 10.067C114.867 10.0002 114.914 9.88362 114.892 9.76929L114.602 8.26123C114.554 8.01064 114.825 7.82101 115.044 7.95197L116.362 8.74015C116.462 8.79989 116.588 8.79538 116.683 8.7286L117.344 8.26544Z" fill="white"/>
<rect x="116.5" y="9.5" width="4" height="2" stroke="white" stroke-dasharray="0.25 0.25"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

20
packages/docs/static/img/add_to_tab.svg vendored Normal file
View File

@ -0,0 +1,20 @@
<svg width="156" height="18" viewBox="0 0 156 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="4" width="156" height="14" fill="#1C1C2A"/>
<rect y="4" width="30" height="14" fill="#10192C"/>
<rect x="31" y="4" width="30" height="14" fill="#10192C"/>
<rect x="62" y="4" width="30" height="14" fill="#000C18"/>
<rect x="30" y="4" width="1" height="14" fill="#2B2B4A"/>
<rect x="61" y="4" width="1" height="14" fill="#2B2B4A"/>
<rect x="92" y="4" width="1" height="14" fill="#2B2B4A"/>
<rect x="66" y="9" width="7" height="4" rx="2" fill="white"/>
<rect x="76" y="9" width="12" height="4" rx="2" fill="white"/>
<rect x="33" y="9" width="15" height="4" rx="2" fill="#777777"/>
<rect x="2" y="9" width="6" height="4" rx="2" fill="#777777"/>
<rect x="10" y="9" width="18" height="4" rx="2" fill="#777777"/>
<path d="M31 4H61V18H31V4Z" fill="#E1E1E1" fill-opacity="0.25"/>
<rect x="49.5" y="0.5" width="29" height="13" fill="#000C18" stroke="#2B2B4A"/>
<rect x="53" y="5" width="7" height="4" rx="2" fill="white"/>
<rect x="64" y="5" width="12" height="4" rx="2" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M66.3445 13.2654C66.5198 13.1426 66.5104 12.8799 66.3266 12.77L62.1804 10.2905C61.9614 10.1595 61.6906 10.3492 61.7388 10.5997L62.6506 15.344C62.691 15.5542 62.9347 15.653 63.1101 15.5302L63.7716 15.067C63.8669 15.0002 63.9142 14.8836 63.8922 14.7693L63.6024 13.2612C63.5542 13.0106 63.825 12.821 64.044 12.952L65.362 13.7401C65.4619 13.7999 65.5876 13.7954 65.683 13.7286L66.3445 13.2654Z" fill="white"/>
<rect x="65.5" y="14.5" width="4" height="2" stroke="white" stroke-dasharray="0.25 0.25"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,45 @@
<svg width="156" height="121" viewBox="0 0 156 121" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="156" height="14" fill="#1C1C2A"/>
<rect width="30" height="14" fill="#10192C"/>
<rect x="31" width="30" height="14" fill="#10192C"/>
<rect x="62" width="30" height="14" fill="#000C18"/>
<rect x="30" width="1" height="14" fill="#2B2B4A"/>
<rect x="61" width="1" height="14" fill="#2B2B4A"/>
<rect x="92" width="1" height="14" fill="#2B2B4A"/>
<rect x="66" y="5" width="7" height="4" rx="2" fill="white"/>
<rect x="76" y="5" width="12" height="4" rx="2" fill="white"/>
<rect x="33" y="5" width="15" height="4" rx="2" fill="#777777"/>
<rect x="2" y="5" width="6" height="4" rx="2" fill="#777777"/>
<rect x="10" y="5" width="18" height="4" rx="2" fill="#777777"/>
<rect y="14" width="156" height="107" fill="#000C18"/>
<rect x="38" y="14" width="80" height="25" fill="#E1E1E1" fill-opacity="0.25"/>
<rect x="38" y="96" width="80" height="25" fill="#E1E1E1" fill-opacity="0.25"/>
<rect y="29" width="30" height="77" fill="#E1E1E1" fill-opacity="0.25"/>
<rect x="38" y="48" width="80" height="38" fill="#E1E1E1" fill-opacity="0.25"/>
<rect x="126" y="29" width="30" height="77" fill="#E1E1E1" fill-opacity="0.25"/>
<rect x="63.5" y="20.5" width="29" height="13" fill="#000C18" stroke="#2B2B4A"/>
<rect x="67" y="25" width="7" height="4" rx="2" fill="white"/>
<rect x="78" y="25" width="12" height="4" rx="2" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M80.3445 33.2654C80.5198 33.1426 80.5104 32.8799 80.3266 32.77L76.1804 30.2905C75.9614 30.1595 75.6906 30.3492 75.7388 30.5997L76.6506 35.344C76.691 35.5542 76.9347 35.653 77.1101 35.5302L77.7716 35.067C77.8669 35.0002 77.9142 34.8836 77.8922 34.7693L77.6024 33.2612C77.5542 33.0106 77.825 32.821 78.044 32.952L79.362 33.7401C79.4619 33.7999 79.5876 33.7954 79.683 33.7286L80.3445 33.2654Z" fill="white"/>
<rect x="79.5" y="34.5" width="4" height="2" stroke="white" stroke-dasharray="0.25 0.25"/>
<rect x="122.5" y="62.5" width="29" height="13" fill="#000C18" stroke="#2B2B4A"/>
<rect x="126" y="67" width="7" height="4" rx="2" fill="white"/>
<rect x="137" y="67" width="12" height="4" rx="2" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M139.344 75.2654C139.52 75.1426 139.51 74.8799 139.327 74.77L135.18 72.2905C134.961 72.1595 134.691 72.3492 134.739 72.5997L135.651 77.344C135.691 77.5542 135.935 77.653 136.11 77.5302L136.772 77.067C136.867 77.0002 136.914 76.8836 136.892 76.7693L136.602 75.2612C136.554 75.0106 136.825 74.821 137.044 74.952L138.362 75.7401C138.462 75.7999 138.588 75.7954 138.683 75.7286L139.344 75.2654Z" fill="white"/>
<rect x="138.5" y="76.5" width="4" height="2" stroke="white" stroke-dasharray="0.25 0.25"/>
<rect x="62.5" y="100.5" width="29" height="13" fill="#000C18" stroke="#2B2B4A"/>
<rect x="66" y="105" width="7" height="4" rx="2" fill="white"/>
<rect x="77" y="105" width="12" height="4" rx="2" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M79.3445 113.265C79.5198 113.143 79.5104 112.88 79.3266 112.77L75.1804 110.29C74.9614 110.16 74.6906 110.349 74.7388 110.6L75.6506 115.344C75.691 115.554 75.9347 115.653 76.1101 115.53L76.7716 115.067C76.8669 115 76.9142 114.884 76.8922 114.769L76.6024 113.261C76.5542 113.011 76.825 112.821 77.044 112.952L78.362 113.74C78.4619 113.8 78.5876 113.795 78.683 113.729L79.3445 113.265Z" fill="white"/>
<rect x="78.5" y="114.5" width="4" height="2" stroke="white" stroke-dasharray="0.25 0.25"/>
<rect x="4.5" y="62.5" width="29" height="13" fill="#000C18" stroke="#2B2B4A"/>
<rect x="8" y="67" width="7" height="4" rx="2" fill="white"/>
<rect x="19" y="67" width="12" height="4" rx="2" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.3445 75.2654C21.5198 75.1426 21.5104 74.8799 21.3266 74.77L17.1804 72.2905C16.9614 72.1595 16.6906 72.3492 16.7388 72.5997L17.6506 77.344C17.691 77.5542 17.9347 77.653 18.1101 77.5302L18.7716 77.067C18.8669 77.0002 18.9142 76.8836 18.8922 76.7693L18.6024 75.2612C18.5542 75.0106 18.825 74.821 19.044 74.952L20.362 75.7401C20.4619 75.7999 20.5876 75.7954 20.683 75.7286L21.3445 75.2654Z" fill="white"/>
<rect x="20.5" y="76.5" width="4" height="2" stroke="white" stroke-dasharray="0.25 0.25"/>
<rect x="63.5" y="62.5" width="29" height="13" fill="#000C18" stroke="#2B2B4A"/>
<rect x="67" y="67" width="7" height="4" rx="2" fill="white"/>
<rect x="78" y="67" width="12" height="4" rx="2" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M80.3445 75.2654C80.5198 75.1426 80.5104 74.8799 80.3266 74.77L76.1804 72.2905C75.9614 72.1595 75.6906 72.3492 75.7388 72.5997L76.6506 77.344C76.691 77.5542 76.9347 77.653 77.1101 77.5302L77.7716 77.067C77.8669 77.0002 77.9142 76.8836 77.8922 76.7693L77.6024 75.2612C77.5542 75.0106 77.825 74.821 78.044 74.952L79.362 75.7401C79.4619 75.7999 79.5876 75.7954 79.683 75.7286L80.3445 75.2654Z" fill="white"/>
<rect x="79.5" y="76.5" width="4" height="2" stroke="white" stroke-dasharray="0.25 0.25"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,53 @@
<svg width="163" height="131" viewBox="0 0 163 131" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 5C4 2.23858 6.23858 0 9 0H154C156.761 0 159 2.23858 159 5V11H4V5Z" fill="#DCDCDC"/>
<rect x="4" y="10" width="155" height="1" fill="#BABABA"/>
<rect x="4" y="11" width="155" height="61" fill="#000C18"/>
<rect x="81" y="73" width="78" height="58" fill="#000C18"/>
<rect x="4" y="73" width="77" height="58" fill="#000C18"/>
<rect x="4" y="72" width="155" height="1" fill="#2B2B4A"/>
<path d="M81 73H80V131H81V73Z" fill="#2B2B4A"/>
<rect x="4" y="11" width="155" height="14" fill="#1C1C2A"/>
<rect x="4" y="11" width="30" height="14" fill="#10192C"/>
<rect x="35" y="11" width="30" height="14" fill="#000C18"/>
<rect x="34" y="11" width="1" height="14" fill="#2B2B4A"/>
<rect x="65" y="11" width="1" height="14" fill="#2B2B4A"/>
<rect x="81" y="73" width="78" height="14" fill="#1C1C2A"/>
<rect x="81" y="73" width="24" height="14" fill="#10192C"/>
<rect x="105" y="73" width="24" height="14" fill="#000C18"/>
<rect x="105" y="73" width="0.503226" height="14" fill="#2B2B4A"/>
<rect x="129" y="73" width="0.503226" height="14" fill="#2B2B4A"/>
<rect x="38" y="16" width="12" height="4" rx="2" fill="#777777"/>
<rect x="107" y="78" width="7" height="4" rx="2" fill="#777777"/>
<rect x="115" y="78" width="11" height="4" rx="2" fill="#777777"/>
<rect x="53" y="16" width="4" height="4" rx="2" fill="#777777"/>
<rect x="7" y="16" width="5" height="4" rx="2" fill="#282828"/>
<rect x="13" y="16" width="16" height="4" rx="2" fill="#282828"/>
<rect x="84" y="78" width="16" height="4" rx="2" fill="#282828"/>
<rect x="8" y="3" width="4" height="4" rx="2" fill="#FD605E"/>
<rect x="14" y="3" width="4" height="4" rx="2" fill="#FBBC3F"/>
<rect x="20" y="3" width="4" height="4" rx="2" fill="#34C942"/>
<rect x="149" y="34" width="10" height="77" fill="#E1E1E1" fill-opacity="0.25"/>
<rect x="4" y="34" width="10" height="77" fill="#E1E1E1" fill-opacity="0.25"/>
<rect x="18" y="11" width="124" height="10" fill="#E1E1E1" fill-opacity="0.25"/>
<rect x="18" y="121" width="124" height="10" fill="#E1E1E1" fill-opacity="0.25"/>
<rect x="90.5" y="112.5" width="29" height="13" fill="#000C18" stroke="#2B2B4A"/>
<rect x="94" y="117" width="7" height="4" rx="2" fill="white"/>
<rect x="105" y="117" width="12" height="4" rx="2" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M107.344 125.265C107.52 125.143 107.51 124.88 107.327 124.77L103.18 122.29C102.961 122.16 102.691 122.349 102.739 122.6L103.651 127.344C103.691 127.554 103.935 127.653 104.11 127.53L104.772 127.067C104.867 127 104.914 126.884 104.892 126.769L104.602 125.261C104.554 125.011 104.825 124.821 105.044 124.952L106.362 125.74C106.462 125.8 106.588 125.795 106.683 125.729L107.344 125.265Z" fill="white"/>
<rect x="106.5" y="126.5" width="4" height="2" stroke="white" stroke-dasharray="0.25 0.25"/>
<rect x="0.5" y="67.5" width="29" height="13" fill="#000C18" stroke="#2B2B4A"/>
<rect x="4" y="72" width="7" height="4" rx="2" fill="white"/>
<rect x="15" y="72" width="12" height="4" rx="2" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.3445 80.2654C17.5198 80.1426 17.5104 79.8799 17.3266 79.77L13.1804 77.2905C12.9614 77.1595 12.6906 77.3492 12.7388 77.5997L13.6506 82.344C13.691 82.5542 13.9347 82.653 14.1101 82.5302L14.7716 82.067C14.8669 82.0002 14.9142 81.8836 14.8922 81.7693L14.6024 80.2612C14.5542 80.0106 14.825 79.821 15.044 79.952L16.362 80.7401C16.4619 80.7999 16.5876 80.7954 16.683 80.7286L17.3445 80.2654Z" fill="white"/>
<rect x="16.5" y="81.5" width="4" height="2" stroke="white" stroke-dasharray="0.25 0.25"/>
<rect x="133.5" y="50.5" width="29" height="13" fill="#000C18" stroke="#2B2B4A"/>
<rect x="137" y="55" width="7" height="4" rx="2" fill="white"/>
<rect x="148" y="55" width="12" height="4" rx="2" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M150.344 63.2654C150.52 63.1426 150.51 62.8799 150.327 62.77L146.18 60.2905C145.961 60.1595 145.691 60.3492 145.739 60.5997L146.651 65.344C146.691 65.5542 146.935 65.653 147.11 65.5302L147.772 65.067C147.867 65.0002 147.914 64.8836 147.892 64.7693L147.602 63.2612C147.554 63.0106 147.825 62.821 148.044 62.952L149.362 63.7401C149.462 63.7999 149.588 63.7954 149.683 63.7286L150.344 63.2654Z" fill="white"/>
<rect x="149.5" y="64.5" width="4" height="2" stroke="white" stroke-dasharray="0.25 0.25"/>
<rect x="84.5" y="14.5" width="29" height="13" fill="#000C18" stroke="#2B2B4A"/>
<rect x="88" y="19" width="7" height="4" rx="2" fill="white"/>
<rect x="99" y="19" width="12" height="4" rx="2" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M101.344 27.2654C101.52 27.1426 101.51 26.8799 101.327 26.77L97.1804 24.2905C96.9614 24.1595 96.6906 24.3492 96.7388 24.5997L97.6506 29.344C97.691 29.5542 97.9347 29.653 98.1101 29.5302L98.7716 29.067C98.8669 29.0002 98.9142 28.8836 98.8922 28.7693L98.6024 27.2612C98.5542 27.0106 98.825 26.821 99.044 26.952L100.362 27.7401C100.462 27.7999 100.588 27.7954 100.683 27.7286L101.344 27.2654Z" fill="white"/>
<rect x="100.5" y="28.5" width="4" height="2" stroke="white" stroke-dasharray="0.25 0.25"/>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -0,0 +1,119 @@
---
sidebar_position: 1
description: How to get started with Dockview
---
import { SimpleSplitview } from '@site/src/components/simpleSplitview';
import { SimpleSplitview2 } from '@site/src/components/simpleSplitview2';
# Basics
asd
This section will take you through a number of concepts that can be applied to all dockview components.
## Panels
The below examples use `ReactSplitview` but the logic holds for `ReactPaneview`, `ReactGridview` and `ReactDockview` using their respective implementations and interfaces.
All components require you to provide an `onReady` prop which you can use to build and control your component.
### Adding a panel with parameters
You can pass parameters to a panel through the `params` object
```tsx
const onReady = (event: SplitviewReadyEvent) => {
event.api.addPanel({
id: 'panel_1',
component: 'myComponent',
params: {
title: 'My Title',
},
});
};
```
and you can access those properties through the `props.params` object. The TypeScript interface accepts an optional generic type `T` that corresponds to the params objects type.
```tsx
const MyComponent = (props: ISplitviewPanelProps<{ title: string }>) => {
return <div>{`My first panel has the title: ${props.params.title}`}</div>;
};
```
## API
There are two types of API you will interact with using `dockview`.
- The `panel API` is accessible via `props.api` in user defined panels and via the `.api` variable found on panel instances. This API contains actions and variable related to the the individual panel.
- The `container API` is accessible via `event.api` in the `onReady` events and `props.containerApi` in user defined panels. This API contains actions and variable related to the component as a whole.
```tsx
const MyComponent = (props: ISplitviewPanelProps<{ title: string }>) => {
React.useEffect(() => {
const disposable = props.api.onDidActiveChange((event) => {
console.log(`is panel active: ${event.isActive}`);
});
return () => {
disposable.dispose(); // remember to dispose of any subscriptions
};
}, [props.api]);
const addAnotherPanel = React.useCallback(() => {
props.containerApi.addPanel({
id: 'another_id',
component: 'anotherComponent',
});
}, [props.containerApi]);
return (
<div>
<span>{`My first panel has the title: ${props.params.title}`}</span>
<button onClick={addAnotherPanel}>Add Panel</button>
</div>
);
};
```
### Serialization
All components support `toJSON(): T` which returns a Typed object representation of the components state. This same Typed object can be used to deserialize a view using `fromJSON(object: T): void`.
## Auto resizing
`SplitviewReact`, `GridviewReact`, `PaneviewReact` and `DockviewReact` will all automatically resize to fill the size of their parent element.
Internally this is achieved using a [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) which some users may need to polyfill.
You can disable this by settings the `disableAutoResizing` prop to be `true`.
You can manually resize a component using the API method `layout(width: number, height: number): void`.
An advanced case may use this in conjunction with `disableAutoResizing=true` to allow a parent component to have ultimate control over the dimensions of the component.
## Events
Many API properties can be listened on using the `Event` pattern. For example `api.onDidFocusChange(() => {...})`.
You should dispose of any event listeners you create cleaning up any listeners you would have created.
```tsx
React.useEffect(() => {
const disposable = api.onDidFocusChange(() => {
// write some code
});
return () => {
disposable.dispose();
};
}, []);
```
## Proportional layout
The `proportionalLayout` property indicates the expected behaviour of the component as it's container resizes, should all views resize equally or should just one view expand to fill the new space. `proportionalLayout` can be set as a property on `SplitviewReact` and `GridviewReact` components.
Although not configurable on `DockviewReact` and `PaneviewReact` these both behave as if `proportionalLayout=true` was set for them.
<SimpleSplitview2 proportional={false} />
<SimpleSplitview2 proportional={true} />
## Browser support
dockview is intended to support all major browsers. Some users may require a polyfill for [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver).

View File

@ -0,0 +1,9 @@
{
"label": "Components",
"collapsible": true,
"collapsed": false,
"link": {
"type": "generated-index",
"title": "Components"
}
}

View File

@ -0,0 +1,663 @@
---
description: Dockview Documentation
---
import { SimpleDockview } from '@site/src/components/simpleDockview';
import {
RenderingDockview,
Checkbox,
} from '@site/src/components/dockview/rendering';
import { DndDockview } from '@site/src/components/dockview/dnd';
import { EventsDockview } from '@site/src/components/dockview/events';
import { ContextMenuDockview } from '@site/src/components/dockview/contextMenu';
import { NestedDockview } from '@site/src/components/dockview/nested';
import { CustomHeadersDockview } from '@site/src/components/dockview/customHeaders';
import { ResizeDockview } from '@site/src/components/dockview/resize';
import { DockviewGroupControl } from '@site/src/components/dockview/groupControl';
import { DockviewWatermark } from '@site/src/components/dockview/watermark';
import { DockviewPersistance } from '@site/src/components/dockview/persistance';
import {
DockviewNative,
DockviewNative2,
} from '@site/src/components/dockview/native';
import Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
# Dockview
## Introduction
Dockview is an abstraction built on top of [Gridviews](./gridview) where each view is a container of many tabbed panels.
<div
style={{
height: '300px',
backgroundColor: 'rgb(30,30,30)',
color: 'white',
margin: '20px 0px',
}}
>
<SimpleDockview />
</div>
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
You can create a Dockview through the use of the `ReactDockview` component.
```tsx
import { ReactDockview } from 'dockview';
```
| Property | Type | Optional | Default | Description |
| --------------------- | ------------------------------------ | -------- | --------- | ------------------------------------------------------------ |
| onReady | (event: SplitviewReadyEvent) => void | No | | |
| components | object | No | | |
| tabComponents | object | Yes | | |
| watermarkComponent | object | Yes | | |
| hideBorders | boolean | Yes | false | |
| className | string | Yes | '' | |
| disableAutoResizing | boolean | Yes | false | See <Link to="../basics/#auto-resizing">Auto Resizing</Link> |
| onDidDrop | Event | Yes | false | |
| showDndOverlay | Event | Yes | false | |
| defaultTabComponent | object | Yes | | |
| groupControlComponent | object | Yes | | |
| tabHeight | number | Yes | | |
| singleTabMode | 'fullwidth' \| 'default' | Yes | 'default' | |
## Dockview API
The Dockview API is exposed both at the `onReady` event and on each panel through `props.containerApi`.
Through this API you can control general features of the component and access all added panels.
```tsx title="Dockview API via Panel component"
const MyComponent = (props: IDockviewPanelProps<{ title: string }>) => {
// props.containerApi...
return <div>{`My first panel has the title: ${props.params.title}`}</div>;
};
```
```tsx title="Dockview API via the onReady callback"
const onReady = (event: DockviewReadyEvent) => {
// event.api...
};
```
| Property | Type | Description |
| ---------------------- | ---------------------------------------------------- | -------------------------------------------------------- |
| height | `number` | Component pixel height |
| width | `number` | Component pixel width |
| minimumHeight | `number` | |
| maximumHeight | `number` | |
| maximumWidth | `number` | |
| maximumWidth | `number` | |
| length | `number` | Number of panels |
| size | `number` | Number of Groups |
| panels | `IDockviewPanel[]` | |
| groups | `GroupPanel[]` | |
| activePanel | `IDockviewPanel \| undefined` | |
| activeGroup | `IDockviewPanel \| undefined` | |
| | | |
| onDidLayoutChange | `Event<void>` | |
| onDidLayoutFromJSON | `Event<void>` | |
| onDidAddGroup | `Event<GroupPanel>` | |
| onDidRemoveGroup | `Event<GroupPanel>` | |
| onDidActiveGroupChange | `Event<GroupPanel \| undefined>` | |
| onDidAddPanel | `Event<IDockviewPanel>` | |
| onDidRemovePanel | `Event<IDockviewPanel>` | |
| onDidActivePanelChange | `Event<IDockviewPanel \| undefined>` | |
| onDidDrop | `Event<DockviewDropEvent` | |
| | | |
| addPanel | `addPanel(options: AddPanelOptions): IDockviewPanel` | |
| getPanel | `(id: string) \| IDockviewPanel \| undefined` | |
| addGroup | `(options? AddGroupOptions): void` | |
| closeAllGroups | `(): void` | |
| removeGroup | `(group: GroupPanel): void` | |
| getGroup | `(id: string): GroupPanel \| undefined` | |
| | | |
| getTabHeight | `(): number \| undefined` | |
| setTabHeight | `(height: number \| undefined): void` | |
| updateOptions | `(options:SplitviewComponentUpdateOptions): void` | |
| focus | `(): void` | |
| layout | `(width: number, height:number): void` | <Link to="../basics/#auto-resizing">Auto Resizing</Link> |
| fromJSON | `(data: SerializedDockview): void` | <Link to="../basics/#serialization">Serialization</Link> |
| toJSON | `(): SerializedDockview` | <Link to="../basics/#serialization">Serialization</Link> |
| clear | `(): void` | Clears the current layout |
## Dockview Panel API
```tsx
const MyComponent = (props: IDockviewPanelProps<{ title: string }>) => {
// props.api...
return <div>{`My first panel has the title: ${props.params.title}`}</div>;
};
```
| Property | Type | Description |
| ---------------------- | ----------------------------------------------------------- | ---------------- |
| id | `string` | Panel id |
| isFocused | `boolean` | Is panel focused |
| isActive | `boolean` | Is panel active |
| width | `number` | Panel width |
| height | `number` | Panel height |
| onDidDimensionsChange | `Event<PanelDimensionChangeEvent>` | |
| onDidFocusChange | `Event<FocusEvent>` | |
| onDidVisibilityChange | `Event<VisibilityEvent>` | |
| onDidActiveChange | `Event<ActiveEvent>` | |
| setActive | `(): void` | |
| | | |
| onDidConstraintsChange | `onDidConstraintsChange: Event<PanelConstraintChangeEvent>` | |
| setConstraints | `(value: PanelConstraintChangeEvent2): void;` | |
| setSize | `(event: SizeEvent): void` | |
| | | |
| group | `GroupPanel | undefined` |
| isGroupActive | `boolean` | |
| title | `string` | |
| suppressClosable | `boolean` | |
| close | `(): void` | |
| setTitle | `(title: string): void` | |
## Layout Persistance
Layouts are loaded and saved via to `fromJSON` and `toJSON` methods on the Dockview api.
The api also exposes an event `onDidLayoutChange` you can listen on to determine when the layout has changed.
Below are some snippets showing how you might load from and save to localStorage.
```tsx title="Saving the layout state to localStorage"
React.useEffect(() => {
if (!api) {
return;
}
const disposable = api.onDidLayoutChange(() => {
const layout = api.toJSON();
localStorage.setItem(
'dockview_persistance_layout',
JSON.stringify(layout)
);
});
return () => {
disposable.dispose();
};
}, [api]);
```
```tsx title="Loading a layout from localStorage"
const onReady = (event: DockviewReadyEvent) => {
const layoutString = localStorage.getItem('dockview_persistance_layout');
let success = false;
if (layoutString) {
try {
const layout = JSON.parse(layoutString);
event.api.fromJSON(layout);
success = true;
} catch (err) {
//
}
}
if (!success) {
// do something if there is no layout or there was a loading error
}
};
```
Here is an example using the above code loading from and saving to localStorage.
If you refresh the page you should notice your layout is loaded as you left it.
<DockviewPersistance />
## Resizing
Each Dockview contains of a number of groups and each group has a number of panels.
Logically a user may want to resize a panel, but this translates to resizing the group which contains that panel.
You can set the size of a panel using `props.api.setSize(...)`.
You can also set the size of the group associated with the panel using `props.api.group.api.setSize(...)` although this isn't recommended
due to the clunky syntax.
```tsx
// it's mandatory to provide either a height or a width, providing both is optional
props.api.setSize({
height: 100,
width: 200,
});
// you could also resize the panels group, although not recommended it achieved the same result
props.api.group.api.setSize({
height: 100,
width: 200,
});
```
You can see an example invoking both approaches below.
<ResizeDockview />
## Watermark
When the dockview is empty you may want to display some fallback content, this is refered to as the `watermark`.
By default there the watermark has no content but you can provide as a prop to `DockviewReact` a `watermarkComponent`
which will be rendered when there are no panels or groups.
<DockviewWatermark />
## Drag And Drop
### Built-in behaviours
Dockview supports a wide variety of built-in Drag and Drop possibilities.
Below are some examples of the operations you can perform.
<img style={{ width: '60%' }} src={useBaseUrl('/img/add_to_tab.svg')} />
> Drag a tab onto another tab to place it inbetween existing tabs.
<img style={{ width: '60%' }} src={useBaseUrl('/img/add_to_empty_space.svg')} />
> Drag a tab to the right of the last tab to place it after the existing tabs.
<img style={{ width: '60%' }} src={useBaseUrl('/img/add_to_group.svg')} />
> Drag a group onto an existing group to merge the two groups.
<div style={{ display: 'flex', justifyContent: 'space-around' }}>
<img style={{ width: '40%' }} src={useBaseUrl('/img/drop_positions.svg')} />
<img
style={{ width: '40%' }}
src={useBaseUrl('/img/magnet_drop_positions.svg')}
/>
</div>
> Drag into the left/right/top/bottom target zone of a panel to create a new group in the selected direction.
> Drag into the center of a panel to add to that group.
> Drag to the edge of the dockview component to create a new group on the selected edge.
### Extended behaviours
For interaction with the Drag events directly the component exposes some method to help determine whether external drag events should be interacted with or not.
```tsx
/**
* called when an ondrop event which does not originate from the dockview libray and
* passes the showDndOverlay condition occurs
**/
const onDidDrop = (event: DockviewDropEvent) => {
const { group } = event;
event.api.addPanel({
id: 'test',
component: 'default',
position: {
referencePanel: group.activePanel.id,
direction: 'within',
},
});
};
/**
* called for drag over events which do not originate from the dockview library
* allowing the developer to decide where the overlay should be shown for a
* particular drag event
**/
const showDndOverlay = (event: DockviewDndOverlayEvent) => {
return true;
};
return (
<DockviewReact
components={components}
onReady={onReady}
className="dockview-theme-abyss"
onDidDrop={onDidDrop}
showDndOverlay={showDndOverlay}
/>
);
```
<DndDockview />
## Panels
### Add Panel
Using the dockview API you can access the `addPanel` method which returns an instance of the created panel.
The minimum method signature is:
```ts
const panel = api.addPanel({
id: 'my_unique_panel_id',
component: 'my_component',
});
```
where `id` is the unique id of the panel and `component` is the implenentation which
will be used to render the panel. You will have registered this using the `components` prop of the `DockviewReactComponent` component.
You can optionally provide a `tabComponent` parameters to the `addPanel` method which will render the tab using a custom renderer.
You will have registered this using the `tabComponents` prop of the `DockviewReactComponent` component.
```ts
const panel = api.addPanel({
id: 'my_unique_panel_id',
component: 'my_component',
tabComponent: 'my_tab_component',
});
```
You can pass properties to the panel using the `params` key.
You can update these properties through the panels `api` object and its `updateParameters` method.
```ts
const panel = api.addPanel({
id: 'my_unique_panel_id',
component: 'my_component',
params: {
myCustomKey: 'my_custom_value',
},
});
panel.api.updateParameters({
myCustomKey: 'my_custom_value',
myOtherCustomKey: 'my_other_custom_key',
});
```
> Note `updateParameters` does not accept partial parameter updates, you should call it with the entire set of parameters
> you want the panel to receive.
Finally `addPanel` accepts a `position` object which tells dockview where to place the panel.
- This object optionally accepts either a `referencePanel` or `referenceGroup` which can be the associated id as a string
or the panel/group object reference.
- This object accepts a `direction` property which dictates where,
relative to the provided reference the new panel will be placed.
> If neither a `referencePanel` or `referenceGroup` then the provided `direction` will be treated as absolute.
> If no `direction` is provided the library will place the new panel in a pre-determined position.
```ts
const panel = api.addPanel({
id: 'panel_1',
component: 'default',
});
const panel2 = api.addPanel({
id: 'panel_2',
component: 'default',
position: {
referencePanel: panel1,
direction: 'right',
},
});
```
### Panel Rendering
By default `DockviewReact` only adds to the DOM those panels that are visible,
if a panel is not the active tab and not shown the contents of the hidden panel will be removed from the DOM.
However the React Components associated with each panel are only created once and will always exist for as long as the panel exists, hidden or not.
> For example this means that any hooks in those components will run whether the panel is visible or not which may lead to excessive background work depending
> on the panels implementation.
This is the default behaviour to ensure the greatest flexibility for the user but through the panels `props.api` you can listen to the visiblity state of the panel
and write additional logic to optimize your application.
For example if you wanted to unmount the React Components when the panel is not visible you could create a Higher-Order-Component that listens to the panels
visiblity state and only renders the panel when visible.
```tsx title="Only rendering the React Component when the panel is visible, otherwise rendering a null React Component"
import { IDockviewPanelProps } from 'dockview';
import * as React from 'react';
function RenderWhenVisible(
component: React.FunctionComponent<IDockviewPanelProps>
) {
const HigherOrderComponent = (props: IDockviewPanelProps) => {
const [visible, setVisible] = React.useState<boolean>(
props.api.isVisible
);
React.useEffect(() => {
const disposable = props.api.onDidVisibilityChange((event) =>
setVisible(event.isVisible)
);
return () => {
disposable.dispose();
};
}, [props.api]);
if (!visible) {
return null;
}
return React.createElement(component, props);
};
return HigherOrderComponent;
}
```
```tsx
const components = { default: RenderWhenVisible(MyComponent) };
```
Toggling the checkbox you can see that when you only render those panels which are visible the underling React component is destroyed when it becomes hidden and re-created when it becomes visible.
<Checkbox />
<div
style={{
height: '300px',
backgroundColor: 'rgb(30,30,30)',
color: 'white',
margin: '20px 0px',
}}
>
<RenderingDockview renderVisibleOnly={false} />
</div>
## Headers
### Custom Tab Headers
You can provide custom renderers for your tab headers for maximum customization.
A default implementation of `DockviewDefaultTab` is provided should you only wish to attach minor
changes and events that do not alter the default behaviour, for example to add a custom context menu event
handler.
```tsx title="Attaching a custom context menu event handlers to a custom header"
import { IDockviewPanelHeaderProps, DockviewDefaultTab } from 'dockview';
const MyCustomheader = (props: IDockviewPanelHeaderProps) => {
const onContextMenu = (event: React.MouseEvent) => {
event.preventDefault();
alert('context menu');
};
return <DockviewDefaultTab onContextMenu={onContextMenu} {...props} />;
};
```
You are also free to define a custom renderer entirely from scratch and not make use of the `DockviewDefaultTab` component.
To use a custom renderer you can must register a collection of tab components.
```tsx
const tabComponents = {
myCustomHeader: MyCustomHeader,
};
return <DockviewReact tabComponents={tabComponents} ... />;
```
```tsx
api.addPanel({
id: 'panel_1',
component: 'default',
tabComponent: 'myCustomHeader', // <-- your registered renderers
title: 'Panel 1',
});
```
You can also override the default tab renderer which will be used when no `tabComponent` is provided to the `addPanel` function.
```tsx
<DockviewReact defaultTabComponent={MyCustomHeader} ... />;
```
As a simple example the below attachs a custom event handler for the context menu on all tabs as a default tab renderer
The below example uses a custom tab renderer to reigster a popover when the user right clicked on a tab.
This still makes use of the `DockviewDefaultTab` since it's only a minor change.
<CustomHeadersDockview />
### Default Tab Title
If you are using the default tab renderer you can set the title of a tab when creating it
```tsx
api.addPanel({
id: 'panel_1',
component: 'my_component',
title: 'my_custom_title', // <-- special param for title
});
```
You can update the title through the panel api which can be accessed via `props.api` if you are inside the panel
component or via `api.getPanel('panel1').api` if you are accessing from outside of the panel component.
```tsx
api.updateTitle('my_new_custom_title');
```
> Note this only works when using the default tab implementation.
### Custom Tab Title
If you are using a custom tab implementation you should pass variables through as a parameter and render them
through your tab components implementation.
```tsx title="Add a panel with custom parameters"
api.addPanel({
id: 'panel_2',
component: 'my_component',
tabComponent: 'my_tab',
params: {
myTitle: 'Window 2', // <-- passing a variable to use as a title
},
});
```
```tsx title="Accessing custom parameters from a custom tab renderer"
const tabComponents = {
default: (props: IDockviewPanelHeaderProps<{ myTitle: string }>) => {
const title = props.params.myTitle; // <-- accessing my custom varaible
return <div>{/** tab implementation as chosen by developer */}</div>;
},
};
```
### Hidden Headers
You may wish to hide the header section of a group. This can achieved through the `hidden` variable on `panel.group.header`.
```tsx
panel.group.header.hidden = true;
```
### Full width tabs
`DockviewReactComponent` accepts the prop `singleTabMode`. If set `singleTabMode=fullwidth` then when there is only one tab in a group this tab will expand
to the entire width of the group. For example:
> This can be conmbined with <Link to="./dockview/#locked-group">Locked Groups</Link> to create an application that feels more like a Window Manager
> rather than a collection of groups and tabs.
```tsx
<DockviewReactComponent singleTabMode="fullwidth" {...otherProps} />
```
<DockviewNative />
## Groups
### Locked group
Locking a group will disable all drop events for this group ensuring no additional panels can be added to the group through drop events.
You can still add groups to a locked panel programatically using the API though.
```tsx
panel.group.locked = true;
```
### Group Controls Panel
`DockviewReact` accepts a prop `groupControlComponent` which expects a React component whos props are `IDockviewGroupControlProps`.
This control will be rendered inside the header bar on the right hand side for each group of tabs.
```tsx
const Component: React.FunctionComponent<IDockviewGroupControlProps> = () => {
return <div>{'...'}</div>;
};
return <DockviewReact {...props} groupControlComponent={Component} />;
```
As a simple example the below uses the `groupControlComponent` to render a small control that indicates whether the group
is active and which panel is active in that group.
```tsx
const GroupControlComponent = (props: IDockviewGroupControlProps) => {
const isGroupActive = props.isGroupActive;
const activePanel = props.activePanel;
return (
<div className="dockview-groupcontrol-demo">
<span
className="dockview-groupcontrol-demo-group-active"
style={{
background: isGroupActive ? 'green' : 'red',
}}
>
{isGroupActive ? 'Group Active' : 'Group Inactive'}
</span>
<span className="dockview-groupcontrol-demo-active-panel">{`activePanel: ${
activePanel?.id || 'null'
}`}</span>
</div>
);
};
```
<DockviewGroupControl />
## Events
<EventsDockview />
## Advanced Examples
### Nested Dockviews
You can safely create multiple dockview instances within one page and nest dockviews within other dockviews.
If you wish to interact with the drop event from one dockview instance in another dockview instance you can implement the `showDndOverlay` and `onDidDrop` props on `DockviewReact`.
<NestedDockview />
### Example
hello
<DockviewNative2 />

View File

@ -0,0 +1,120 @@
---
description: Gridview Documentation
---
import { SimpleGridview } from '@site/src/components/simpleGridview';
import { EventsGridview } from '@site/src/components/gridview/events';
import Link from '@docusaurus/Link';
# Gridview
## Introduction
<div
style={{
height: '300px',
backgroundColor: 'rgb(30,30,30)',
color: 'white',
margin: '20px 0px',
}}
>
<SimpleGridview />
</div>
## GridviewReact Component
```tsx
import { ReactGridview } from 'dockview';
```
| Property | Type | Optional | Default | Description |
| ------------------- | ------------------------------------ | -------- | ---------------------- | ------------------------------------------------------------------------ |
| onReady | (event: SplitviewReadyEvent) => void | No | | |
| components | object | No | | |
| orientation | Orientation | Yes | Orientation.HORIZONTAL | |
| proportionalLayout | boolean | Yes | true | See <Link to="../basics/#proportional-layout">Proportional layout</Link> |
| hideBorders | boolean | Yes | false | |
| className | string | Yes | '' | |
| disableAutoResizing | boolean | Yes | false | See <Link to="../basics/#auto-resizing">Auto Resizing</Link> |
## Gridview API
```tsx
const MyComponent = (props: IGridviewPanelProps<{ title: string }>) => {
// props.containerApi...
return <div>{`My first panel has the title: ${props.params.title}`}</div>;
};
```
```tsx
const onReady = (event: GridviewReadyEvent) => {
// event.api...
};
```
| Property | Type | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| height | `number` | Component pixel height |
| width | `number` | Component pixel width |
| minimumHeight | `number` | |
| maximumHeight | `number` | |
| maximumWidth | `number` | |
| maximumWidth | `number` | |
| length | `number` | Number of panels |
| panels | `ISplitviewPanel[]` | all panels |
| orientation | `Orientation` | |
| | | |
| onDidLayoutChange | `Event<void>` | Fires on layout change |
| onDidLayoutFromJSON | `Event<void>` | Fires of layout change caused by a fromJSON deserialization call |
| onDidAddPanel | `Event<IGridviewPanel>` | Fires when a view is added |
| onDidRemovePanel | `Event<IGridviewPanel>` | Fires when a view is removed |
| onDidActivePanelChange | `Event<IGridviewPanel \| undefined>` | Fires when the active group changes |
| | | |
| addPanel | `addPanel(options: AddComponentOptions): IGridviewPanel` | |
| removePanel | `(panel: IGridviewPanel, sizing?: Sizing): void` | |
| movePanel | `(panel: IGridviewPanel, options: {direction: Direction, refernece:string, size?: number}): void` | |
| getPanel | `(id: string) \| IGridviewPanel \| undefined` | |
| | | |
| updateOptions | `(options:SplitviewComponentUpdateOptions): void` | |
| focus | `(): void` | Focus the active panel, if exists |
| layout | `(width: number, height:number): void` | <Link to="../basics/#auto-resizing">Auto Resizing</Link> |
| fromJSON | `(data: SerializedGridview): void` | <Link to="../basics/#serialization">Serialization</Link> |
| toJSON | `(): SerializedGridview` | <Link to="../basics/#serialization">Serialization</Link> |
| clear | `(): void` | Clears the current layout |
## Gridview Panel API
```tsx
const MyComponent = (props: IGridviewPanelProps<{ title: string }>) => {
// props.api...
return <div>{`My first panel has the title: ${props.params.title}`}</div>;
};
```
| Property | Type | Description |
| ---------------------- | ----------------------------------------------------------- | ---------------- |
| id | `string` | Panel id |
| isFocused | `boolean` | Is panel focsed |
| isActive | `boolean` | Is panel active |
| isVisible | `boolean` | Is panel visible |
| width | `number` | Panel width |
| height | `number` | Panel height |
| | | |
| onDidDimensionsChange | `Event<PanelDimensionChangeEvent>` | |
| onDidFocusChange | `Event<FocusEvent>` | |
| onDidVisibilityChange | `Event<VisibilityEvent>` | |
| onDidActiveChange | `Event<ActiveEvent>` | |
| onDidConstraintsChange | `onDidConstraintsChange: Event<PanelConstraintChangeEvent>` | |
| | | |
| setVisible | `(isVisible: boolean): void` | |
| setActive | `(): void` | |
| setConstraints | `(value: PanelConstraintChangeEvent2): void;` | |
| setSize | `(event: SizeEvent): void` | |
## Events
`GridviewReact` exposes a number of events that the developer can listen to and below is a simple example with a log panel showing those events that occur.
<EventsGridview />

View File

@ -0,0 +1,285 @@
---
description: Paneview Documentation
---
import { SimplePaneview } from '@site/src/components/simplePaneview';
import { CustomHeaderPaneview } from '@site/src/components/paneview/customHeader';
import { DragAndDropPaneview } from '@site/src/components/paneview/dragAndDrop';
import { SideBySidePaneview } from '@site/src/components/paneview/sideBySide';
import Link from '@docusaurus/Link';
# Paneview
A paneview is a collapsed collection of vertically stacked panels and panel headers.
The panel header will always remain visible however the panel will only be visible when the panel is expanded.
:::info
Paneview panels can be re-ordered by dragging and dropping the panel headers.
:::
---
# Introduction
<div
style={{
height: '400px',
backgroundColor: 'rgb(30,30,30)',
color: 'white',
margin: '20px 0px',
}}
>
<SimplePaneview />
</div>
```tsx title="Simple Paneview example"
import {
IPaneviewPanelProps,
PaneviewReact,
PaneviewReadyEvent,
} from 'dockview';
const components = {
default: (props: IPaneviewPanelProps<{ title: string }>) => {
return (
<div
style={{
padding: '10px',
height: '100%',
backgroundColor: 'rgb(60,60,60)',
}}
>
{props.params.title}
</div>
);
},
};
SimplePaneview = () => {
const onReady = (event: PaneviewReadyEvent) => {
event.api.addPanel({
id: 'panel_1',
component: 'default',
params: {
title: 'Panel 1',
},
title: 'Panel 1',
});
event.api.addPanel({
id: 'panel_2',
component: 'default',
params: {
title: 'Panel 2',
},
title: 'Panel 2',
});
event.api.addPanel({
id: 'panel_3',
component: 'default',
params: {
title: 'Panel 3',
},
title: 'Panel 3',
});
};
return (
<PaneviewReact
components={components}
headerComponents={headerComponents}
onReady={onReady}
className="dockview-theme-abyss"
/>
);
};
```
## PaneviewReact Component
You can create a Paneview through the use of the `ReactPaneview` component.
```tsx
import { ReactPaneview } from 'dockview';
```
| Property | Type | Optional | Default | Description |
| ------------------- | ------------------------------------ | -------- | ------- | -------------------------------------------------------- |
| onReady | (event: SplitviewReadyEvent) => void | No | | |
| components | object | No | | |
| headerComponents | object | Yes | | |
| className | string | Yes | '' | |
| disableAutoResizing | boolean | Yes | false | <Link to="../basics/#auto-resizing">Auto Resizing</Link> |
| disableDnd | boolean | Yes | false | |
| onDidDrop | Event | Yes | | |
## Paneview API
The Paneview API is exposed both at the `onReady` event and on each panel through `props.containerApi`.
Through this API you can control general features of the component and access all added panels.
```tsx title="Paneview API via Panel component"
const MyComponent = (props: IGridviewPanelProps<{ title: string }>) => {
// props.containerApi...
return <div>{`My first panel has the title: ${props.params.title}`}</div>;
};
```
```tsx title="Paneview API via the onReady callback"
const onReady = (event: GridviewReadyEvent) => {
// event.api...
};
```
| Property | Type | Description |
| ------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| height | `number` | Component pixel height |
| width | `number` | Component pixel width |
| minimumSize | `number` | The sum of the `minimumSize` property for each panel |
| maximumSize | `number` | The sum of the `maximumSize` property for each panel |
| length | `number` | Number of panels |
| panels | `IPaneviewPanel[]` | All panels |
| | | |
| onDidLayoutChange | `Event<void>` | Fires on layout change |
| onDidLayoutFromJSON | `Event<void>` | Fires of layout change caused by a fromJSON deserialization call |
| onDidAddView | `Event<IPaneviewPanel>` | Fires when a view is added |
| onDidRemoveView | `Event<IPaneviewPanel>` | Fires when a view is removed |
| onDidDrop | `Event<PaneviewDropEvent` | Fires on an external drop event (See <Link to="./paneview/#drag-and-drop">Drag and Drop</Link>) |
| | | |
| addPanel | `addPanel(options: AddPaneviewComponentOptions): IPaneviewPanel` | |
| removePanel | `(panel: IPaneviewPanel): void` | |
| movePanel | `(from: number, to: number): void` | |
| getPanel | `(id:string): IPaneviewPanel \| undefined` | |
| | | |
| focus | `(): void` | Focus the active panel, if exists |
| layout | `(width: number, height:number): void` | See <Link to="../basics/#auto-resizing">Auto Resizing</Link> |
| fromJSON | `(data: SerializedPaneview): void` | <Link to="../basics/#serialization">Serialization</Link> |
| toJSON | `(): SerializedPaneview` | <Link to="../basics/#serialization">Serialization</Link> |
| clear | `(): void` | Clears the current layout |
## Paneview Panel API
```tsx
const MyComponent = (props: IGridviewPanelProps<{ title: string }>) => {
// props.api...
return <div>{`My first panel has the title: ${props.params.title}`}</div>;
};
```
| Property | Type | Description |
| ---------------------- | ----------------------------------------------------------- | ---------------- |
| id | `string` | Panel id |
| isFocused | `boolean` | Is panel focsed |
| isActive | `boolean` | Is panel active |
| isVisible | `boolean` | Is panel visible |
| width | `number` | Panel width |
| height | `number` | Panel height |
| | |
| onDidDimensionsChange | `Event<PanelDimensionChangeEvent>` | |
| onDidFocusChange | `Event<FocusEvent>` | |
| onDidVisibilityChange | `Event<VisibilityEvent>` | |
| onDidActiveChange | `Event<ActiveEvent>` | |
| onDidConstraintsChange | `onDidConstraintsChange: Event<PanelConstraintChangeEvent>` | |
| | |
| setVisible | `(isVisible: boolean): void` | |
| setActive | `(): void` | |
| setConstraints | `(value: PanelConstraintChangeEvent2): void;` | |
| setSize | `(event: SizeEvent): void` | |
## Advanced Features
### Custom Header
You can provide a custom component to render an alternative header.
<div
style={{
height: '400px',
backgroundColor: 'rgb(30,30,30)',
color: 'white',
margin: '20px 0px',
}}
>
<CustomHeaderPaneview />
</div>
You can provide a `headerComponent` option when creating a panel to tell the library to use a custom header component.
```tsx
const onReady = (event: PaneviewReadyEvent) => {
event.api.addPanel({
id: 'panel_1',
component: 'default',
headerComponent: 'myHeaderComponent',
params: {
valueA: 'A',
},
title: 'Panel 1',
});
};
```
This header must be defined in the collection of components provided to the `headerComponents` props for `ReactPaneivew`
```tsx
import { IPaneviewPanelProps } from 'dockview';
const MyHeaderComponent = (props: IPaneviewPanelProps<{ title: string }>) => {
const [expanded, setExpanded] = React.useState<boolean>(
props.api.isExpanded
);
React.useEffect(() => {
const disposable = props.api.onDidExpansionChange((event) => {
setExpanded(event.isExpanded);
});
return () => {
disposable.dispose();
};
}, []);
const onClick = () => {
props.api.setExpanded(!expanded);
};
return (
<div
style={{
padding: '10px',
height: '100%',
backgroundColor: 'rgb(60,60,60)',
}}
>
<a
onClick={onClick}
className={expanded ? 'expanded' : 'collapsed'}
/>
<span>{props.params.title}</span>
</div>
);
};
const headerComponents = { myHeaderComponent: MyHeaderComponent };
```
### Drag And Drop
If you provide the `PaneviewReact` component with the prop `onDidDrop` you will be able to interact with custom drop events.
<DragAndDropPaneview />
### Interactions
You can safely create multiple paneview instances within one page. They will not interact with each other by default.
If you wish to interact with the drop event from one paneview instance in another paneview instance you can implement the `showDndOverlay` and `onDidDrop` props on `PaneviewReact`.
As an example see how dragging a header from one control to another will only trigger an interactable event for the developer if the checkbox is enabled.
<SideBySidePaneview />

View File

@ -0,0 +1,246 @@
---
description: Splitview Documentation
---
import { SimpleSplitview } from '@site/src/components/simpleSplitview';
import { SplitviewExample1 } from '@site/src/components/splitview/active';
import Link from '@docusaurus/Link';
# Splitview
## Introduction
A Splitview is a collection of resizable horizontally or vertically stacked panels.
<div
style={{
height: '100px',
backgroundColor: 'rgb(30,30,30)',
color: 'white',
margin: '20px 0px',
}}
>
<SimpleSplitview />
</div>
```tsx title="Simple Splitview example"
import {
ISplitviewPanelProps,
Orientation,
SplitviewReact,
SplitviewReadyEvent,
} from 'dockview';
const components = {
default: (props: ISplitviewPanelProps<{ title: string }>) => {
return <div style={{ padding: '20px' }}>{props.params.title}</div>;
},
};
export const SimpleSplitview = () => {
const onReady = (event: SplitviewReadyEvent) => {
event.api.addPanel({
id: 'panel_1',
component: 'default',
params: {
title: 'Panel 1',
},
});
event.api.addPanel({
id: 'panel_2',
component: 'default',
params: {
title: 'Panel 2',
},
});
event.api.addPanel({
id: 'panel_3',
component: 'default',
params: {
title: 'Panel 3',
},
});
};
return (
<SplitviewReact
components={components}
onReady={onReady}
orientation={Orientation.HORIZONTAL}
className="dockview-theme-abyss"
/>
);
};
```
## SplitviewReact Component
You can create a Splitview through the use of the `ReactSplitview` component.
```tsx
import { ReactSplitview } from 'dockview';
```
Using the `onReady` prop you can access to the component `api` and add panels either through deserialization or the individual addition of panels.
| Property | Type | Optional | Default | Description |
| ------------------- | -------------------------------------- | -------- | ------------------------ | ------------------------------------------------------------------------ |
| onReady | `(event: SplitviewReadyEvent) => void` | No | | Function |
| components | `Record<string, ISplitviewPanelProps>` | No | | Panel renderers |
| orientation | `Orientation` | Yes | `Orientation.HORIZONTAL` | Orientation of the Splitview |
| proportionalLayout | `boolean` | Yes | `true` | See <Link to="../basics/#proportional-layout">Proportional layout</Link> |
| hideBorders | `boolean` | Yes | `false` | Hide the borders between panels |
| className | `string` | Yes | `''` | Attaches a classname |
| disableAutoResizing | `boolean` | Yes | `false` | See <Link to="../basics/#auto-resizing">Auto Resizing</Link> |
## Splitview API
The Splitview API is exposed both at the `onReady` event and on each panel through `props.containerApi`.
Through this API you can control general features of the component and access all added panels.
```tsx title="Splitview API via Panel component"
const MyComponent = (props: ISplitviewPanelProps<{ title: string }>) => {
// props.containerApi...
return <div>{`My first panel has the title: ${props.params.title}`}</div>;
};
```
```tsx title="Splitview API via the onReady callback"
const onReady = (event: SplitviewReadyEvent) => {
// event.api...
};
```
| Property | Type | Description |
| ------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------- |
| height | `number` | Component pixel height |
| width | `number` | Component pixel width |
| minimumSize | `number` | The sum of the `minimumSize` property for each panel |
| maximumSize | `number` | The sum of the `maximumSize` property for each panel |
| length | `number` | Number of panels |
| panels | `ISplitviewPanel[]` | All panels |
| | | |
| onDidLayoutChange | `Event<void>` | Fires on layout change |
| onDidLayoutFromJSON | `Event<void>` | Fires of layout change caused by a fromJSON deserialization call |
| onDidAddView | `Event<IView>` | Fires when a view is added |
| onDidRemoveView | `Event<IView>` | Fires when a view is removed |
| | | |
| addPanel | `addPanel(options: AddSplitviewComponentOptions): ISplitviewPanel` | |
| removePanel | `(panel: ISplitviewPanel, sizing?: Sizing): void` | |
| getPanel | `(id:string): ISplitviewPanel \| undefined` | |
| movePanel | `(from: number, to: number): void` | |
| | |
| updateOptions | `(options: SplitviewComponentUpdateOptions): void` | |
| focus | `(): void` | Focus the active panel, if exists |
| layout | `(width: number, height:number): void` | See <Link to="../basics/#auto-resizing">Auto Resizing</Link> |
| fromJSON | `(data: SerializedSplitview): void` | <Link to="../basics/#serialization">Serialization</Link> |
| toJSON | `(): SerializedSplitview` | <Link to="../basics/#serialization">Serialization</Link> |
| clear | `(): void` | Clears the current layout |
## Splitview Panel API
The Splitview panel API is exposed on each panel containing actions and variables specific to that panel.
```tsx title="Splitview panel API via Panel component"
const MyComponent = (props: ISplitviewPanelProps<{ title: string }>) => {
// props.api...
return <div>{`My first panel has the title: ${props.params.title}`}</div>;
};
```
| Property | Type | Description |
| ---------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| id | `string` | Panel id |
| isFocused | `boolean` | Is panel focsed |
| isActive | `boolean` | Is panel active |
| isVisible | `boolean` | Is panel visible |
| width | `number` | Panel width |
| height | `number` | Panel height |
| | | |
| onDidDimensionsChange | `Event<PanelDimensionChangeEvent>` | Fires when panel dimensions change |
| onDidFocusChange | `Event<FocusEvent>` | Fire when panel is focused and blurred |
| onDidVisibilityChange | `Event<VisibilityEvent>` | Fires when the panels visiblity property is changed (see <Link to="./splitview/#visibility">Panel Visibility</Link>) |
| onDidActiveChange | `Event<ActiveEvent>` | Fires when the panels active property is changed (see <Link to="./splitview/#active">Active Panel</Link>) |
| onDidConstraintsChange | `onDidConstraintsChange: Event<PanelConstraintChangeEvent>` | Fires when the panels size contrainsts change (see <Link to="./splitview/#contraints">Panel Constraints</Link>) |
| | | |
| setVisible | `(isVisible: boolean): void` | |
| setActive | `(): void` | |
| | | |
| setConstraints | `(value: PanelConstraintChangeEvent2): void;` | |
| setSize | `(event: PanelSizeEvent): void` | |
## Advanced Features
Listed below are some functionalities avalaible through both the panel and component APIs. The live demo shows examples of these in real-time.
<div
style={{
height: '200px',
margin: '20px 0px',
}}
>
<SplitviewExample1 />
</div>
### Visibility
A panels visibility can be controlled and monitored through the following code.
A panel with visibility set to `false` will remain as a part of the components list of panels but will not be rendered.
```tsx
const disposable = props.api.onDidVisibilityChange(({ isVisible }) => {
//
});
```
```tsx
api.setVisible(true);
```
### Active
Only one panel in the `splitview` can be the active panel at any one time.
Setting a panel as active will set all the others as inactive.
A focused panel is always the active panel but an active panel is not always focused.
```tsx
const disposable = props.api.onDidActiveChange(({ isActive }) => {
//
});
```
```tsx
api.setActive();
```
### Contraints
When adding a panel you can specify pixel size contraints
```tsx
event.api.addPanel({
id: 'panel_3',
component: 'default',
minimumSize: 100,
maximumSize: 1000,
});
```
These contraints can be updated throughout the lifecycle of the `splitview` using the panel API
```tsx
props.api.onDidConstraintsChange(({ maximumSize, minimumSize }) => {
//
});
```
```tsx
api.setConstraints({
maximumSize: 200,
minimumSize: 400,
});
```

View File

@ -0,0 +1,25 @@
---
sidebar_position: 4
description: Dockview examples
---
import { SimpleSplitview } from '@site/src/components/simpleSplitview';
import { SimpleGridview } from '@site/src/components/simpleGridview';
import { SimplePaneview } from '@site/src/components/simplePaneview';
import { SimpleDockview } from '@site/src/components/simpleDockview';
# Examples
<iframe
src="https://codesandbox.io/embed/dockview-template-mdc9f7?fontsize=14&hidenavigation=1&theme=dark"
style={{
width: '100%',
height: '500px',
border: 0,
borderRaduis: '4px',
overflow: 'hidden',
}}
title="dockview-template"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>

View File

@ -0,0 +1,149 @@
---
sidebar_position: 0
description: A zero dependency layout manager built for React
---
import { SimpleSplitview } from '@site/src/components/simpleSplitview';
import { SimpleGridview } from '@site/src/components/simpleGridview';
import { SimplePaneview } from '@site/src/components/simplePaneview';
import { SimpleDockview } from '@site/src/components/simpleDockview';
# Introduction
**dockview** is a zero dependency layout manager that supports tab, grids and splitviews.
## Features
- Themable and customizable
- Support for the serialization and deserialization of layouts
- Drag and drop support
## Quick start
`dockview` has a peer dependency on `react >= 16.8.0` and `react-dom >= 16.8.0`. To install `dockview` you can run:
```shell
npm install dockview
```
You must also import the dockview stylesheet found under [`dockview/dict/styles/dockview.css`](https://unpkg.com/browse/dockview@latest/dist/styles/dockview.css),
depending on your solution this might be:
```css
@import './node_modules/dockview/dist/styles/dockview.css';
```
A dark and light theme are provided, one of these classes (or a custom theme) must be attached at any point above your components in the HTML tree. To cover the entire web page you might attach the class to the `body` component:
```html
<body classname="dockview-theme-abyss">
...
</body>
<body classname="dockview-theme-light">
...
</body>
```
There are 4 components you may want to use:
Splitview
<div
style={{
height: '100px',
backgroundColor: 'rgb(30,30,30)',
color: 'white',
margin: '20px 0px',
}}
>
<SimpleSplitview />
</div>
<div
style={{
height: '300px',
backgroundColor: 'rgb(30,30,30)',
color: 'white',
margin: '20px 0px',
}}
>
<SimpleGridview />
</div>
<div
style={{
height: '300px',
backgroundColor: 'rgb(30,30,30)',
color: 'white',
margin: '20px 0px',
}}
>
<SimplePaneview />
</div>
<div
style={{
height: '300px',
backgroundColor: 'rgb(30,30,30)',
color: 'white',
margin: '20px 0px',
}}
>
<SimpleDockview />
</div>
```tsx
import {
DockviewReact,
DockviewReadyEvent,
PanelCollection,
IDockviewPanelProps,
IDockviewPanelHeaderProps,
} from 'dockview';
const components: PanelCollection<IDockviewPanelProps> = {
default: (props: IDockviewPanelProps<{ someProps: string }>) => {
return <div>{props.params.someProps}</div>;
},
};
const headers: PanelCollection<IDockviewPanelHeaderProps> = {
customTab: (props: IDockviewPanelHeaderProps) => {
return (
<div>
<span>{props.api.title}</span>
<span onClick={() => props.api.close()}>{'[x]'}</span>
</div>
);
},
};
const Component = () => {
const onReady = (event: DockviewReadyEvent) => {
event.api.addPanel({
id: 'panel1',
component: 'default',
tabComponent: 'customTab', // optional custom header
params: {
someProps: 'Hello',
},
});
event.api.addPanel({
id: 'panel2',
component: 'default',
params: {
someProps: 'World',
},
position: { referencePanel: 'panel1', direction: 'below' },
});
};
return (
<DockviewReact
components={components}
tabComponents={headers} // optional headers renderer
onReady={onReady}
/>
);
};
```

View File

@ -0,0 +1,89 @@
---
sidebar_position: 3
description: Theming Dockview Components
---
import { CustomCSSDockview } from '@site/src/components/dockview/customCss';
# Theme
## Introduction
`dockview` requires some css to work correctly.
The css is exported as one file under [`dockview/dict/styles/dockview.css`](https://unpkg.com/browse/dockview@latest/dist/styles/dockview.css)
and depending can be imported
```css
@import './node_modules/dockview/dist/styles/dockview.css';
```
## Provided themes
The following are provided as classes that you can attached to your components for themeing
- `.dockview-theme-light`
- `.dockview-theme-dark`
- `.dockview-theme-abyss`
## Customizing Theme
`dockview` supports theming through the use of css properties.
You can view the built-in themes at [`dockview/src/theme.scss`](https://github.com/mathuo/dockview/blob/master/packages/dockview/src/theme.scss)
and are free to build your own themes based on these css properties.
| CSS Property | Description |
| ---------------------------------------------------- | ----------- |
| --dv-paneview-active-outline-color | |
| --dv-tabs-and-actions-container-font-size | |
| --dv-tabs-and-actions-container-height | |
| --dv-tab-close-icon | |
| --dv-drag-over-background-color | |
| --dv-drag-over-border-color | |
| --dv-tabs-container-scrollbar-color | |
| | |
| --dv-group-view-background-color | |
| | |
| --dv-tabs-and-actions-container-background-color | |
| | |
| --dv-activegroup-visiblepanel-tab-background-color | |
| --dv-activegroup-hiddenpanel-tab-background-color | |
| --dv-inactivegroup-visiblepanel-tab-background-color | |
| --dv-inactivegroup-hiddenpanel-tab-background-color | |
| --dv-tab-divider-color | |
| | |
| --dv-activegroup-visiblepanel-tab-color | |
| --dv-activegroup-hiddenpanel-tab-color | |
| --dv-inactivegroup-visiblepanel-tab-color | |
| --dv-inactivegroup-hiddenpanel-tab-color | |
| | |
| --dv-separator-border | |
| --dv-paneview-header-border-color | |
You can further customise the theme through adjusting class properties but this is up you.
As an example if you wanted to add a bottom border to the tab container for an active group in the `DockviewReact` component you could write:
```css
.groupview {
&.active-group {
> .tabs-and-actions-container {
border-bottom: 2px solid var(--dv-activegroup-visiblepanel-tab-background-color);
}
}
&.inactive-group {
> .tabs-and-actions-container {
border-bottom: 2px solid var(--dv-inactivegroup-visiblepanel-tab-background-color);
}
}
}
```
<div
style={{
height: '300px',
backgroundColor: 'rgb(30,30,30)',
color: 'white',
margin: '20px 0px',
}}
>
<CustomCSSDockview />
</div>

View File

@ -0,0 +1,8 @@
{
"tutorialSidebar": [
{
"type": "autogenerated",
"dirName": "."
}
]
}

View File

@ -1,4 +1,5 @@
[
"1.6.0",
"1.5.2",
"1.5.1"
]

9987
yarn.lock

File diff suppressed because it is too large Load Diff