Merge branch 'master' of https://github.com/mathuo/dockview into 281-type-hints-for-panel-parameters

This commit is contained in:
mathuo 2023-07-25 22:28:05 +01:00
commit 141b2beaf3
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
148 changed files with 10923 additions and 3819 deletions

View File

@ -30,7 +30,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@ -9,16 +9,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly.
with:
persist-credentials: false
uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '16.x'
- uses: actions/cache@v2
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
@ -26,7 +23,6 @@ jobs:
${{ runner.os }}-node-
- run: yarn install
- run: lerna bootstrap
- run: npm run build
working-directory: packages/dockview-core
- run: npm run build

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
# might be required for sonar to work correctly
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
@ -16,7 +16,7 @@ jobs:
with:
node-version: '16.x'
- uses: actions/cache@v2
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}

View File

@ -27,6 +27,7 @@ Please see the website: https://dockview.dev
- Themable and customizable
- Serialization / deserialization support
- Tabular docking and Drag and Drop support
- Floating groups, customized header bars and tab
- Documentation and examples
Want to inspect the latest deployment? Go to https://unpkg.com/browse/dockview@latest/

View File

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

View File

@ -1,7 +1,7 @@
<div align="center">
<h1>dockview</h1>
<p>Zero dependency layout manager supporting tabs, grids and splitviews written in TypeScript</p>
<p>Zero dependency layout manager supporting tabs, groups, grids and splitviews written in TypeScript</p>
</div>
@ -25,6 +25,7 @@ Please see the website: https://dockview.dev
- Themable and customizable
- Serialization / deserialization support
- Tabular docking and Drag and Drop support
- Floating groups, customized header bars and tab
- Documentation and examples
Want to inspect the latest deployment? Go to https://unpkg.com/browse/dockview-core@latest/

View File

@ -1,6 +1,6 @@
{
"name": "dockview-core",
"version": "1.7.5",
"version": "1.8.2",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews with ReactJS support",
"main": "./dist/cjs/index.js",
"types": "./dist/cjs/index.d.ts",
@ -14,12 +14,12 @@
},
"homepage": "https://github.com/mathuo/dockview",
"scripts": {
"build:ci": "npm run build:cjs && npm run build:esm && npm run build:css",
"build:package": "npm run build:cjs && npm run build:esm && npm run build:css",
"build:cjs": "cross-env ../../node_modules/.bin/tsc --project ./tsconfig.json --extendedDiagnostics",
"build:css": "gulp sass",
"build:esm": "cross-env ../../node_modules/.bin/tsc --project ./tsconfig.esm.json --extendedDiagnostics",
"build:modulefiles": "rollup -c",
"build": "npm run build:ci && npm run build:modulefiles",
"build:bundles": "rollup -c",
"build": "npm run build:package && npm run build:bundles",
"clean": "rimraf dist/ .build/ .rollup.cache/",
"prepublishOnly": "npm run rebuild && npm run test",
"docs": "typedoc",

View File

@ -46,6 +46,7 @@ function createBundle(format, options) {
const output = {
file,
format,
sourcemap: true,
globals: {},
banner: [
`/**`,
@ -57,13 +58,9 @@ function createBundle(format, options) {
].join('\n'),
};
const plugins = [
typescript({
tsconfig: 'tsconfig.esm.json',
compilerOptions: {
declaration: false,
},
}),
];

View File

@ -5,6 +5,12 @@ import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
describe('groupPanelApi', () => {
test('title', () => {
const accessor: Partial<DockviewComponent> = {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
const panelMock = jest.fn<DockviewPanel, []>(() => {
return {
update: jest.fn(),
@ -18,7 +24,11 @@ describe('groupPanelApi', () => {
const panel = new panelMock();
const group = new groupMock();
const cut = new DockviewPanelApiImpl(panel, group);
const cut = new DockviewPanelApiImpl(
panel,
group,
<DockviewComponent>accessor
);
cut.setTitle('test_title');
expect(panel.setTitle).toBeCalledTimes(1);
@ -44,7 +54,8 @@ describe('groupPanelApi', () => {
const cut = new DockviewPanelApiImpl(
<IDockviewPanel>groupPanel,
<DockviewGroupPanel>groupViewPanel
<DockviewGroupPanel>groupViewPanel,
<DockviewComponent>accessor
);
cut.updateParameters({ keyA: 'valueA' });
@ -73,7 +84,8 @@ describe('groupPanelApi', () => {
const cut = new DockviewPanelApiImpl(
<IDockviewPanel>groupPanel,
<DockviewGroupPanel>groupViewPanel
<DockviewGroupPanel>groupViewPanel,
<DockviewComponent>accessor
);
let events = 0;

View File

@ -3,6 +3,7 @@ import {
last,
pushToEnd,
pushToStart,
remove,
sequenceEquals,
tail,
} from '../array';
@ -47,4 +48,22 @@ describe('array', () => {
expect(sequenceEquals([1, 2, 3, 4], [1, 2, 3])).toBeFalsy();
expect(sequenceEquals([1, 2, 3, 4], [1, 2, 3, 4, 5])).toBeFalsy();
});
test('remove', () => {
const arr1 = [1, 2, 3, 4];
remove(arr1, 2);
expect(arr1).toEqual([1, 3, 4]);
const arr2 = [1, 2, 2, 3, 4];
remove(arr2, 2);
expect(arr2).toEqual([1, 2, 3, 4]);
const arr3 = [1];
remove(arr3, 2);
expect(arr3).toEqual([1]);
remove(arr3, 1);
expect(arr3).toEqual([]);
remove(arr3, 1);
expect(arr3).toEqual([]);
});
});

View File

@ -118,4 +118,62 @@ describe('abstractDragHandler', () => {
expect(webview.style.pointerEvents).toBe('auto');
expect(span.style.pointerEvents).toBeFalsy();
});
test('that .preventDefault() is called for cancelled events', () => {
const element = document.createElement('div');
const handler = new (class TestClass extends DragHandler {
constructor(el: HTMLElement) {
super(el);
}
protected isCancelled(_event: DragEvent): boolean {
return true;
}
getData(): IDisposable {
return {
dispose: () => {
// /
},
};
}
})(element);
const event = new Event('dragstart');
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toBeCalledTimes(1);
handler.dispose();
});
test('that .preventDefault() is not called for non-cancelled events', () => {
const element = document.createElement('div');
const handler = new (class TestClass extends DragHandler {
constructor(el: HTMLElement) {
super(el);
}
protected isCancelled(_event: DragEvent): boolean {
return false;
}
getData(): IDisposable {
return {
dispose: () => {
// /
},
};
}
})(element);
const event = new Event('dragstart');
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toBeCalledTimes(0);
handler.dispose();
});
});

View File

@ -34,6 +34,48 @@ describe('droptarget', () => {
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 200);
});
test('that dragover events are marked', () => {
droptarget = new Droptarget(element, {
canDisplayOverlay: () => true,
acceptedTargetZones: ['center'],
});
fireEvent.dragEnter(element);
const event = new Event('dragover');
fireEvent(element, event);
expect(
(event as any)['__dockview_droptarget_event_is_used__']
).toBeTruthy();
});
test('that the drop target is removed when receiving a marked dragover event', () => {
let position: Position | undefined = undefined;
droptarget = new Droptarget(element, {
canDisplayOverlay: () => true,
acceptedTargetZones: ['center'],
});
droptarget.onDrop((event) => {
position = event.position;
});
fireEvent.dragEnter(element);
fireEvent.dragOver(element);
const target = element.querySelector(
'.drop-target-dropzone'
) as HTMLElement;
fireEvent.drop(target);
expect(position).toBe('center');
const event = new Event('dragover');
(event as any)['__dockview_droptarget_event_is_used__'] = true;
fireEvent(element, event);
expect(element.querySelector('.drop-target-dropzone')).toBeNull();
});
test('directionToPosition', () => {
expect(directionToPosition('above')).toBe('top');
expect(directionToPosition('below')).toBe('bottom');

View File

@ -0,0 +1,101 @@
import { fireEvent } from '@testing-library/dom';
import { GroupDragHandler } from '../../dnd/groupDragHandler';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer';
describe('groupDragHandler', () => {
test('that the dnd transfer object is setup and torndown', () => {
const element = document.createElement('div');
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = {
id: 'test_group_id',
api: { isFloating: false } as any,
};
return partial as DockviewGroupPanel;
});
const group = new groupMock();
const cut = new GroupDragHandler(element, 'test_accessor_id', group);
fireEvent.dragStart(element, new Event('dragstart'));
expect(
LocalSelectionTransfer.getInstance<PanelTransfer>().hasData(
PanelTransfer.prototype
)
).toBeTruthy();
const transferObject =
LocalSelectionTransfer.getInstance<PanelTransfer>().getData(
PanelTransfer.prototype
)![0];
expect(transferObject).toBeTruthy();
expect(transferObject.viewId).toBe('test_accessor_id');
expect(transferObject.groupId).toBe('test_group_id');
expect(transferObject.panelId).toBeNull();
fireEvent.dragStart(element, new Event('dragend'));
expect(
LocalSelectionTransfer.getInstance<PanelTransfer>().hasData(
PanelTransfer.prototype
)
).toBeFalsy();
cut.dispose();
});
test('that the event is cancelled when isFloating and shiftKey=true', () => {
const element = document.createElement('div');
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = {
api: { isFloating: true } as any,
};
return partial as DockviewGroupPanel;
});
const group = new groupMock();
const cut = new GroupDragHandler(element, 'accessor_id', group);
const event = new KeyboardEvent('dragstart', { shiftKey: false });
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toBeCalledTimes(1);
const event2 = new KeyboardEvent('dragstart', { shiftKey: true });
const spy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(element, event);
expect(spy2).toBeCalledTimes(0);
cut.dispose();
});
test('that the event is never cancelled when the group is not floating', () => {
const element = document.createElement('div');
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = {
api: { isFloating: false } as any,
};
return partial as DockviewGroupPanel;
});
const group = new groupMock();
const cut = new GroupDragHandler(element, 'accessor_id', group);
const event = new KeyboardEvent('dragstart', { shiftKey: false });
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toBeCalledTimes(0);
const event2 = new KeyboardEvent('dragstart', { shiftKey: true });
const spy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(element, event);
expect(spy2).toBeCalledTimes(0);
cut.dispose();
});
});

View File

@ -0,0 +1,156 @@
import { Overlay } from '../../dnd/overlay';
describe('overlay', () => {
test('toJSON', () => {
const container = document.createElement('div');
const content = document.createElement('div');
document.body.appendChild(container);
container.appendChild(content);
const cut = new Overlay({
height: 200,
width: 100,
left: 10,
top: 20,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
jest.spyOn(
container.childNodes.item(0) as HTMLElement,
'getBoundingClientRect'
).mockImplementation(() => {
return { left: 80, top: 100, width: 40, height: 50 } as any;
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return { left: 20, top: 30, width: 100, height: 100 } as any;
}
);
expect(cut.toJSON()).toEqual({
top: 70,
left: 60,
width: 40,
height: 50,
});
});
test('that out-of-bounds dimensions are fixed', () => {
const container = document.createElement('div');
const content = document.createElement('div');
document.body.appendChild(container);
container.appendChild(content);
const cut = new Overlay({
height: 200,
width: 100,
left: -1000,
top: -1000,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
jest.spyOn(
container.childNodes.item(0) as HTMLElement,
'getBoundingClientRect'
).mockImplementation(() => {
return { left: 80, top: 100, width: 40, height: 50 } as any;
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return { left: 20, top: 30, width: 100, height: 100 } as any;
}
);
expect(cut.toJSON()).toEqual({
top: 70,
left: 60,
width: 40,
height: 50,
});
});
test('setBounds', () => {
const container = document.createElement('div');
const content = document.createElement('div');
document.body.appendChild(container);
container.appendChild(content);
const cut = new Overlay({
height: 1000,
width: 1000,
left: 0,
top: 0,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
const element: HTMLElement = container.querySelector(
'.dv-resize-container'
)!;
expect(element).toBeTruthy();
jest.spyOn(element, 'getBoundingClientRect').mockImplementation(() => {
return { left: 300, top: 400, width: 1000, height: 1000 } as any;
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return { left: 0, top: 0, width: 1000, height: 1000 } as any;
}
);
cut.setBounds({ height: 100, width: 200, left: 300, top: 400 });
expect(element.style.height).toBe('100px');
expect(element.style.width).toBe('200px');
expect(element.style.left).toBe('300px');
expect(element.style.top).toBe('400px');
});
test('that the resize handles are added', () => {
const container = document.createElement('div');
const content = document.createElement('div');
const cut = new Overlay({
height: 500,
width: 500,
left: 100,
top: 200,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
expect(container.querySelector('.dv-resize-handle-top')).toBeTruthy();
expect(
container.querySelector('.dv-resize-handle-bottom')
).toBeTruthy();
expect(container.querySelector('.dv-resize-handle-left')).toBeTruthy();
expect(container.querySelector('.dv-resize-handle-right')).toBeTruthy();
expect(
container.querySelector('.dv-resize-handle-topleft')
).toBeTruthy();
expect(
container.querySelector('.dv-resize-handle-topright')
).toBeTruthy();
expect(
container.querySelector('.dv-resize-handle-bottomleft')
).toBeTruthy();
expect(
container.querySelector('.dv-resize-handle-bottomright')
).toBeTruthy();
cut.dispose();
});
});

View File

@ -1,14 +1,14 @@
import { fireEvent } from '@testing-library/dom';
import { Emitter, Event } from '../../../events';
import { ContentContainer } from '../../../dockview/components/panel/content';
import { Emitter, Event } from '../../../../events';
import { ContentContainer } from '../../../../dockview/components/panel/content';
import {
GroupPanelContentPartInitParameters,
IContentRenderer,
} from '../../../dockview/types';
import { CompositeDisposable } from '../../../lifecycle';
import { PanelUpdateEvent } from '../../../panel/types';
import { IDockviewPanel } from '../../../dockview/dockviewPanel';
import { IDockviewPanelModel } from '../../../dockview/dockviewPanelModel';
} from '../../../../dockview/types';
import { CompositeDisposable } from '../../../../lifecycle';
import { PanelUpdateEvent } from '../../../../panel/types';
import { IDockviewPanel } from '../../../../dockview/dockviewPanel';
import { IDockviewPanelModel } from '../../../../dockview/dockviewPanelModel';
class TestContentRenderer
extends CompositeDisposable

View File

@ -1,9 +1,9 @@
import { fireEvent } from '@testing-library/dom';
import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer';
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { DockviewGroupPanelModel } from '../../dockview/dockviewGroupPanelModel';
import { Tab } from '../../dockview/components/tab/tab';
import { LocalSelectionTransfer, PanelTransfer } from '../../../dnd/dataTransfer';
import { DockviewComponent } from '../../../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../../../dockview/dockviewGroupPanel';
import { DockviewGroupPanelModel } from '../../../dockview/dockviewGroupPanelModel';
import { Tab } from '../../../dockview/components/tab/tab';
describe('tab', () => {
test('that empty tab has inactive-tab class', () => {

View File

@ -0,0 +1,641 @@
import {
LocalSelectionTransfer,
PanelTransfer,
} from '../../../../dnd/dataTransfer';
import { TabsContainer } from '../../../../dockview/components/titlebar/tabsContainer';
import { DockviewComponent } from '../../../../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../../../../dockview/dockviewGroupPanel';
import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanelModel';
import { fireEvent } from '@testing-library/dom';
import { TestPanel } from '../../dockviewGroupPanelModel.spec';
import { IDockviewPanel } from '../../../../dockview/dockviewPanel';
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 {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
model: groupView,
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
const emptySpace = cut.element
.getElementsByClassName('void-container')
.item(0);
if (!emptySpace) {
fail('element not found');
}
jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation(
() => 100
);
fireEvent.dragEnter(emptySpace);
fireEvent.dragOver(emptySpace);
expect(groupView.canDisplayOverlay).toBeCalled();
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(0);
});
test('that a drag over event from another 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<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
panels: [],
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
const emptySpace = cut.element
.getElementsByClassName('void-container')
.item(0);
if (!emptySpace) {
fail('element not found');
}
jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation(
() => 100
);
LocalSelectionTransfer.getInstance().setData(
[
new PanelTransfer(
'testcomponentid',
'anothergroupid',
'anotherpanelid'
),
],
PanelTransfer.prototype
);
fireEvent.dragEnter(emptySpace);
fireEvent.dragOver(emptySpace);
expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(1);
});
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<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
panels: [],
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element
.getElementsByClassName('void-container')
.item(0);
if (!emptySpace) {
fail('element not found');
}
jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation(
() => 100
);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'anothergroupid', 'panel2')],
PanelTransfer.prototype
);
fireEvent.dragEnter(emptySpace);
fireEvent.dragOver(emptySpace);
expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
).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<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
panels: [],
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element
.getElementsByClassName('void-container')
.item(0);
if (!emptySpace) {
fail('element not found');
}
jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation(
() => 100
);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'anothergroupid', 'panel1')],
PanelTransfer.prototype
);
fireEvent.dragEnter(emptySpace);
fireEvent.dragOver(emptySpace);
expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(1);
});
test('that dropping a tab from another component should not 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<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element
.getElementsByClassName('void-container')
.item(0);
if (!emptySpace) {
fail('element not found');
}
jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation(
() => 100
);
LocalSelectionTransfer.getInstance().setData(
[
new PanelTransfer(
'anothercomponentid',
'anothergroupid',
'panel1'
),
],
PanelTransfer.prototype
);
fireEvent.dragEnter(emptySpace);
fireEvent.dragOver(emptySpace);
expect(groupView.canDisplayOverlay).toBeCalledTimes(1);
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(0);
});
test('left actions', () => {
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{}) as DockviewGroupPanel;
});
const accessor = new accessorMock();
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
let query = cut.element.querySelectorAll(
'.tabs-and-actions-container > .left-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.length).toBe(0);
// add left action
const left = document.createElement('div');
left.className = 'test-left-actions-element';
cut.setLeftActionsElement(left);
query = cut.element.querySelectorAll(
'.tabs-and-actions-container > .left-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe(
'test-left-actions-element'
);
expect(query[0].children.length).toBe(1);
// add left action
const left2 = document.createElement('div');
left2.className = 'test-left-actions-element-2';
cut.setLeftActionsElement(left2);
query = cut.element.querySelectorAll(
'.tabs-and-actions-container > .left-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe(
'test-left-actions-element-2'
);
expect(query[0].children.length).toBe(1);
// remove left action
cut.setLeftActionsElement(undefined);
query = cut.element.querySelectorAll(
'.tabs-and-actions-container > .left-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.length).toBe(0);
});
test('right actions', () => {
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{}) as DockviewGroupPanel;
});
const accessor = new accessorMock();
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
let query = cut.element.querySelectorAll(
'.tabs-and-actions-container > .right-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.length).toBe(0);
// add right action
const right = document.createElement('div');
right.className = 'test-right-actions-element';
cut.setRightActionsElement(right);
query = cut.element.querySelectorAll(
'.tabs-and-actions-container > .right-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe(
'test-right-actions-element'
);
expect(query[0].children.length).toBe(1);
// add right action
const right2 = document.createElement('div');
right2.className = 'test-right-actions-element-2';
cut.setRightActionsElement(right2);
query = cut.element.querySelectorAll(
'.tabs-and-actions-container > .right-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe(
'test-right-actions-element-2'
);
expect(query[0].children.length).toBe(1);
// remove right action
cut.setRightActionsElement(undefined);
query = cut.element.querySelectorAll(
'.tabs-and-actions-container > .right-actions-container'
);
expect(query.length).toBe(1);
expect(query[0].children.length).toBe(0);
});
test('that a tab will become floating when clicked if not floating and shift is selected', () => {
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { isFloating: false } as any,
}) as DockviewGroupPanel;
});
const accessor = new accessorMock();
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const container = cut.element.querySelector('.void-container')!;
expect(container).toBeTruthy();
jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation(
() => {
return { top: 50, left: 100, width: 0, height: 0 } as any;
}
);
jest.spyOn(
accessor.element,
'getBoundingClientRect'
).mockImplementation(() => {
return { top: 10, left: 20, width: 0, height: 0 } as any;
});
const event = new KeyboardEvent('mousedown', { shiftKey: true });
const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault');
fireEvent(container, event);
expect(accessor.addFloatingGroup).toBeCalledWith(
groupPanel,
{
x: 100,
y: 60,
},
{ inDragMode: true }
);
expect(accessor.addFloatingGroup).toBeCalledTimes(1);
expect(eventPreventDefaultSpy).toBeCalledTimes(1);
const event2 = new KeyboardEvent('mousedown', { shiftKey: false });
const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(container, event2);
expect(accessor.addFloatingGroup).toBeCalledTimes(1);
expect(eventPreventDefaultSpy2).toBeCalledTimes(0);
});
test('that a tab that is already floating cannot be floated again', () => {
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { isFloating: true } as any,
}) as DockviewGroupPanel;
});
const accessor = new accessorMock();
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const container = cut.element.querySelector('.void-container')!;
expect(container).toBeTruthy();
jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation(
() => {
return { top: 50, left: 100, width: 0, height: 0 } as any;
}
);
jest.spyOn(
accessor.element,
'getBoundingClientRect'
).mockImplementation(() => {
return { top: 10, left: 20, width: 0, height: 0 } as any;
});
const event = new KeyboardEvent('mousedown', { shiftKey: true });
const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault');
fireEvent(container, event);
expect(accessor.addFloatingGroup).toBeCalledTimes(0);
expect(eventPreventDefaultSpy).toBeCalledTimes(0);
const event2 = new KeyboardEvent('mousedown', { shiftKey: false });
const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(container, event2);
expect(accessor.addFloatingGroup).toBeCalledTimes(0);
expect(eventPreventDefaultSpy2).toBeCalledTimes(0);
});
test('that selecting a tab with shift down will move that tab into a new floating group', () => {
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { isFloating: true } as any,
model: {} as any,
}) as DockviewGroupPanel;
});
const accessor = new accessorMock();
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const panelMock = jest.fn<IDockviewPanel, [string]>((id: string) => {
const partial: Partial<IDockviewPanel> = {
id,
view: {
tab: {
element: document.createElement('div'),
} as any,
content: {
element: document.createElement('div'),
} as any,
} as any,
};
return partial as IDockviewPanel;
});
const panel = new panelMock('test_id');
cut.openPanel(panel);
const el = cut.element.querySelector('.tab')!;
expect(el).toBeTruthy();
const event = new KeyboardEvent('mousedown', { shiftKey: true });
const preventDefaultSpy = jest.spyOn(event, 'preventDefault');
fireEvent(el, event);
// a floating group with a single tab shouldn't be eligible
expect(preventDefaultSpy).toBeCalledTimes(0);
expect(accessor.addFloatingGroup).toBeCalledTimes(0);
const panel2 = new panelMock('test_id_2');
cut.openPanel(panel2);
fireEvent(el, event);
expect(preventDefaultSpy).toBeCalledTimes(1);
expect(accessor.addFloatingGroup).toBeCalledTimes(1);
});
});

View File

@ -247,8 +247,8 @@ describe('groupview', () => {
id: 'dockview-1',
removePanel: removePanelMock,
removeGroup: removeGroupMock,
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
onDidAddPanel: () => ({ dispose: jest.fn() }),
onDidRemovePanel: () => ({ dispose: jest.fn() }),
}) as DockviewComponent;
groupview = new DockviewGroupPanel(dockview, 'groupview-1', options);
@ -858,6 +858,47 @@ describe('groupview', () => {
).toBe(0);
});
test('that the watermark is removed when dispose is called', () => {
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
};
});
const container = document.createElement('div');
const cut = new DockviewGroupPanelModel(
container,
dockview,
'groupviewid',
{},
new groupPanelMock() as DockviewGroupPanel
);
cut.initialize();
expect(
container.getElementsByClassName('watermark-test-container').length
).toBe(1);
cut.dispose();
expect(
container.getElementsByClassName('watermark-test-container').length
).toBe(0);
});
test('that watermark is added', () => {
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {

View File

@ -0,0 +1,21 @@
import { quasiDefaultPrevented, quasiPreventDefault } from '../dom';
describe('dom', () => {
test('quasiPreventDefault', () => {
const event = new Event('myevent');
expect((event as any)['dv-quasiPreventDefault']).toBeUndefined();
quasiPreventDefault(event);
expect((event as any)['dv-quasiPreventDefault']).toBe(true);
});
test('quasiDefaultPrevented', () => {
const event = new Event('myevent');
expect(quasiDefaultPrevented(event)).toBeFalsy();
(event as any)['dv-quasiPreventDefault'] = false;
expect(quasiDefaultPrevented(event)).toBeFalsy();
(event as any)['dv-quasiPreventDefault'] = true;
expect(quasiDefaultPrevented(event)).toBeTruthy();
});
});

View File

@ -690,4 +690,37 @@ describe('gridview', () => {
gridview.element.querySelectorAll('.mock-grid-view').length
).toBe(4);
});
test('that calling insertOrthogonalSplitviewAtRoot() for an empty view doesnt add any nodes', () => {
const gridview = new Gridview(
false,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
expect(gridview.serialize()).toEqual({
height: 1000,
orientation: 'HORIZONTAL',
root: {
data: [],
size: 1000,
type: 'branch',
},
width: 1000,
});
gridview.insertOrthogonalSplitviewAtRoot();
expect(gridview.serialize()).toEqual({
height: 1000,
orientation: 'VERTICAL',
root: {
data: [],
size: 1000,
type: 'branch',
},
width: 1000,
});
});
});

View File

@ -471,6 +471,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -528,7 +530,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
// gridview.layout(800, 400);
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -552,7 +555,6 @@ describe('gridview', () => {
},
activePanel: 'panel_1',
});
// gridview.layout(800, 400, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
@ -587,7 +589,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
// gridview.layout(800, 400);
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -620,7 +623,6 @@ describe('gridview', () => {
},
activePanel: 'panel_1',
});
// gridview.layout(800, 400, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
@ -664,7 +666,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
// gridview.layout(800, 400);
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -706,7 +709,6 @@ describe('gridview', () => {
},
activePanel: 'panel_1',
});
// gridview.layout(800, 400, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
@ -759,7 +761,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
// gridview.layout(800, 400);
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -801,7 +804,6 @@ describe('gridview', () => {
},
activePanel: 'panel_1',
});
// gridview.layout(800, 400, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
@ -854,6 +856,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -895,7 +899,6 @@ describe('gridview', () => {
},
activePanel: 'panel_1',
});
gridview.layout(800, 400, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
@ -948,7 +951,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
// gridview.layout(800, 400);
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -1005,7 +1009,6 @@ describe('gridview', () => {
},
activePanel: 'panel_1',
});
// gridview.layout(800, 400, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
@ -1198,6 +1201,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -1254,7 +1259,8 @@ describe('gridview', () => {
},
activePanel: 'panel_1',
});
gridview.layout(800, 400, true);
// gridview.layout(800, 400, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
@ -1322,6 +1328,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -1445,6 +1453,8 @@ describe('gridview', () => {
components: { default: TestGridview },
});
gridview.layout(800, 400);
gridview.fromJSON({
grid: {
height: 400,
@ -1908,4 +1918,683 @@ describe('gridview', () => {
return disposable.dispose();
});
test('that fromJSON layouts are resized to the current dimensions', async () => {
const container = document.createElement('div');
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
});
gridview.layout(1600, 800);
gridview.fromJSON({
grid: {
height: 400,
width: 800,
orientation: Orientation.HORIZONTAL,
root: {
type: 'branch',
size: 400,
data: [
{
type: 'leaf',
size: 200,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 400,
data: [
{
type: 'leaf',
size: 250,
data: {
id: 'panel_2',
component: 'default',
snap: false,
},
},
{
type: 'leaf',
size: 150,
data: {
id: 'panel_3',
component: 'default',
snap: false,
},
},
],
},
{
type: 'leaf',
size: 200,
data: {
id: 'panel_4',
component: 'default',
snap: false,
},
},
],
},
},
activePanel: 'panel_1',
});
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
height: 800,
width: 1600,
orientation: Orientation.HORIZONTAL,
root: {
type: 'branch',
size: 800,
data: [
{
type: 'leaf',
size: 400,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 800,
data: [
{
type: 'leaf',
size: 500,
data: {
id: 'panel_2',
component: 'default',
snap: false,
},
},
{
type: 'leaf',
size: 300,
data: {
id: 'panel_3',
component: 'default',
snap: false,
},
},
],
},
{
type: 'leaf',
size: 400,
data: {
id: 'panel_4',
component: 'default',
snap: false,
},
},
],
},
},
activePanel: 'panel_1',
});
});
test('that a deep HORIZONTAL layout with fromJSON dimensions identical to the current dimensions loads', async () => {
const container = document.createElement('div');
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
});
gridview.layout(6000, 5000);
gridview.fromJSON({
grid: {
height: 5000,
width: 6000,
orientation: Orientation.HORIZONTAL,
root: {
type: 'branch',
size: 5000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 3000,
data: [
{
type: 'branch',
size: 4000,
data: [
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_2',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 2000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_3',
component: 'default',
snap: false,
},
},
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_4',
component: 'default',
snap: false,
},
},
],
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_5',
component: 'default',
snap: false,
},
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_6',
component: 'default',
snap: false,
},
},
],
},
},
activePanel: 'panel_1',
});
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
height: 5000,
width: 6000,
orientation: Orientation.HORIZONTAL,
root: {
type: 'branch',
size: 5000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 3000,
data: [
{
type: 'branch',
size: 4000,
data: [
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_2',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 2000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_3',
component: 'default',
snap: false,
},
},
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_4',
component: 'default',
snap: false,
},
},
],
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_5',
component: 'default',
snap: false,
},
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_6',
component: 'default',
snap: false,
},
},
],
},
},
activePanel: 'panel_1',
});
gridview.layout(6000, 5000, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
height: 5000,
width: 6000,
orientation: Orientation.HORIZONTAL,
root: {
type: 'branch',
size: 5000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 3000,
data: [
{
type: 'branch',
size: 4000,
data: [
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_2',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 2000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_3',
component: 'default',
snap: false,
},
},
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_4',
component: 'default',
snap: false,
},
},
],
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_5',
component: 'default',
snap: false,
},
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_6',
component: 'default',
snap: false,
},
},
],
},
},
activePanel: 'panel_1',
});
});
test('that a deep VERTICAL layout with fromJSON dimensions identical to the current dimensions loads', async () => {
const container = document.createElement('div');
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
});
gridview.layout(5000, 6000);
gridview.fromJSON({
grid: {
height: 6000,
width: 5000,
orientation: Orientation.VERTICAL,
root: {
type: 'branch',
size: 5000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 3000,
data: [
{
type: 'branch',
size: 4000,
data: [
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_2',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 2000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_3',
component: 'default',
snap: false,
},
},
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_4',
component: 'default',
snap: false,
},
},
],
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_5',
component: 'default',
snap: false,
},
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_6',
component: 'default',
snap: false,
},
},
],
},
},
activePanel: 'panel_1',
});
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
height: 6000,
width: 5000,
orientation: Orientation.VERTICAL,
root: {
type: 'branch',
size: 5000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 3000,
data: [
{
type: 'branch',
size: 4000,
data: [
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_2',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 2000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_3',
component: 'default',
snap: false,
},
},
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_4',
component: 'default',
snap: false,
},
},
],
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_5',
component: 'default',
snap: false,
},
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_6',
component: 'default',
snap: false,
},
},
],
},
},
activePanel: 'panel_1',
});
gridview.layout(5000, 6000, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
height: 6000,
width: 5000,
orientation: Orientation.VERTICAL,
root: {
type: 'branch',
size: 5000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 3000,
data: [
{
type: 'branch',
size: 4000,
data: [
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_2',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 2000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_3',
component: 'default',
snap: false,
},
},
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_4',
component: 'default',
snap: false,
},
},
],
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_5',
component: 'default',
snap: false,
},
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_6',
component: 'default',
snap: false,
},
},
],
},
},
activePanel: 'panel_1',
});
});
});

View File

@ -1,334 +0,0 @@
import { DockviewComponent } from '../../../dockview/dockviewComponent';
import { TabsContainer } from '../../../dockview/components/titlebar/tabsContainer';
import { fireEvent } from '@testing-library/dom';
import {
LocalSelectionTransfer,
PanelTransfer,
} from '../../../dnd/dataTransfer';
import { TestPanel } from '../dockviewGroupPanelModel.spec';
import { DockviewGroupPanelModel } from '../../../dockview/dockviewGroupPanelModel';
import { DockviewGroupPanel } from '../../../dockview/dockviewGroupPanel';
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 {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
model: groupView,
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
const emptySpace = cut.element
.getElementsByClassName('void-container')
.item(0);
if (!emptySpace) {
fail('element not found');
}
jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation(
() => 100
);
fireEvent.dragEnter(emptySpace);
fireEvent.dragOver(emptySpace);
expect(groupView.canDisplayOverlay).toBeCalled();
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(0);
});
test('that a drag over event from another 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<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
panels: [],
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
const emptySpace = cut.element
.getElementsByClassName('void-container')
.item(0);
if (!emptySpace) {
fail('element not found');
}
jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation(
() => 100
);
LocalSelectionTransfer.getInstance().setData(
[
new PanelTransfer(
'testcomponentid',
'anothergroupid',
'anotherpanelid'
),
],
PanelTransfer.prototype
);
fireEvent.dragEnter(emptySpace);
fireEvent.dragOver(emptySpace);
expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(1);
});
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<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
panels: [],
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element
.getElementsByClassName('void-container')
.item(0);
if (!emptySpace) {
fail('element not found');
}
jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation(
() => 100
);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'anothergroupid', 'panel2')],
PanelTransfer.prototype
);
fireEvent.dragEnter(emptySpace);
fireEvent.dragOver(emptySpace);
expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
).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<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
panels: [],
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element
.getElementsByClassName('void-container')
.item(0);
if (!emptySpace) {
fail('element not found');
}
jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation(
() => 100
);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'anothergroupid', 'panel1')],
PanelTransfer.prototype
);
fireEvent.dragEnter(emptySpace);
fireEvent.dragOver(emptySpace);
expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(1);
});
test('that dropping a tab from another component should not 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<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
};
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new TabsContainer(accessor, groupPanel);
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element
.getElementsByClassName('void-container')
.item(0);
if (!emptySpace) {
fail('element not found');
}
jest.spyOn(emptySpace, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace, 'clientWidth', 'get').mockImplementation(
() => 100
);
LocalSelectionTransfer.getInstance().setData(
[
new PanelTransfer(
'anothercomponentid',
'anothergroupid',
'panel1'
),
],
PanelTransfer.prototype
);
fireEvent.dragEnter(emptySpace);
fireEvent.dragOver(emptySpace);
expect(groupView.canDisplayOverlay).toBeCalledTimes(1);
expect(
cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(0);
});
});

View File

@ -96,7 +96,7 @@ describe('componentFactory', () => {
expect(component).toHaveBeenCalled();
expect(componentResult instanceof component);
expect(componentResult instanceof component).toBeTruthy();
});
});
});

View File

@ -408,4 +408,85 @@ describe('componentPaneview', () => {
expect(panel1Spy).toHaveBeenCalledTimes(1);
expect(panel2Spy).toHaveBeenCalledTimes(1);
});
test('that fromJSON layouts are resized to the current dimensions', async () => {
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
},
});
paneview.layout(400, 600);
paneview.fromJSON({
size: 6,
views: [
{
size: 1,
data: {
id: 'panel1',
component: 'testPanel',
title: 'Panel 1',
},
expanded: true,
},
{
size: 2,
data: {
id: 'panel2',
component: 'testPanel',
title: 'Panel 2',
},
expanded: true,
},
{
size: 3,
data: {
id: 'panel3',
component: 'testPanel',
title: 'Panel 3',
},
expanded: true,
},
],
});
// heights slightly differ because header height isn't accounted for
expect(JSON.parse(JSON.stringify(paneview.toJSON()))).toEqual({
size: 600,
views: [
{
size: 122,
data: {
id: 'panel1',
component: 'testPanel',
title: 'Panel 1',
},
expanded: true,
minimumSize: 100,
},
{
size: 122,
data: {
id: 'panel2',
component: 'testPanel',
title: 'Panel 2',
},
expanded: true,
minimumSize: 100,
},
{
size: 356,
data: {
id: 'panel3',
component: 'testPanel',
title: 'Panel 3',
},
expanded: true,
minimumSize: 100,
},
],
});
});
});

View File

@ -599,7 +599,7 @@ describe('splitview', () => {
expect(container.childNodes.length).toBe(0);
});
test('dnd: mouse events to move sash', () => {
test('dnd: pointer events to move sash', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: false,
@ -629,107 +629,51 @@ describe('splitview', () => {
expect(view2.element.parentElement!.style.pointerEvents).toBe('');
// start the drag event
fireEvent.mouseDown(sashElement, { clientX: 50, clientY: 100 });
expect(addEventListenerSpy).toBeCalledTimes(5);
// during a sash drag the views should have pointer-events disabled
expect(view1.element.parentElement!.style.pointerEvents).toBe('none');
expect(view2.element.parentElement!.style.pointerEvents).toBe('none');
// expect a delta move of 70 - 50 = 20
fireEvent.mouseMove(document, { clientX: 70, clientY: 110 });
expect([view1.size, view2.size]).toEqual([220, 180]);
// expect a delta move of 75 - 70 = 5
fireEvent.mouseMove(document, { clientX: 75, clientY: 110 });
expect([view1.size, view2.size]).toEqual([225, 175]);
// end the drag event
fireEvent.mouseUp(document);
expect(removeEventListenerSpy).toBeCalledTimes(5);
// expect pointer-eventes on views to be restored
expect(view1.element.parentElement!.style.pointerEvents).toBe('');
expect(view2.element.parentElement!.style.pointerEvents).toBe('');
fireEvent.mouseMove(document, { clientX: 100, clientY: 100 });
// expect no additional resizes
expect([view1.size, view2.size]).toEqual([225, 175]);
// expect no additional document listeners
expect(addEventListenerSpy).toBeCalledTimes(5);
expect(removeEventListenerSpy).toBeCalledTimes(5);
});
test('dnd: touch events to move sash', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: false,
});
splitview.layout(400, 500);
const view1 = new Testview(0, 1000);
const view2 = new Testview(0, 1000);
splitview.addView(view1);
splitview.addView(view2);
const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
const removeEventListenerSpy = jest.spyOn(
document,
'removeEventListener'
fireEvent(
sashElement,
new MouseEvent('pointerdown', { clientX: 50, clientY: 100 })
);
const sashElement = container
.getElementsByClassName('sash')
.item(0) as HTMLElement;
// validate the expected state before drag
expect([view1.size, view2.size]).toEqual([200, 200]);
expect(sashElement).toBeTruthy();
expect(view1.element.parentElement!.style.pointerEvents).toBe('');
expect(view2.element.parentElement!.style.pointerEvents).toBe('');
// start the drag event
fireEvent.touchStart(sashElement, {
touches: [{ clientX: 50, clientY: 100 }],
});
expect(addEventListenerSpy).toBeCalledTimes(5);
expect(addEventListenerSpy).toBeCalledTimes(3);
// during a sash drag the views should have pointer-events disabled
expect(view1.element.parentElement!.style.pointerEvents).toBe('none');
expect(view2.element.parentElement!.style.pointerEvents).toBe('none');
// expect a delta move of 70 - 50 = 20
fireEvent.touchMove(document, {
touches: [{ clientX: 70, clientY: 110 }],
});
fireEvent(
document,
new MouseEvent('pointermove', { clientX: 70, clientY: 110 })
);
expect([view1.size, view2.size]).toEqual([220, 180]);
// expect a delta move of 75 - 70 = 5
fireEvent.touchMove(document, {
touches: [{ clientX: 75, clientY: 110 }],
});
fireEvent(
document,
new MouseEvent('pointermove', { clientX: 75, clientY: 110 })
);
expect([view1.size, view2.size]).toEqual([225, 175]);
// end the drag event
fireEvent.touchEnd(document);
fireEvent(
document,
new MouseEvent('pointerup', { clientX: 70, clientY: 110 })
);
expect(removeEventListenerSpy).toBeCalledTimes(5);
expect(removeEventListenerSpy).toBeCalledTimes(3);
// expect pointer-eventes on views to be restored
expect(view1.element.parentElement!.style.pointerEvents).toBe('');
expect(view2.element.parentElement!.style.pointerEvents).toBe('');
fireEvent.touchMove(document, {
touches: [{ clientX: 100, clientY: 100 }],
});
fireEvent(
document,
new MouseEvent('pointermove', { clientX: 100, clientY: 100 })
);
// expect no additional resizes
expect([view1.size, view2.size]).toEqual([225, 175]);
// expect no additional document listeners
expect(addEventListenerSpy).toBeCalledTimes(5);
expect(removeEventListenerSpy).toBeCalledTimes(5);
expect(addEventListenerSpy).toBeCalledTimes(3);
expect(removeEventListenerSpy).toBeCalledTimes(3);
});
});

View File

@ -330,7 +330,7 @@ describe('componentSplitview', () => {
testPanel: TestPanel,
},
});
splitview.layout(600, 400);
splitview.layout(400, 6);
splitview.fromJSON({
views: [
@ -535,4 +535,57 @@ describe('componentSplitview', () => {
expect(panel1Spy).toHaveBeenCalledTimes(1);
expect(panel2Spy).toHaveBeenCalledTimes(1);
});
test('that fromJSON layouts are resized to the current dimensions', async () => {
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL,
components: {
testPanel: TestPanel,
},
});
splitview.layout(400, 600);
splitview.fromJSON({
views: [
{
size: 1,
data: { id: 'panel1', component: 'testPanel' },
snap: false,
},
{
size: 2,
data: { id: 'panel2', component: 'testPanel' },
snap: true,
},
{ size: 3, data: { id: 'panel3', component: 'testPanel' } },
],
size: 6,
orientation: Orientation.VERTICAL,
activeView: 'panel1',
});
expect(JSON.parse(JSON.stringify(splitview.toJSON()))).toEqual({
views: [
{
size: 100,
data: { id: 'panel1', component: 'testPanel' },
snap: false,
},
{
size: 200,
data: { id: 'panel2', component: 'testPanel' },
snap: true,
},
{
size: 300,
data: { id: 'panel3', component: 'testPanel' },
snap: false,
},
],
size: 600,
orientation: Orientation.VERTICAL,
activeView: 'panel1',
});
});
});

View File

@ -436,7 +436,11 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.addPanel(options);
}
addGroup(options?: AddGroupOptions): IDockviewGroupPanel {
removePanel(panel: IDockviewPanel): void {
this.component.removePanel(panel);
}
addGroup(options?: AddGroupOptions): DockviewGroupPanel {
return this.component.addGroup(options);
}
@ -460,6 +464,13 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.getPanel(id);
}
addFloatingGroup(
item: IDockviewPanel | DockviewGroupPanel,
coord?: { x: number; y: number }
): void {
return this.component.addFloatingGroup(item, coord);
}
fromJSON(data: SerializedDockview): void {
this.component.fromJSON(data);
}

View File

@ -0,0 +1,54 @@
import { Position } from '../dnd/droptarget';
import { DockviewComponent } from '../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { Emitter, Event } from '../events';
import { GridviewPanelApi, GridviewPanelApiImpl } from './gridviewPanelApi';
export interface DockviewGroupPanelApi extends GridviewPanelApi {
readonly onDidFloatingStateChange: Event<DockviewGroupPanelFloatingChangeEvent>;
readonly isFloating: boolean;
moveTo(options: { group: DockviewGroupPanel; position?: Position }): void;
}
export interface DockviewGroupPanelFloatingChangeEvent {
readonly isFloating: boolean;
}
export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
private _group: DockviewGroupPanel | undefined;
readonly _onDidFloatingStateChange =
new Emitter<DockviewGroupPanelFloatingChangeEvent>();
readonly onDidFloatingStateChange: Event<DockviewGroupPanelFloatingChangeEvent> =
this._onDidFloatingStateChange.event;
get isFloating() {
if (!this._group) {
throw new Error(`DockviewGroupPanelApiImpl not initialized`);
}
return this._group.model.isFloating;
}
constructor(id: string, private readonly accessor: DockviewComponent) {
super(id);
this.addDisposables(this._onDidFloatingStateChange);
}
moveTo(options: { group: DockviewGroupPanel; position?: Position }): void {
if (!this._group) {
throw new Error(`DockviewGroupPanelApiImpl not initialized`);
}
this.accessor.moveGroupOrPanel(
options.group,
this._group.id,
undefined,
options.position ?? 'center'
);
}
initialize(group: DockviewGroupPanel): void {
this._group = group;
}
}

View File

@ -3,6 +3,8 @@ import { GridviewPanelApiImpl, GridviewPanelApi } from './gridviewPanelApi';
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { MutableDisposable } from '../lifecycle';
import { IDockviewPanel } from '../dockview/dockviewPanel';
import { DockviewComponent } from '../dockview/dockviewComponent';
import { Position } from '../dnd/droptarget';
export interface TitleEvent {
readonly title: string;
@ -24,6 +26,11 @@ export interface DockviewPanelApi
readonly onDidGroupChange: Event<void>;
close(): void;
setTitle(title: string): void;
moveTo(options: {
group: DockviewGroupPanel;
position?: Position;
index?: number;
}): void;
}
export class DockviewPanelApiImpl
@ -73,7 +80,11 @@ export class DockviewPanelApiImpl
return this._group;
}
constructor(private panel: IDockviewPanel, group: DockviewGroupPanel) {
constructor(
private panel: IDockviewPanel,
group: DockviewGroupPanel,
private readonly accessor: DockviewComponent
) {
super(panel.id);
this.initialize(panel);
@ -88,11 +99,25 @@ export class DockviewPanelApiImpl
);
}
public setTitle(title: string): void {
moveTo(options: {
group: DockviewGroupPanel;
position?: Position;
index?: number;
}): void {
this.accessor.moveGroupOrPanel(
options.group,
this._group.id,
this.panel.id,
options.position ?? 'center',
options.index
);
}
setTitle(title: string): void {
this.panel.setTitle(title);
}
public close(): void {
close(): void {
this.group.model.closePanel(this.panel);
}
}

View File

@ -61,3 +61,13 @@ export function firstIndex<T>(
return -1;
}
export function remove<T>(array: T[], value: T): boolean {
const index = array.findIndex((t) => t === value);
if (index > -1) {
array.splice(index, 1);
return true;
}
return false;
}

View File

@ -27,10 +27,19 @@ export abstract class DragHandler extends CompositeDisposable {
abstract getData(dataTransfer?: DataTransfer | null): IDisposable;
protected isCancelled(_event: DragEvent): boolean {
return false;
}
private configure(): void {
this.addDisposables(
this._onDragStart,
addDisposableListener(this.el, 'dragstart', (event) => {
if (this.isCancelled(event)) {
event.preventDefault();
return;
}
const iframes = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),

View File

@ -7,7 +7,8 @@
top: 0px;
height: 100%;
width: 100%;
z-index: 10000;
z-index: 1000;
pointer-events: none;
> .drop-target-selection {
position: relative;
@ -15,7 +16,9 @@
height: 100%;
width: 100%;
background-color: var(--dv-drag-over-background-color);
transition: top 70ms ease-out,left 70ms ease-out,width 70ms ease-out,height 70ms ease-out,opacity .15s ease-out;
transition: top 70ms ease-out, left 70ms ease-out,
width 70ms ease-out, height 70ms ease-out,
opacity 0.15s ease-out;
will-change: transform;
pointer-events: none;

View File

@ -58,10 +58,13 @@ export class Droptarget extends CompositeDisposable {
private targetElement: HTMLElement | undefined;
private overlayElement: HTMLElement | undefined;
private _state: Position | undefined;
private _acceptedTargetZonesSet: Set<Position>;
private readonly _onDrop = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
private static USED_EVENT_ID = '__dockview_droptarget_event_is_used__';
get state(): Position | undefined {
return this._state;
}
@ -83,7 +86,7 @@ export class Droptarget extends CompositeDisposable {
super();
// use a set to take advantage of #<set>.has
const acceptedTargetZonesSet = new Set(
this._acceptedTargetZonesSet = new Set(
this.options.acceptedTargetZones
);
@ -92,6 +95,11 @@ export class Droptarget extends CompositeDisposable {
new DragAndDropObserver(this.element, {
onDragEnter: () => undefined,
onDragOver: (e) => {
if (this._acceptedTargetZonesSet.size === 0) {
this.removeDropTarget();
return;
}
const width = this.element.clientWidth;
const height = this.element.clientHeight;
@ -106,14 +114,19 @@ export class Droptarget extends CompositeDisposable {
const y = e.clientY - rect.top;
const quadrant = this.calculateQuadrant(
acceptedTargetZonesSet,
this._acceptedTargetZonesSet,
x,
y,
width,
height
);
if (quadrant === null) {
/**
* If the event has already been used by another DropTarget instance
* then don't show a second drop target, only one target should be
* active at any one time
*/
if (this.isAlreadyUsed(e) || quadrant === null) {
// no drop target should be displayed
this.removeDropTarget();
return;
@ -121,12 +134,16 @@ export class Droptarget extends CompositeDisposable {
if (typeof this.options.canDisplayOverlay === 'boolean') {
if (!this.options.canDisplayOverlay) {
this.removeDropTarget();
return;
}
} else if (!this.options.canDisplayOverlay(e, quadrant)) {
this.removeDropTarget();
return;
}
this.markAsUsed(e);
if (!this.targetElement) {
this.targetElement = document.createElement('div');
this.targetElement.className = 'drop-target-dropzone';
@ -139,14 +156,6 @@ export class Droptarget extends CompositeDisposable {
this.element.append(this.targetElement);
}
if (this.options.acceptedTargetZones.length === 0) {
return;
}
if (!this.targetElement || !this.overlayElement) {
return;
}
this.toggleClasses(quadrant, width, height);
this.setState(quadrant);
@ -175,11 +184,30 @@ export class Droptarget extends CompositeDisposable {
);
}
public dispose(): void {
setTargetZones(acceptedTargetZones: Position[]): void {
this._acceptedTargetZonesSet = new Set(acceptedTargetZones);
}
dispose(): void {
this.removeDropTarget();
super.dispose();
}
/**
* Add a property to the event object for other potential listeners to check
*/
private markAsUsed(event: DragEvent): void {
(event as any)[Droptarget.USED_EVENT_ID] = true;
}
/**
* Check is the event has already been used by another instance od DropTarget
*/
private isAlreadyUsed(event: DragEvent): boolean {
const value = (event as any)[Droptarget.USED_EVENT_ID];
return typeof value === 'boolean' && value;
}
private toggleClasses(
quadrant: Position,
width: number,

View File

@ -1,4 +1,6 @@
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { quasiPreventDefault } from '../dom';
import { addDisposableListener } from '../events';
import { IDisposable } from '../lifecycle';
import { DragHandler } from './abstractDragHandler';
import { LocalSelectionTransfer, PanelTransfer } from './dataTransfer';
@ -14,6 +16,31 @@ export class GroupDragHandler extends DragHandler {
private readonly group: DockviewGroupPanel
) {
super(element);
this.addDisposables(
addDisposableListener(
element,
'mousedown',
(e) => {
if (e.shiftKey) {
/**
* You cannot call e.preventDefault() because that will prevent drag events from firing
* but we also need to stop any group overlay drag events from occuring
* Use a custom event marker that can be checked by the overlay drag events
*/
quasiPreventDefault(e);
}
},
true
)
);
}
override isCancelled(_event: DragEvent): boolean {
if (this.group.api.isFloating && !_event.shiftKey) {
return true;
}
return false;
}
getData(dataTransfer: DataTransfer | null): IDisposable {

View File

@ -0,0 +1,122 @@
.dv-debug {
.dv-resize-container {
.dv-resize-handle-top {
background-color: red;
}
.dv-resize-handle-bottom {
background-color: green;
}
.dv-resize-handle-left {
background-color: yellow;
}
.dv-resize-handle-right {
background-color: blue;
}
.dv-resize-handle-topleft,
.dv-resize-handle-topright,
.dv-resize-handle-bottomleft,
.dv-resize-handle-bottomright {
background-color: cyan;
}
}
}
.dv-resize-container {
position: absolute;
z-index: 997;
&.dv-bring-to-front {
z-index: 998;
}
border: 1px solid var(--dv-tab-divider-color);
box-shadow: var(--dv-floating-box-shadow);
&.dv-resize-container-dragging {
opacity: 0.5;
}
.dv-resize-handle-top {
height: 4px;
width: calc(100% - 8px);
left: 4px;
top: -2px;
z-index: 999;
position: absolute;
cursor: ns-resize;
}
.dv-resize-handle-bottom {
height: 4px;
width: calc(100% - 8px);
left: 4px;
bottom: -2px;
z-index: 999;
position: absolute;
cursor: ns-resize;
}
.dv-resize-handle-left {
height: calc(100% - 8px);
width: 4px;
left: -2px;
top: 4px;
z-index: 999;
position: absolute;
cursor: ew-resize;
}
.dv-resize-handle-right {
height: calc(100% - 8px);
width: 4px;
right: -2px;
top: 4px;
z-index: 999;
position: absolute;
cursor: ew-resize;
}
.dv-resize-handle-topleft {
height: 4px;
width: 4px;
top: -2px;
left: -2px;
z-index: 999;
position: absolute;
cursor: nw-resize;
}
.dv-resize-handle-topright {
height: 4px;
width: 4px;
right: -2px;
top: -2px;
z-index: 999;
position: absolute;
cursor: ne-resize;
}
.dv-resize-handle-bottomleft {
height: 4px;
width: 4px;
left: -2px;
bottom: -2px;
z-index: 999;
position: absolute;
cursor: sw-resize;
}
.dv-resize-handle-bottomright {
height: 4px;
width: 4px;
right: -2px;
bottom: -2px;
z-index: 999;
position: absolute;
cursor: se-resize;
}
}

View File

@ -0,0 +1,484 @@
import {
getElementsByTagName,
quasiDefaultPrevented,
toggleClass,
} from '../dom';
import {
Emitter,
Event,
addDisposableListener,
addDisposableWindowListener,
} from '../events';
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { clamp } from '../math';
const bringElementToFront = (() => {
let previous: HTMLElement | null = null;
function pushToTop(element: HTMLElement) {
if (previous !== element && previous !== null) {
toggleClass(previous, 'dv-bring-to-front', false);
}
toggleClass(element, 'dv-bring-to-front', true);
previous = element;
}
return pushToTop;
})();
export class Overlay extends CompositeDisposable {
private _element: HTMLElement = document.createElement('div');
private readonly _onDidChange = new Emitter<void>();
readonly onDidChange: Event<void> = this._onDidChange.event;
private readonly _onDidChangeEnd = new Emitter<void>();
readonly onDidChangeEnd: Event<void> = this._onDidChangeEnd.event;
private static MINIMUM_HEIGHT = 20;
private static MINIMUM_WIDTH = 20;
constructor(
private readonly options: {
height: number;
width: number;
left: number;
top: number;
container: HTMLElement;
content: HTMLElement;
minimumInViewportWidth: number;
minimumInViewportHeight: number;
}
) {
super();
this.addDisposables(this._onDidChange, this._onDidChangeEnd);
this._element.className = 'dv-resize-container';
this.setupResize('top');
this.setupResize('bottom');
this.setupResize('left');
this.setupResize('right');
this.setupResize('topleft');
this.setupResize('topright');
this.setupResize('bottomleft');
this.setupResize('bottomright');
this._element.appendChild(this.options.content);
this.options.container.appendChild(this._element);
// if input bad resize within acceptable boundaries
this.setBounds({
height: this.options.height,
width: this.options.width,
top: this.options.top,
left: this.options.left,
});
}
setBounds(
bounds: Partial<{
height: number;
width: number;
top: number;
left: number;
}> = {}
): void {
if (typeof bounds.height === 'number') {
this._element.style.height = `${bounds.height}px`;
}
if (typeof bounds.width === 'number') {
this._element.style.width = `${bounds.width}px`;
}
if (typeof bounds.top === 'number') {
this._element.style.top = `${bounds.top}px`;
}
if (typeof bounds.left === 'number') {
this._element.style.left = `${bounds.left}px`;
}
const containerRect = this.options.container.getBoundingClientRect();
const overlayRect = this._element.getBoundingClientRect();
// region: ensure bounds within allowable limits
// a minimum width of minimumViewportWidth must be inside the viewport
const xOffset = Math.max(
0,
overlayRect.width - this.options.minimumInViewportWidth
);
// a minimum height of minimumViewportHeight must be inside the viewport
const yOffset = Math.max(
0,
overlayRect.height - this.options.minimumInViewportHeight
);
const left = clamp(
overlayRect.left - containerRect.left,
-xOffset,
Math.max(0, containerRect.width - overlayRect.width + xOffset)
);
const top = clamp(
overlayRect.top - containerRect.top,
-yOffset,
Math.max(0, containerRect.height - overlayRect.height + yOffset)
);
this._element.style.left = `${left}px`;
this._element.style.top = `${top}px`;
this._onDidChange.fire();
}
toJSON(): { top: number; left: number; height: number; width: number } {
const container = this.options.container.getBoundingClientRect();
const element = this._element.getBoundingClientRect();
return {
top: element.top - container.top,
left: element.left - container.left,
width: element.width,
height: element.height,
};
}
setupDrag(
dragTarget: HTMLElement,
options: { inDragMode: boolean } = { inDragMode: false }
): void {
const move = new MutableDisposable();
const track = () => {
let offset: { x: number; y: number } | null = null;
const iframes = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
for (const iframe of iframes) {
iframe.style.pointerEvents = 'none';
}
move.value = new CompositeDisposable(
{
dispose: () => {
for (const iframe of iframes) {
iframe.style.pointerEvents = 'auto';
}
},
},
addDisposableWindowListener(window, 'mousemove', (e) => {
const containerRect =
this.options.container.getBoundingClientRect();
const x = e.clientX - containerRect.left;
const y = e.clientY - containerRect.top;
toggleClass(
this._element,
'dv-resize-container-dragging',
true
);
const overlayRect = this._element.getBoundingClientRect();
if (offset === null) {
offset = {
x: e.clientX - overlayRect.left,
y: e.clientY - overlayRect.top,
};
}
const xOffset = Math.max(
0,
overlayRect.width - this.options.minimumInViewportWidth
);
const yOffset = Math.max(
0,
overlayRect.height -
this.options.minimumInViewportHeight
);
const left = clamp(
x - offset.x,
-xOffset,
Math.max(
0,
containerRect.width - overlayRect.width + xOffset
)
);
const top = clamp(
y - offset.y,
-yOffset,
Math.max(
0,
containerRect.height - overlayRect.height + yOffset
)
);
this.setBounds({ top, left });
}),
addDisposableWindowListener(window, 'mouseup', () => {
toggleClass(
this._element,
'dv-resize-container-dragging',
false
);
move.dispose();
this._onDidChangeEnd.fire();
})
);
};
this.addDisposables(
move,
addDisposableListener(dragTarget, 'mousedown', (event) => {
if (event.defaultPrevented) {
event.preventDefault();
return;
}
// if somebody has marked this event then treat as a defaultPrevented
// without actually calling event.preventDefault()
if (quasiDefaultPrevented(event)) {
return;
}
track();
}),
addDisposableListener(
this.options.content,
'mousedown',
(event) => {
if (event.defaultPrevented) {
return;
}
// if somebody has marked this event then treat as a defaultPrevented
// without actually calling event.preventDefault()
if (quasiDefaultPrevented(event)) {
return;
}
if (event.shiftKey) {
track();
}
}
),
addDisposableListener(
this.options.content,
'mousedown',
() => {
bringElementToFront(this._element);
},
true
)
);
bringElementToFront(this._element);
if (options.inDragMode) {
track();
}
}
private setupResize(
direction:
| 'top'
| 'bottom'
| 'left'
| 'right'
| 'topleft'
| 'topright'
| 'bottomleft'
| 'bottomright'
): void {
const resizeHandleElement = document.createElement('div');
resizeHandleElement.className = `dv-resize-handle-${direction}`;
this._element.appendChild(resizeHandleElement);
const move = new MutableDisposable();
this.addDisposables(
move,
addDisposableListener(resizeHandleElement, 'mousedown', (e) => {
e.preventDefault();
let startPosition: {
originalY: number;
originalHeight: number;
originalX: number;
originalWidth: number;
} | null = null;
const iframes = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
for (const iframe of iframes) {
iframe.style.pointerEvents = 'none';
}
move.value = new CompositeDisposable(
addDisposableWindowListener(window, 'mousemove', (e) => {
const containerRect =
this.options.container.getBoundingClientRect();
const overlayRect =
this._element.getBoundingClientRect();
const y = e.clientY - containerRect.top;
const x = e.clientX - containerRect.left;
if (startPosition === null) {
// record the initial dimensions since as all subsequence moves are relative to this
startPosition = {
originalY: y,
originalHeight: overlayRect.height,
originalX: x,
originalWidth: overlayRect.width,
};
}
let top: number | undefined = undefined;
let height: number | undefined = undefined;
let left: number | undefined = undefined;
let width: number | undefined = undefined;
const minimumInViewportHeight =
this.options.minimumInViewportHeight;
const minimumInViewportWidth =
this.options.minimumInViewportWidth;
function moveTop(): void {
top = clamp(
y,
-Number.MAX_VALUE,
startPosition!.originalY +
startPosition!.originalHeight >
containerRect.height
? containerRect.height -
minimumInViewportHeight
: Math.max(
0,
startPosition!.originalY +
startPosition!.originalHeight -
Overlay.MINIMUM_HEIGHT
)
);
height =
startPosition!.originalY +
startPosition!.originalHeight -
top;
}
function moveBottom(): void {
top =
startPosition!.originalY -
startPosition!.originalHeight;
height = clamp(
y - top,
top < 0
? -top + minimumInViewportHeight
: Overlay.MINIMUM_HEIGHT,
Number.MAX_VALUE
);
}
function moveLeft(): void {
left = clamp(
x,
-Number.MAX_VALUE,
startPosition!.originalX +
startPosition!.originalWidth >
containerRect.width
? containerRect.width -
minimumInViewportWidth
: Math.max(
0,
startPosition!.originalX +
startPosition!.originalWidth -
Overlay.MINIMUM_WIDTH
)
);
width =
startPosition!.originalX +
startPosition!.originalWidth -
left;
}
function moveRight(): void {
left =
startPosition!.originalX -
startPosition!.originalWidth;
width = clamp(
x - left,
left < 0
? -left + minimumInViewportWidth
: Overlay.MINIMUM_WIDTH,
Number.MAX_VALUE
);
}
switch (direction) {
case 'top':
moveTop();
break;
case 'bottom':
moveBottom();
break;
case 'left':
moveLeft();
break;
case 'right':
moveRight();
break;
case 'topleft':
moveTop();
moveLeft();
break;
case 'topright':
moveTop();
moveRight();
break;
case 'bottomleft':
moveBottom();
moveLeft();
break;
case 'bottomright':
moveBottom();
moveRight();
break;
}
this.setBounds({ height, width, top, left });
}),
{
dispose: () => {
for (const iframe of iframes) {
iframe.style.pointerEvents = 'auto';
}
},
},
addDisposableWindowListener(window, 'mouseup', () => {
move.dispose();
this._onDidChangeEnd.fire();
})
);
})
);
}
override dispose(): void {
this._element.remove();
super.dispose();
}
}

View File

@ -6,7 +6,7 @@ import {
PanelTransfer,
} from '../../../dnd/dataTransfer';
import { toggleClass } from '../../../dom';
import { IDockviewComponent } from '../../dockviewComponent';
import { DockviewComponent } from '../../dockviewComponent';
import { DockviewDropTargets, ITabRenderer } from '../../types';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { DroptargetEvent, Droptarget } from '../../../dnd/droptarget';
@ -38,7 +38,7 @@ export class Tab extends CompositeDisposable implements ITab {
constructor(
public readonly panelId: string,
private readonly accessor: IDockviewComponent,
private readonly accessor: DockviewComponent,
private readonly group: DockviewGroupPanel
) {
super();
@ -79,13 +79,6 @@ export class Tab extends CompositeDisposable implements ITab {
if (event.defaultPrevented) {
return;
}
/**
* TODO: alternative to stopPropagation
*
* I need to stop the event propagation here since otherwise it'll be intercepted by event handlers
* on the tabs-container. I cannot use event.preventDefault() since I need the on DragStart event to occur
*/
event.stopPropagation();
this._onChanged.fire(event);
})

View File

@ -9,7 +9,7 @@ import { DockviewComponent } from '../../dockviewComponent';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { VoidContainer } from './voidContainer';
import { toggleClass } from '../../../dom';
import { IDockviewPanel } from '../../dockviewPanel';
import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel';
export interface TabDropIndexEvent {
readonly event: DragEvent;
@ -28,7 +28,8 @@ export interface ITabsContainer extends IDisposable {
isActive: (tab: ITab) => boolean;
closePanel: (panel: IDockviewPanel) => void;
openPanel: (panel: IDockviewPanel, index?: number) => void;
setActionElement(element: HTMLElement | undefined): void;
setRightActionsElement(element: HTMLElement | undefined): void;
setLeftActionsElement(element: HTMLElement | undefined): void;
hidden: boolean;
show(): void;
hide(): void;
@ -40,12 +41,14 @@ export class TabsContainer
{
private readonly _element: HTMLElement;
private readonly tabContainer: HTMLElement;
private readonly actionContainer: HTMLElement;
private readonly rightActionsContainer: HTMLElement;
private readonly leftActionsContainer: HTMLElement;
private readonly voidContainer: VoidContainer;
private tabs: IValueDisposable<ITab>[] = [];
private selectedIndex = -1;
private actions: HTMLElement | undefined;
private rightActions: HTMLElement | undefined;
private leftActions: HTMLElement | undefined;
private _hidden = false;
@ -79,17 +82,31 @@ export class TabsContainer
this._element.style.display = 'none';
}
setActionElement(element: HTMLElement | undefined): void {
if (this.actions === element) {
setRightActionsElement(element: HTMLElement | undefined): void {
if (this.rightActions === element) {
return;
}
if (this.actions) {
this.actions.remove();
this.actions = undefined;
if (this.rightActions) {
this.rightActions.remove();
this.rightActions = undefined;
}
if (element) {
this.actionContainer.appendChild(element);
this.actions = element;
this.rightActionsContainer.appendChild(element);
this.rightActions = element;
}
}
setLeftActionsElement(element: HTMLElement | undefined): void {
if (this.leftActions === element) {
return;
}
if (this.leftActions) {
this.leftActions.remove();
this.leftActions = undefined;
}
if (element) {
this.leftActionsContainer.appendChild(element);
this.leftActions = element;
}
}
@ -146,8 +163,11 @@ export class TabsContainer
})
);
this.actionContainer = document.createElement('div');
this.actionContainer.className = 'action-container';
this.rightActionsContainer = document.createElement('div');
this.rightActionsContainer.className = 'right-actions-container';
this.leftActionsContainer = document.createElement('div');
this.leftActionsContainer.className = 'left-actions-container';
this.tabContainer = document.createElement('div');
this.tabContainer.className = 'tabs-container';
@ -155,8 +175,9 @@ export class TabsContainer
this.voidContainer = new VoidContainer(this.accessor, this.group);
this._element.appendChild(this.tabContainer);
this._element.appendChild(this.leftActionsContainer);
this._element.appendChild(this.voidContainer.element);
this._element.appendChild(this.actionContainer);
this._element.appendChild(this.rightActionsContainer);
this.addDisposables(
this.voidContainer,
@ -166,6 +187,36 @@ export class TabsContainer
index: this.tabs.length,
});
}),
addDisposableListener(
this.voidContainer.element,
'mousedown',
(event) => {
const isFloatingGroupsEnabled =
!this.accessor.options.disableFloatingGroups;
if (
isFloatingGroupsEnabled &&
event.shiftKey &&
!this.group.api.isFloating
) {
event.preventDefault();
const { top, left } =
this.element.getBoundingClientRect();
const { top: rootTop, left: rootLeft } =
this.accessor.element.getBoundingClientRect();
this.accessor.addFloatingGroup(
this.group,
{
x: left - rootLeft + 20,
y: top - rootTop + 20,
},
{ inDragMode: true }
);
}
}
),
addDisposableListener(this.tabContainer, 'mousedown', (event) => {
if (event.defaultPrevented) {
return;
@ -242,6 +293,37 @@ export class TabsContainer
const disposable = CompositeDisposable.from(
tabToAdd.onChanged((event) => {
const isFloatingGroupsEnabled =
!this.accessor.options.disableFloatingGroups;
const isFloatingWithOnePanel =
this.group.api.isFloating && this.size === 1;
if (
isFloatingGroupsEnabled &&
!isFloatingWithOnePanel &&
event.shiftKey
) {
event.preventDefault();
const panel = this.accessor.getGroupPanel(tabToAdd.panelId);
const { top, left } =
tabToAdd.element.getBoundingClientRect();
const { top: rootTop, left: rootLeft } =
this.accessor.element.getBoundingClientRect();
this.accessor.addFloatingGroup(
panel as DockviewPanel,
{
x: left - rootLeft,
y: top - rootTop,
},
{ inDragMode: true }
);
return;
}
const alreadyFocused =
panel.id === this.group.model.activePanel?.id &&
this.group.model.isContentFocused;

View File

@ -1,7 +1,7 @@
import { GroupviewPanelState } from './types';
import { DockviewGroupPanel } from './dockviewGroupPanel';
import { DockviewPanel, IDockviewPanel } from './dockviewPanel';
import { IDockviewComponent } from './dockviewComponent';
import { DockviewComponent } from './dockviewComponent';
import { DockviewPanelModel } from './dockviewPanelModel';
import { DockviewApi } from '../api/component.api';
@ -21,7 +21,7 @@ interface LegacyState extends GroupviewPanelState {
}
export class DefaultDockviewDeserialzier implements IPanelDeserializer {
constructor(private readonly layout: IDockviewComponent) {}
constructor(private readonly layout: DockviewComponent) {}
public fromJSON(
panelData: GroupviewPanelState,

View File

@ -1,14 +1,15 @@
.dv-dockview {
position: relative;
background-color: var(--dv-group-view-background-color);
position: relative;
background-color: var(--dv-group-view-background-color);
.dv-watermark-container {
position: absolute;
top: 0px;
left: 0px;
height: 100%;
width: 100%;
}
.dv-watermark-container {
position: absolute;
top: 0px;
left: 0px;
height: 100%;
width: 100%;
z-index: 1;
}
}
.groupview {

View File

@ -5,7 +5,7 @@ import {
ISerializedLeafNode,
} from '../gridview/gridview';
import { directionToPosition, Droptarget, Position } from '../dnd/droptarget';
import { tail, sequenceEquals } from '../array';
import { tail, sequenceEquals, remove } from '../array';
import { DockviewPanel, IDockviewPanel } from './dockviewPanel';
import { CompositeDisposable } from '../lifecycle';
import { Event, Emitter } from '../events';
@ -41,16 +41,27 @@ import {
GroupPanelViewState,
GroupviewDropEvent,
} from './dockviewGroupPanelModel';
import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel';
import { DockviewGroupPanel } from './dockviewGroupPanel';
import { DockviewPanelModel } from './dockviewPanelModel';
import { getPanelData } from '../dnd/dataTransfer';
import { Parameters } from '../panel/types';
import { Overlay } from '../dnd/overlay';
import { toggleClass, watchElementResize } from '../dom';
import {
DockviewFloatingGroupPanel,
IDockviewFloatingGroupPanel,
} from './dockviewFloatingGroupPanel';
export interface PanelReference {
update: (event: { params: { [key: string]: any } }) => void;
remove: () => void;
}
export interface SerializedFloatingGroup {
data: GroupPanelViewState;
position: { height: number; width: number; left: number; top: number };
}
export interface SerializedDockview {
grid: {
root: SerializedGridObject<GroupPanelViewState>;
@ -58,8 +69,9 @@ export interface SerializedDockview {
width: number;
orientation: Orientation;
};
panels: { [key: string]: GroupviewPanelState };
panels: Record<string, GroupviewPanelState>;
activeGroup?: string;
floatingGroups?: SerializedFloatingGroup[];
}
export type DockviewComponentUpdateOptions = Pick<
@ -72,7 +84,9 @@ export type DockviewComponentUpdateOptions = Pick<
| 'showDndOverlay'
| 'watermarkFrameworkComponent'
| 'defaultTabComponent'
| 'createGroupControlElement'
| 'createLeftHeaderActionsElement'
| 'createRightHeaderActionsElement'
| 'disableFloatingGroups'
>;
export interface DockviewDropEvent extends GroupviewDropEvent {
@ -84,6 +98,7 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
readonly activePanel: IDockviewPanel | undefined;
readonly totalPanels: number;
readonly panels: IDockviewPanel[];
readonly floatingGroups: IDockviewFloatingGroupPanel[];
readonly onDidDrop: Event<DockviewDropEvent>;
readonly orientation: Orientation;
updateOptions(options: DockviewComponentUpdateOptions): void;
@ -104,7 +119,7 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
getGroupPanel: (id: string) => IDockviewPanel | undefined;
createWatermarkComponent(): IWatermarkRenderer;
// lifecycle
addGroup(options?: AddGroupOptions): IDockviewGroupPanel;
addGroup(options?: AddGroupOptions): DockviewGroupPanel;
closeAllGroups(): void;
// events
moveToNext(options?: MovementOptions): void;
@ -118,6 +133,10 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
readonly onDidAddPanel: Event<IDockviewPanel>;
readonly onDidLayoutFromJSON: Event<void>;
readonly onDidActivePanelChange: Event<IDockviewPanel | undefined>;
addFloatingGroup(
item: IDockviewPanel | DockviewGroupPanel,
coord?: { x: number; y: number }
): void;
}
export class DockviewComponent
@ -149,6 +168,8 @@ export class DockviewComponent
readonly onDidActivePanelChange: Event<IDockviewPanel | undefined> =
this._onDidActivePanelChange.event;
readonly floatingGroups: DockviewFloatingGroupPanel[] = [];
get orientation(): Orientation {
return this.gridview.orientation;
}
@ -183,7 +204,7 @@ export class DockviewComponent
parentElement: options.parentElement,
});
this.element.classList.add('dv-dockview');
toggleClass(this.gridview.element, 'dv-dockview', true);
this.addDisposables(
this._onDidDrop,
@ -231,10 +252,26 @@ export class DockviewComponent
if (data.viewId !== this.id) {
return false;
}
if (position === 'center') {
// center drop target is only allowed if there are no panels in the grid
// floating panels are allowed
return this.gridview.length === 0;
}
return true;
}
if (this.options.showDndOverlay) {
if (position === 'center' && this.gridview.length !== 0) {
/**
* for external events only show the four-corner drag overlays, disable
* the center position so that external drag events can fall through to the group
* and panel drop target handlers
*/
return false;
}
return this.options.showDndOverlay({
nativeEvent: event,
position: position,
@ -245,7 +282,7 @@ export class DockviewComponent
return false;
},
acceptedTargetZones: ['top', 'bottom', 'left', 'right'],
acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'],
overlayModel: {
activationSize: { type: 'pixels', value: 10 },
size: { type: 'pixels', value: 20 },
@ -280,6 +317,106 @@ export class DockviewComponent
this.updateWatermark();
}
addFloatingGroup(
item: DockviewPanel | DockviewGroupPanel,
coord?: { x?: number; y?: number; height?: number; width?: number },
options?: { skipRemoveGroup?: boolean; inDragMode: boolean }
): void {
let group: DockviewGroupPanel;
if (item instanceof DockviewPanel) {
group = this.createGroup();
this.removePanel(item, {
removeEmptyGroup: true,
skipDispose: true,
});
group.model.openPanel(item);
} else {
group = item;
const skip =
typeof options?.skipRemoveGroup === 'boolean' &&
options.skipRemoveGroup;
if (!skip) {
this.doRemoveGroup(item, { skipDispose: true });
}
}
group.model.isFloating = true;
const overlayLeft =
typeof coord?.x === 'number' ? Math.max(coord.x, 0) : 100;
const overlayTop =
typeof coord?.y === 'number' ? Math.max(coord.y, 0) : 100;
const overlay = new Overlay({
container: this.gridview.element,
content: group.element,
height: coord?.height ?? 300,
width: coord?.width ?? 300,
left: overlayLeft,
top: overlayTop,
minimumInViewportWidth: 100,
minimumInViewportHeight: 100,
});
const el = group.element.querySelector('.void-container');
if (!el) {
throw new Error('failed to find drag handle');
}
overlay.setupDrag(<HTMLElement>el, {
inDragMode:
typeof options?.inDragMode === 'boolean'
? options.inDragMode
: false,
});
const floatingGroupPanel = new DockviewFloatingGroupPanel(
group,
overlay
);
const disposable = watchElementResize(group.element, (entry) => {
const { width, height } = entry.contentRect;
group.layout(width, height); // let the group know it's size is changing so it can fire events to the panel
});
floatingGroupPanel.addDisposables(
overlay.onDidChange(() => {
// this is either a resize or a move
// to inform the panels .layout(...) the group with it's current size
// don't care about resize since the above watcher handles that
group.layout(group.height, group.width);
}),
overlay.onDidChangeEnd(() => {
this._bufferOnDidLayoutChange.fire();
}),
group.onDidChange((event) => {
overlay.setBounds({
height: event?.height,
width: event?.width,
});
}),
{
dispose: () => {
disposable.dispose();
group.model.isFloating = false;
remove(this.floatingGroups, floatingGroupPanel);
this.updateWatermark();
},
}
);
this.floatingGroups.push(floatingGroupPanel);
this.updateWatermark();
}
private orthogonalize(position: Position): DockviewGroupPanel {
switch (position) {
case 'top':
@ -305,6 +442,7 @@ export class DockviewComponent
switch (position) {
case 'top':
case 'left':
case 'center':
return this.createGroupAtLocation([0]); // insert into first position
case 'bottom':
case 'right':
@ -328,6 +466,21 @@ export class DockviewComponent
this.layout(this.gridview.width, this.gridview.height, true);
}
override layout(
width: number,
height: number,
forceResize?: boolean | undefined
): void {
super.layout(width, height, forceResize);
if (this.floatingGroups) {
for (const floating of this.floatingGroups) {
// ensure floting groups stay within visible boundaries
floating.overlay.setBounds();
}
}
}
focus(): void {
this.activeGroup?.focus();
}
@ -399,11 +552,26 @@ export class DockviewComponent
return collection;
}, {} as { [key: string]: GroupviewPanelState });
return {
const floats: SerializedFloatingGroup[] = this.floatingGroups.map(
(floatingGroup) => {
return {
data: floatingGroup.group.toJSON() as GroupPanelViewState,
position: floatingGroup.overlay.toJSON(),
};
}
);
const result: SerializedDockview = {
grid: data,
panels,
activeGroup: this.activeGroup?.id,
};
if (floats.length > 0) {
result.floatingGroups = floats;
}
return result;
}
fromJSON(data: SerializedDockview): void {
@ -415,47 +583,72 @@ export class DockviewComponent
throw new Error('root must be of type branch');
}
// take note of the existing dimensions
const width = this.width;
const height = this.height;
const createGroupFromSerializedState = (data: GroupPanelViewState) => {
const { id, locked, hideHeader, views, activeView } = data;
const group = this.createGroup({
id,
locked: !!locked,
hideHeader: !!hideHeader,
});
this._onDidAddGroup.fire(group);
for (const child of views) {
const panel = this._deserializer.fromJSON(panels[child], group);
const isActive =
typeof activeView === 'string' && activeView === panel.id;
group.model.openPanel(panel, {
skipSetPanelActive: !isActive,
skipSetGroupActive: true,
});
}
if (!group.activePanel && group.panels.length > 0) {
group.model.openPanel(group.panels[group.panels.length - 1], {
skipSetGroupActive: true,
});
}
return group;
};
this.gridview.deserialize(grid, {
fromJSON: (node: ISerializedLeafNode<GroupPanelViewState>) => {
const { id, locked, hideHeader, views, activeView } = node.data;
const group = this.createGroup({
id,
locked: !!locked,
hideHeader: !!hideHeader,
});
this._onDidAddGroup.fire(group);
for (const child of views) {
const panel = this._deserializer.fromJSON(
panels[child],
group
);
const isActive =
typeof activeView === 'string' &&
activeView === panel.id;
group.model.openPanel(panel, {
skipSetPanelActive: !isActive,
skipSetGroupActive: true,
});
}
if (!group.activePanel && group.panels.length > 0) {
group.model.openPanel(
group.panels[group.panels.length - 1],
{
skipSetGroupActive: true,
}
);
}
return group;
return createGroupFromSerializedState(node.data);
},
});
this.layout(width, height, true);
const serializedFloatingGroups = data.floatingGroups ?? [];
for (const serializedFloatingGroup of serializedFloatingGroups) {
const { data, position } = serializedFloatingGroup;
const group = createGroupFromSerializedState(data);
this.addFloatingGroup(
group,
{
x: position.left,
y: position.top,
height: position.height,
width: position.width,
},
{ skipRemoveGroup: true, inDragMode: false }
);
}
for (const floatingGroup of this.floatingGroups) {
floatingGroup.overlay.setBounds();
}
if (typeof activeGroup === 'string') {
const panel = this.getPanel(activeGroup);
if (panel) {
@ -463,8 +656,6 @@ export class DockviewComponent
}
}
this.gridview.layout(this.width, this.height);
this._onDidLayoutFromJSON.fire();
}
@ -476,7 +667,7 @@ export class DockviewComponent
for (const group of groups) {
// remove the group will automatically remove the panels
this.removeGroup(group, true);
this.removeGroup(group, { skipActive: true });
}
if (hasActiveGroup) {
@ -500,13 +691,19 @@ export class DockviewComponent
addPanel<T extends object = Parameters>(
options: AddPanelOptions<T>
): IDockviewPanel {
): DockviewPanel {
if (this.panels.find((_) => _.id === options.id)) {
throw new Error(`panel with id ${options.id} already exists`);
}
let referenceGroup: DockviewGroupPanel | undefined;
if (options.position && options.floating) {
throw new Error(
'you can only provide one of: position, floating as arguments to .addPanel(...)'
);
}
if (options.position) {
if (isPanelOptionsWithPanel(options.position)) {
const referencePanel =
@ -545,13 +742,29 @@ export class DockviewComponent
referenceGroup = this.activeGroup;
}
let panel: IDockviewPanel;
let panel: DockviewPanel;
if (referenceGroup) {
const target = toTarget(
<Direction>options.position?.direction || 'within'
);
if (target === 'center') {
if (options.floating) {
const group = this.createGroup();
panel = this.createPanel(options, group);
group.model.openPanel(panel);
const o =
typeof options.floating === 'object' &&
options.floating !== null
? options.floating
: {};
this.addFloatingGroup(group, o, {
inDragMode: false,
skipRemoveGroup: true,
});
} else if (referenceGroup.api.isFloating || target === 'center') {
panel = this.createPanel(options, referenceGroup);
referenceGroup.model.openPanel(panel);
} else {
@ -565,10 +778,26 @@ export class DockviewComponent
panel = this.createPanel(options, group);
group.model.openPanel(panel);
}
} else if (options.floating) {
const group = this.createGroup();
panel = this.createPanel(options, group);
group.model.openPanel(panel);
const o =
typeof options.floating === 'object' &&
options.floating !== null
? options.floating
: {};
this.addFloatingGroup(group, o, {
inDragMode: false,
skipRemoveGroup: true,
});
} else {
const group = this.createGroupAtLocation();
panel = this.createPanel(options, group);
group.model.openPanel(panel);
}
@ -592,7 +821,9 @@ export class DockviewComponent
group.model.removePanel(panel);
panel.dispose();
if (!options.skipDispose) {
panel.dispose();
}
if (group.size === 0 && options.removeEmptyGroup) {
this.removeGroup(group);
@ -614,7 +845,7 @@ export class DockviewComponent
}
private updateWatermark(): void {
if (this.groups.length === 0) {
if (this.groups.filter((x) => !x.api.isFloating).length === 0) {
if (!this.watermark) {
this.watermark = this.createWatermarkComponent();
@ -626,7 +857,7 @@ export class DockviewComponent
watermarkContainer.className = 'dv-watermark-container';
watermarkContainer.appendChild(this.watermark.element);
this.element.appendChild(watermarkContainer);
this.gridview.element.appendChild(watermarkContainer);
}
} else if (this.watermark) {
this.watermark.element.parentElement!.remove();
@ -696,17 +927,51 @@ export class DockviewComponent
}
}
removeGroup(group: DockviewGroupPanel, skipActive = false): void {
removeGroup(
group: DockviewGroupPanel,
options?:
| {
skipActive?: boolean;
skipDispose?: boolean;
}
| undefined
): void {
const panels = [...group.panels]; // reassign since group panels will mutate
for (const panel of panels) {
this.removePanel(panel, {
removeEmptyGroup: false,
skipDispose: false,
skipDispose: options?.skipDispose ?? false,
});
}
super.doRemoveGroup(group, { skipActive });
this.doRemoveGroup(group, options);
}
protected override doRemoveGroup(
group: DockviewGroupPanel,
options?:
| {
skipActive?: boolean;
skipDispose?: boolean;
}
| undefined
): DockviewGroupPanel {
const floatingGroup = this.floatingGroups.find(
(_) => _.group === group
);
if (floatingGroup) {
if (!options?.skipDispose) {
floatingGroup.group.dispose();
this._groups.delete(group.id);
}
floatingGroup.dispose();
return floatingGroup.group;
}
return super.doRemoveGroup(group, options);
}
moveGroupOrPanel(
@ -757,34 +1022,44 @@ export class DockviewComponent
if (sourceGroup && sourceGroup.size < 2) {
const [targetParentLocation, to] = tail(targetLocation);
const sourceLocation = getGridLocation(sourceGroup.element);
const [sourceParentLocation, from] = tail(sourceLocation);
if (
sequenceEquals(sourceParentLocation, targetParentLocation)
) {
// special case when 'swapping' two views within same grid location
// if a group has one tab - we are essentially moving the 'group'
// which is equivalent to swapping two views in this case
this.gridview.moveView(sourceParentLocation, from, to);
} else {
// source group will become empty so delete the group
const targetGroup = this.doRemoveGroup(sourceGroup, {
skipActive: true,
skipDispose: true,
});
const isFloating = this.floatingGroups.find(
(x) => x.group === sourceGroup
);
// after deleting the group we need to re-evaulate the ref location
const updatedReferenceLocation = getGridLocation(
destinationGroup.element
);
const location = getRelativeLocation(
this.gridview.orientation,
updatedReferenceLocation,
destinationTarget
);
this.doAddGroup(targetGroup, location);
if (!isFloating) {
const sourceLocation = getGridLocation(sourceGroup.element);
const [sourceParentLocation, from] = tail(sourceLocation);
if (
sequenceEquals(
sourceParentLocation,
targetParentLocation
)
) {
// special case when 'swapping' two views within same grid location
// if a group has one tab - we are essentially moving the 'group'
// which is equivalent to swapping two views in this case
this.gridview.moveView(sourceParentLocation, from, to);
}
}
// source group will become empty so delete the group
const targetGroup = this.doRemoveGroup(sourceGroup, {
skipActive: true,
skipDispose: true,
});
// after deleting the group we need to re-evaulate the ref location
const updatedReferenceLocation = getGridLocation(
destinationGroup.element
);
const location = getRelativeLocation(
this.gridview.orientation,
updatedReferenceLocation,
destinationTarget
);
this.doAddGroup(targetGroup, location);
} else {
const groupItem: IDockviewPanel | undefined =
sourceGroup?.model.removePanel(sourceItemId) ||
@ -828,7 +1103,17 @@ export class DockviewComponent
});
}
} else {
this.gridview.removeView(getGridLocation(sourceGroup.element));
const floatingGroup = this.floatingGroups.find(
(x) => x.group === sourceGroup
);
if (floatingGroup) {
floatingGroup.dispose();
} else {
this.gridview.removeView(
getGridLocation(sourceGroup.element)
);
}
const referenceLocation = getGridLocation(
referenceGroup.element
@ -921,7 +1206,7 @@ export class DockviewComponent
private createPanel(
options: AddPanelOptions,
group: DockviewGroupPanel
): IDockviewPanel {
): DockviewPanel {
const contentComponent = options.component;
const tabComponent =
options.tabComponent || this.options.defaultTabComponent;

View File

@ -0,0 +1,37 @@
import { Overlay } from '../dnd/overlay';
import { CompositeDisposable } from '../lifecycle';
import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel';
export interface IDockviewFloatingGroupPanel {
readonly group: IDockviewGroupPanel;
position(
bounds: Partial<{
top: number;
left: number;
height: number;
width: number;
}>
): void;
}
export class DockviewFloatingGroupPanel
extends CompositeDisposable
implements IDockviewFloatingGroupPanel
{
constructor(readonly group: DockviewGroupPanel, readonly overlay: Overlay) {
super();
this.addDisposables(overlay);
}
position(
bounds: Partial<{
top: number;
left: number;
height: number;
width: number;
}>
): void {
this.overlay.setBounds(bounds);
}
}

View File

@ -1,6 +1,5 @@
import { IFrameworkPart } from '../panel/types';
import { DockviewComponent } from '../dockview/dockviewComponent';
import { GridviewPanelApi } from '../api/gridviewPanelApi';
import {
DockviewGroupPanelModel,
GroupOptions,
@ -9,8 +8,13 @@ import {
} from './dockviewGroupPanelModel';
import { GridviewPanel, IGridviewPanel } from '../gridview/gridviewPanel';
import { IDockviewPanel } from '../dockview/dockviewPanel';
import {
DockviewGroupPanelApi,
DockviewGroupPanelApiImpl,
} from '../api/dockviewGroupPanelApi';
export interface IDockviewGroupPanel extends IGridviewPanel {
export interface IDockviewGroupPanel
extends IGridviewPanel<DockviewGroupPanelApi> {
model: IDockviewGroupPanelModel;
locked: boolean;
readonly size: number;
@ -20,13 +24,11 @@ export interface IDockviewGroupPanel extends IGridviewPanel {
export type IDockviewGroupPanelPublic = IDockviewGroupPanel;
export type DockviewGroupPanelApi = GridviewPanelApi;
export class DockviewGroupPanel
extends GridviewPanel
extends GridviewPanel<DockviewGroupPanelApiImpl>
implements IDockviewGroupPanel
{
private readonly _model: IDockviewGroupPanelModel;
private readonly _model: DockviewGroupPanelModel;
get panels(): IDockviewPanel[] {
return this._model.panels;
@ -40,7 +42,7 @@ export class DockviewGroupPanel
return this._model.size;
}
get model(): IDockviewGroupPanelModel {
get model(): DockviewGroupPanelModel {
return this._model;
}
@ -61,10 +63,17 @@ export class DockviewGroupPanel
id: string,
options: GroupOptions
) {
super(id, 'groupview_default', {
minimumHeight: 100,
minimumWidth: 100,
});
super(
id,
'groupview_default',
{
minimumHeight: 100,
minimumWidth: 100,
},
new DockviewGroupPanelApiImpl(id, accessor)
);
this.api.initialize(this); // cannot use 'this' after after 'super' call
this._model = new DockviewGroupPanelModel(
this.element,
@ -94,7 +103,6 @@ export class DockviewGroupPanel
}
toJSON(): any {
// TODO fix typing
return this.model.toJSON();
}
}

View File

@ -18,7 +18,7 @@ import {
import { DockviewDropTargets, IWatermarkRenderer } from './types';
import { DockviewGroupPanel } from './dockviewGroupPanel';
import { IDockviewPanel } from './dockviewPanel';
import { IGroupControlRenderer } from './options';
import { IHeaderActionsRenderer } from './options';
export interface DndService {
canDisplayOverlay(
@ -137,7 +137,9 @@ export class DockviewGroupPanelModel
private watermark?: IWatermarkRenderer;
private _isGroupActive = false;
private _locked = false;
private _control: IGroupControlRenderer | undefined;
private _isFloating = false;
private _rightHeaderActions: IHeaderActionsRenderer | undefined;
private _leftHeaderActions: IHeaderActionsRenderer | undefined;
private mostRecentlyUsed: IDockviewPanel[] = [];
@ -223,6 +225,24 @@ export class DockviewGroupPanelModel
);
}
get isFloating(): boolean {
return this._isFloating;
}
set isFloating(value: boolean) {
this._isFloating = value;
this.dropTarget.setTargetZones(
value ? ['center'] : ['top', 'bottom', 'left', 'right', 'center']
);
toggleClass(this.container, 'dv-groupview-floating', value);
this.groupPanel.api._onDidFloatingStateChange.fire({
isFloating: this.isFloating,
});
}
constructor(
private readonly container: HTMLElement,
private accessor: DockviewComponent,
@ -232,7 +252,7 @@ export class DockviewGroupPanelModel
) {
super();
this.container.classList.add('groupview');
toggleClass(this.container, 'groupview', true);
this.tabsContainer = new TabsContainer(this.accessor, this.groupPanel);
@ -247,6 +267,10 @@ export class DockviewGroupPanelModel
const data = getPanelData();
if (!data && event.shiftKey && !this.isFloating) {
return false;
}
if (data && data.viewId === this.accessor.id) {
if (data.groupId === this.id) {
if (position === 'center') {
@ -319,16 +343,34 @@ export class DockviewGroupPanelModel
this.setActive(this.isActive, true, true);
this.updateContainer();
if (this.accessor.options.createGroupControlElement) {
this._control = this.accessor.options.createGroupControlElement(
this.groupPanel
);
this.addDisposables(this._control);
this._control.init({
if (this.accessor.options.createRightHeaderActionsElement) {
this._rightHeaderActions =
this.accessor.options.createRightHeaderActionsElement(
this.groupPanel
);
this.addDisposables(this._rightHeaderActions);
this._rightHeaderActions.init({
containerApi: new DockviewApi(this.accessor),
api: this.groupPanel.api,
});
this.tabsContainer.setActionElement(this._control.element);
this.tabsContainer.setRightActionsElement(
this._rightHeaderActions.element
);
}
if (this.accessor.options.createLeftHeaderActionsElement) {
this._leftHeaderActions =
this.accessor.options.createLeftHeaderActionsElement(
this.groupPanel
);
this.addDisposables(this._leftHeaderActions);
this._leftHeaderActions.init({
containerApi: new DockviewApi(this.accessor),
api: this.groupPanel.api,
});
this.tabsContainer.setLeftActionsElement(
this._leftHeaderActions.element
);
}
}
@ -511,7 +553,7 @@ export class DockviewGroupPanelModel
}
updateActions(element: HTMLElement | undefined): void {
this.tabsContainer.setActionElement(element);
this.tabsContainer.setRightActionsElement(element);
}
public setActive(
@ -754,6 +796,7 @@ export class DockviewGroupPanelModel
public dispose(): void {
super.dispose();
this.watermark?.element.remove();
this.watermark?.dispose?.();
for (const panel of this.panels) {

View File

@ -8,7 +8,7 @@ import { DockviewGroupPanel } from './dockviewGroupPanel';
import { CompositeDisposable, IDisposable } from '../lifecycle';
import { IPanel, PanelUpdateEvent, Parameters } from '../panel/types';
import { IDockviewPanelModel } from './dockviewPanelModel';
import { IDockviewComponent } from './dockviewComponent';
import { DockviewComponent } from './dockviewComponent';
export interface IDockviewPanel extends IDisposable, IPanel {
readonly view: IDockviewPanelModel;
@ -47,7 +47,7 @@ export class DockviewPanel
constructor(
public readonly id: string,
accessor: IDockviewComponent,
accessor: DockviewComponent,
private readonly containerApi: DockviewApi,
group: DockviewGroupPanel,
readonly view: IDockviewPanelModel
@ -55,7 +55,7 @@ export class DockviewPanel
super();
this._group = group;
this.api = new DockviewPanelApiImpl(this, this._group);
this.api = new DockviewPanelApiImpl(this, this._group, accessor);
this.addDisposables(
this.api.onActiveChange(() => {

View File

@ -9,18 +9,16 @@ import {
DockviewDropTargets,
} from './types';
import { Parameters } from '../panel/types';
import {
DockviewGroupPanel,
DockviewGroupPanelApi,
} from './dockviewGroupPanel';
import { DockviewGroupPanel } from './dockviewGroupPanel';
import { ISplitviewStyles, Orientation } from '../splitview/splitview';
import { PanelTransfer } from '../dnd/dataTransfer';
import { IDisposable } from '../lifecycle';
import { Position } from '../dnd/droptarget';
import { IDockviewPanel } from './dockviewPanel';
import { FrameworkFactory } from '../panel/componentFactory';
import { DockviewGroupPanelApi } from '../api/dockviewGroupPanelApi';
export interface IGroupControlRenderer extends IDisposable {
export interface IHeaderActionsRenderer extends IDisposable {
readonly element: HTMLElement;
init(params: {
containerApi: DockviewApi;
@ -80,11 +78,15 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions {
styles?: ISplitviewStyles;
defaultTabComponent?: string;
showDndOverlay?: (event: DockviewDndOverlayEvent) => boolean;
createGroupControlElement?: (
createRightHeaderActionsElement?: (
group: DockviewGroupPanel
) => IGroupControlRenderer;
) => IHeaderActionsRenderer;
createLeftHeaderActionsElement?: (
group: DockviewGroupPanel
) => IHeaderActionsRenderer;
singleTabMode?: 'fullwidth' | 'default';
parentElement?: HTMLElement;
disableFloatingGroups?: boolean;
}
export interface PanelOptions<P extends object = Parameters> {
@ -132,12 +134,32 @@ export function isPanelOptionsWithGroup(
return false;
}
export interface AddPanelOptions<P extends object = Parameters>
extends Omit<PanelOptions<P>, 'component' | 'tabComponent'> {
type AddPanelFloatingGroupUnion = {
floating:
| {
height?: number;
width?: number;
x?: number;
y?: number;
}
| true;
position: never;
};
type AddPanelPositionUnion = {
floating: false | never;
position: AddPanelPositionOptions;
};
type AddPanelOptionsUnion = AddPanelFloatingGroupUnion | AddPanelPositionUnion;
export type AddPanelOptions<P extends object = Parameters> = Omit<
PanelOptions<P>,
'component' | 'tabComponent'
> & {
component: string;
tabComponent?: string;
position?: AddPanelPositionOptions;
}
} & Partial<AddPanelOptionsUnion>;
type AddGroupOptionsWithPanel = {
referencePanel: string | IDockviewPanel;

View File

@ -1,5 +1,5 @@
import {
Event,
Event as DockviewEvent,
Emitter,
addDisposableListener,
addDisposableWindowListener,
@ -87,8 +87,8 @@ export function getElementsByTagName(tag: string): HTMLElement[] {
}
export interface IFocusTracker extends IDisposable {
readonly onDidFocus: Event<void>;
readonly onDidBlur: Event<void>;
readonly onDidFocus: DockviewEvent<void>;
readonly onDidBlur: DockviewEvent<void>;
refreshState?(): void;
}
@ -101,10 +101,10 @@ export function trackFocus(element: HTMLElement | Window): IFocusTracker {
*/
class FocusTracker extends CompositeDisposable implements IFocusTracker {
private readonly _onDidFocus = new Emitter<void>();
public readonly onDidFocus: Event<void> = this._onDidFocus.event;
public readonly onDidFocus: DockviewEvent<void> = this._onDidFocus.event;
private readonly _onDidBlur = new Emitter<void>();
public readonly onDidBlur: Event<void> = this._onDidBlur.event;
public readonly onDidBlur: DockviewEvent<void> = this._onDidBlur.event;
private _refreshStateHandler: () => void;
@ -172,3 +172,16 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker {
this._refreshStateHandler();
}
}
// quasi: apparently, but not really; seemingly
const QUASI_PREVENT_DEFAULT_KEY = 'dv-quasiPreventDefault';
// mark an event directly for other listeners to check
export function quasiPreventDefault(event: Event): void {
(event as any)[QUASI_PREVENT_DEFAULT_KEY] = true;
}
// check if this event has been marked
export function quasiDefaultPrevented(event: Event): boolean {
return (event as any)[QUASI_PREVENT_DEFAULT_KEY];
}

View File

@ -74,7 +74,7 @@ export class Emitter<T> implements IDisposable {
static ENABLE_TRACKING = false;
static readonly MEMORY_LEAK_WATCHER = new LeakageMonitor();
static setLeakageMonitorEnabled(isEnabled: boolean) {
static setLeakageMonitorEnabled(isEnabled: boolean): void {
if (isEnabled !== Emitter.ENABLE_TRACKING) {
Emitter.MEMORY_LEAK_WATCHER.clear();
}

View File

@ -149,7 +149,7 @@ export class BranchNode extends CompositeDisposable implements IView {
: true,
};
}),
size: this.size,
size: this.orthogonalSize,
};
this.children = childDescriptors.map((c) => c.node);
@ -235,7 +235,7 @@ export class BranchNode extends CompositeDisposable implements IView {
this._size = orthogonalSize;
this._orthogonalSize = size;
this.splitview.layout(this.size, this.orthogonalSize);
this.splitview.layout(orthogonalSize, size);
}
public addChild(

View File

@ -371,8 +371,7 @@ export class Gridview implements IDisposable {
root,
orientation,
deserializer,
orthogonalSize,
true
orthogonalSize
) as BranchNode;
}
@ -380,8 +379,7 @@ export class Gridview implements IDisposable {
node: ISerializedNode,
orientation: Orientation,
deserializer: IViewDeserializer,
orthogonalSize: number,
isRoot = false
orthogonalSize: number
): Node {
let result: Node;
if (node.type === 'branch') {
@ -398,14 +396,13 @@ export class Gridview implements IDisposable {
} as INodeDescriptor;
});
// HORIZONTAL => height=orthogonalsize width=size
// VERTICAL => height=size width=orthogonalsize
result = new BranchNode(
orientation,
this.proportionalLayout,
this.styles,
isRoot ? orthogonalSize : node.size,
isRoot ? node.size : orthogonalSize,
node.size, // <- orthogonal size - flips at each depth
orthogonalSize, // <- size - flips at each depth
children
);
} else {
@ -459,7 +456,9 @@ export class Gridview implements IDisposable {
this.root.size
);
if (oldRoot.children.length === 1) {
if (oldRoot.children.length === 0) {
// no data so no need to add anything back in
} else if (oldRoot.children.length === 1) {
// can remove one level of redundant branching if there is only a single child
const childReference = oldRoot.children[0];
const child = oldRoot.removeChild(0); // remove to prevent disposal when disposing of unwanted root

View File

@ -179,6 +179,10 @@ export class GridviewComponent
const queue: Function[] = [];
// take note of the existing dimensions
const width = this.width;
const height = this.height;
this.gridview.deserialize(grid, {
fromJSON: (node) => {
const { data } = node;
@ -218,7 +222,7 @@ export class GridviewComponent
},
});
this.layout(this.width, this.height, true);
this.layout(width, height, true);
queue.forEach((f) => f());

View File

@ -28,8 +28,8 @@ export interface GridviewInitParameters extends PanelInitParameters {
isVisible?: boolean;
}
export interface IGridviewPanel
extends BasePanelViewExported<GridviewPanelApi> {
export interface IGridviewPanel<T extends GridviewPanelApi = GridviewPanelApi>
extends BasePanelViewExported<T> {
readonly minimumWidth: number;
readonly maximumWidth: number;
readonly minimumHeight: number;
@ -38,8 +38,10 @@ export interface IGridviewPanel
readonly snap: boolean;
}
export abstract class GridviewPanel
extends BasePanelView<GridviewPanelApiImpl>
export abstract class GridviewPanel<
T extends GridviewPanelApiImpl = GridviewPanelApiImpl
>
extends BasePanelView<T>
implements IGridPanelComponentView, IGridviewPanel
{
private _evaluatedMinimumWidth = 0;
@ -134,9 +136,10 @@ export abstract class GridviewPanel
maximumWidth?: number;
minimumHeight?: number;
maximumHeight?: number;
}
},
api?: T
) {
super(id, component, new GridviewPanelApiImpl(id));
super(id, component, api ?? <T>new GridviewPanelApiImpl(id));
if (typeof options?.minimumWidth === 'number') {
this._minimumWidth = options.minimumWidth;

View File

@ -1,7 +1,5 @@
export * from './dnd/dataTransfer';
export { watchElementResize } from './dom';
/**
* Events, Emitters and Disposables are very common concepts that most codebases will contain.
* We export them with a 'Dockview' prefix here to prevent accidental use by others.
@ -71,6 +69,10 @@ export {
SplitviewPanelApi,
} from './api/splitviewPanelApi';
export { ExpansionEvent, PaneviewPanelApi } from './api/paneviewPanelApi';
export {
DockviewGroupPanelApi,
DockviewGroupPanelFloatingChangeEvent,
} from './api/dockviewGroupPanelApi';
export {
CommonApi,
SplitviewApi,

View File

@ -5,7 +5,7 @@ export const clamp = (value: number, min: number, max: number): number => {
return Math.min(max, Math.max(value, min));
};
export const sequentialNumberGenerator = () => {
export const sequentialNumberGenerator = (): { next: () => string } => {
let value = 1;
return { next: () => (value++).toString() };
};

View File

@ -363,6 +363,10 @@ export class PaneviewComponent extends Resizable implements IPaneviewComponent {
const queue: Function[] = [];
// take note of the existing dimensions
const width = this.width;
const height = this.height;
this.paneview = new Paneview(this.element, {
orientation: Orientation.VERTICAL,
descriptor: {
@ -440,7 +444,7 @@ export class PaneviewComponent extends Resizable implements IPaneviewComponent {
},
});
this.layout(this.width, this.height);
this.layout(width, height);
queue.forEach((f) => f());

View File

@ -1,3 +1,24 @@
.dv-debug {
.split-view-container {
.sash-container {
.sash {
&.enabled {
background-color: black;
}
&.disabled {
background-color: orange;
}
&.maximum {
background-color: green;
}
&.minimum {
background-color: red;
}
}
}
}
}
.split-view-container {
position: relative;
overflow: hidden;
@ -12,22 +33,6 @@
}
}
// debug
// .sash {
// &.enabled {
// background-color: black;
// }
// &.disabled {
// background-color: orange;
// }
// &.maximum {
// background-color: green;
// }
// &.minimum {
// background-color: red;
// }
// }
&.horizontal {
height: 100%;
@ -106,6 +111,7 @@
-webkit-user-select: none; // Safari
-moz-user-select: none; // Firefox
-ms-user-select: none; // IE 10 and IE 11
touch-action: none;
&:active {
transition: background-color 0.1s ease-in-out;

View File

@ -393,17 +393,7 @@ export class Splitview {
const sash = document.createElement('div');
sash.className = 'sash';
const onTouchStart = (event: TouchEvent) => {
event.preventDefault();
const touch = event.touches[0];
onStart(touch);
};
const onMouseDown = (event: MouseEvent) => {
onStart(event);
};
const onStart = (event: { clientX: number; clientY: number }) => {
const onPointerStart = (event: PointerEvent) => {
for (const item of this.viewItems) {
item.enabled = false;
}
@ -497,20 +487,7 @@ export class Splitview {
};
}
const onMouseMove = (event: MouseEvent) => {
reposition(event);
};
const onTouchMove = (event: TouchEvent) => {
event.preventDefault();
const touch = event.touches[0];
reposition(touch);
};
const reposition = (event: {
clientX: number;
clientY: number;
}) => {
const onPointerMove = (event: PointerEvent) => {
const current =
this._orientation === Orientation.HORIZONTAL
? event.clientX
@ -543,30 +520,24 @@ export class Splitview {
this.saveProportions();
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', end);
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', end);
document.removeEventListener('touchcancel', end);
document.removeEventListener('pointermove', onPointerMove);
document.removeEventListener('pointerup', end);
document.removeEventListener('pointercancel', end);
this._onDidSashEnd.fire(undefined);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', end);
document.addEventListener('touchmove', onTouchMove);
document.addEventListener('touchend', end);
document.addEventListener('touchcancel', end);
document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerup', end);
document.addEventListener('pointercancel', end);
};
sash.addEventListener('mousedown', onMouseDown);
sash.addEventListener('touchstart', onTouchStart);
sash.addEventListener('pointerdown', onPointerStart);
const sashItem: ISashItem = {
container: sash,
disposable: () => {
sash.removeEventListener('mousedown', onStart);
sash.removeEventListener('touchstart', onTouchStart);
sash.removeEventListener('pointerdown', onPointerStart);
this.sashContainer.removeChild(sash);
},
};

View File

@ -342,6 +342,10 @@ export class SplitviewComponent
const queue: Function[] = [];
// take note of the existing dimensions
const width = this.width;
const height = this.height;
this.splitview = new Splitview(this.element, {
orientation,
proportionalLayout: this.options.proportionalLayout,
@ -392,7 +396,7 @@ export class SplitviewComponent
},
});
this.layout(this.width, this.height);
this.layout(width, height);
queue.forEach((f) => f());

View File

@ -7,6 +7,7 @@
--dv-drag-over-border-color: white;
--dv-tabs-container-scrollbar-color: #888;
--dv-icon-hover-background-color: rgba(90, 93, 94, 0.31);
--dv-floating-box-shadow: 8px 8px 8px 0px rgba(83, 89, 93, 0.5);
}
@mixin dockview-theme-dark-mixin {
@ -225,3 +226,124 @@
.dockview-theme-dracula {
@include dockview-theme-dracula-mixin();
}
@mixin dockview-design-replit-mixin {
&.dv-dockview {
padding: 3px;
}
.view:has(> .groupview) {
padding: 3px;
}
.dv-resize-container:has(> .groupview) {
border-radius: 8px;
}
.groupview {
overflow: hidden;
border-radius: 10px;
.tabs-and-actions-container {
.tab {
margin: 4px;
border-radius: 8px;
.dockview-svg {
height: 8px;
width: 8px;
}
&:hover {
background-color: #e4e5e6 !important;
}
}
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
}
.content-container {
background-color: #fcfcfc;
}
&.active-group {
border: 1px solid rgba(128, 128, 128, 0.35);
}
&.inactive-group {
border: 1px solid transparent;
}
}
.vertical > .sash-container > .sash {
&::after {
content: '';
height: 4px;
width: 40px;
border-radius: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--dv-separator-handle-background-color);
position: absolute;
}
&:hover {
&::after {
background-color: var(
--dv-separator-handle-hover-background-color
);
}
}
}
.horizontal > .sash-container > .sash {
&::after {
content: '';
height: 40px;
width: 4px;
border-radius: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--dv-separator-handle-background-color);
position: absolute;
}
&:hover {
&::after {
background-color: var(
--dv-separator-handle-hover-background-color
);
}
}
}
}
.dockview-theme-replit {
@include dockview-theme-core-mixin();
@include dockview-design-replit-mixin();
//
--dv-group-view-background-color: #ebeced;
//
--dv-tabs-and-actions-container-background-color: #fcfcfc;
//
--dv-activegroup-visiblepanel-tab-background-color: #f0f1f2;
--dv-activegroup-hiddenpanel-tab-background-color: ##fcfcfc;
--dv-inactivegroup-visiblepanel-tab-background-color: #f0f1f2;
--dv-inactivegroup-hiddenpanel-tab-background-color: #fcfcfc;
--dv-tab-divider-color: transparent;
//
--dv-activegroup-visiblepanel-tab-color: rgb(51, 51, 51);
--dv-activegroup-hiddenpanel-tab-color: rgb(51, 51, 51);
--dv-inactivegroup-visiblepanel-tab-color: rgb(51, 51, 51);
--dv-inactivegroup-hiddenpanel-tab-color: rgb(51, 51, 51);
//
--dv-separator-border: transparent;
--dv-paneview-header-border-color: rgb(51, 51, 51);
--dv-background-color: #ebeced;
/////
--dv-separator-handle-background-color: #cfd1d3;
--dv-separator-handle-hover-background-color: #babbbb;
}

View File

@ -1,7 +1,7 @@
<div align="center">
<h1>dockview</h1>
<p>Zero dependency layout manager supporting tabs, grids and splitviews with ReactJS support written in TypeScript</p>
<p>Zero dependency layout manager supporting tabs, groups, grids and splitviews with ReactJS support written in TypeScript</p>
</div>
@ -25,6 +25,7 @@ Please see the website: https://dockview.dev
- Themable and customizable
- Serialization / deserialization support
- Tabular docking and Drag and Drop support
- Floating groups, customized header bars and tab
- Documentation and examples
Want to inspect the latest deployment? Go to https://unpkg.com/browse/dockview@latest/

View File

@ -1,6 +1,6 @@
{
"name": "dockview",
"version": "1.7.5",
"version": "1.8.2",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews with ReactJS support",
"main": "./dist/cjs/index.js",
"types": "./dist/cjs/index.d.ts",
@ -14,12 +14,12 @@
},
"homepage": "https://github.com/mathuo/dockview",
"scripts": {
"build:ci": "npm run build:cjs && npm run build:esm && npm run build:css",
"build:package": "npm run build:cjs && npm run build:esm && npm run build:css",
"build:cjs": "cross-env ../../node_modules/.bin/tsc --project ./tsconfig.json --extendedDiagnostics",
"build:css": "gulp sass",
"build:esm": "cross-env ../../node_modules/.bin/tsc --project ./tsconfig.esm.json --extendedDiagnostics",
"build:modulefiles": "rollup -c",
"build": "npm run build:ci && npm run build:modulefiles",
"build:bundles": "rollup -c",
"build": "npm run build:package && npm run build:bundles",
"clean": "rimraf dist/ .build/ .rollup.cache/",
"docs": "typedoc",
"prepublishOnly": "npm run rebuild && npm run test",
@ -56,7 +56,7 @@
"author": "https://github.com/mathuo",
"license": "MIT",
"dependencies": {
"dockview-core": "^1.7.5"
"dockview-core": "^1.8.2"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.1",

View File

@ -47,6 +47,7 @@ function createBundle(format, options) {
const output = {
file,
format,
sourcemap: true,
globals: {},
banner: [
`/**`,
@ -64,9 +65,6 @@ function createBundle(format, options) {
}),
typescript({
tsconfig: 'tsconfig.esm.json',
compilerOptions: {
declaration: false,
},
}),
];

View File

@ -3,9 +3,9 @@ import {
DockviewGroupPanelApi,
DockviewGroupPanelModel,
} from 'dockview-core';
import { ReactGroupControlsRendererPart } from '../../dockview/groupControlsRenderer';
import { ReactHeaderActionsRendererPart } from '../../dockview/headerActionsRenderer';
describe('groupControlsRenderer', () => {
describe('headerActionsRenderer', () => {
test('#1', () => {
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
@ -28,7 +28,7 @@ describe('groupControlsRenderer', () => {
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new ReactGroupControlsRendererPart(
const cut = new ReactHeaderActionsRendererPart(
jest.fn(),
{
addPortal: jest.fn(),

View File

@ -4,12 +4,12 @@ import {
DockviewDropEvent,
DockviewDndOverlayEvent,
GroupPanelFrameworkComponentFactory,
IGroupControlRenderer,
DockviewPanelApi,
DockviewApi,
IContentRenderer,
ITabRenderer,
DockviewGroupPanel,
IHeaderActionsRenderer,
} from 'dockview-core';
import { ReactPanelContentPart } from './reactContentPart';
import { ReactPanelHeaderPart } from './reactHeaderPart';
@ -18,17 +18,17 @@ import { ReactPortalStore, usePortalsLifecycle } from '../react';
import { IWatermarkPanelProps, ReactWatermarkPart } from './reactWatermarkPart';
import { PanelCollection, PanelParameters } from '../types';
import {
IDockviewGroupControlProps,
ReactGroupControlsRendererPart,
} from './groupControlsRenderer';
IDockviewHeaderActionsProps,
ReactHeaderActionsRendererPart,
} from './headerActionsRenderer';
function createGroupControlElement(
component: React.FunctionComponent<IDockviewGroupControlProps> | undefined,
component: React.FunctionComponent<IDockviewHeaderActionsProps> | undefined,
store: ReactPortalStore
): ((groupPanel: DockviewGroupPanel) => IGroupControlRenderer) | undefined {
): ((groupPanel: DockviewGroupPanel) => IHeaderActionsRenderer) | undefined {
return component
? (groupPanel: DockviewGroupPanel) => {
return new ReactGroupControlsRendererPart(
return new ReactHeaderActionsRendererPart(
component,
store,
groupPanel
@ -65,8 +65,10 @@ export interface IDockviewReactProps {
className?: string;
disableAutoResizing?: boolean;
defaultTabComponent?: React.FunctionComponent<IDockviewPanelHeaderProps>;
groupControlComponent?: React.FunctionComponent<IDockviewGroupControlProps>;
rightHeaderActionsComponent?: React.FunctionComponent<IDockviewHeaderActionsProps>;
leftHeaderActionsComponent?: React.FunctionComponent<IDockviewHeaderActionsProps>;
singleTabMode?: 'fullwidth' | 'default';
disableFloatingGroups?: boolean;
}
const DEFAULT_REACT_TAB = 'props.defaultTabComponent';
@ -150,11 +152,16 @@ export const DockviewReact = React.forwardRef(
? { separatorBorder: 'transparent' }
: undefined,
showDndOverlay: props.showDndOverlay,
createGroupControlElement: createGroupControlElement(
props.groupControlComponent,
createLeftHeaderActionsElement: createGroupControlElement(
props.leftHeaderActionsComponent,
{ addPortal }
),
createRightHeaderActionsElement: createGroupControlElement(
props.rightHeaderActionsComponent,
{ addPortal }
),
singleTabMode: props.singleTabMode,
disableFloatingGroups: props.disableFloatingGroups,
});
const { clientWidth, clientHeight } = domRef.current;
@ -225,6 +232,15 @@ export const DockviewReact = React.forwardRef(
});
}, [props.tabComponents]);
React.useEffect(() => {
if (!dockviewRef.current) {
return;
}
dockviewRef.current.updateOptions({
disableFloatingGroups: props.disableFloatingGroups,
});
}, [props.disableFloatingGroups]);
React.useEffect(() => {
if (!dockviewRef.current) {
return;
@ -250,12 +266,24 @@ export const DockviewReact = React.forwardRef(
return;
}
dockviewRef.current.updateOptions({
createGroupControlElement: createGroupControlElement(
props.groupControlComponent,
createRightHeaderActionsElement: createGroupControlElement(
props.rightHeaderActionsComponent,
{ addPortal }
),
});
}, [props.groupControlComponent]);
}, [props.rightHeaderActionsComponent]);
React.useEffect(() => {
if (!dockviewRef.current) {
return;
}
dockviewRef.current.updateOptions({
createLeftHeaderActionsElement: createGroupControlElement(
props.leftHeaderActionsComponent,
{ addPortal }
),
});
}, [props.leftHeaderActionsComponent]);
return (
<div

View File

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

View File

@ -4,7 +4,7 @@ export * from './dockview/dockview';
export * from './dockview/defaultTab';
export * from './splitview/splitview';
export * from './gridview/gridview';
export { IDockviewGroupControlProps } from './dockview/groupControlsRenderer';
export { IDockviewHeaderActionsProps } from './dockview/headerActionsRenderer';
export { IWatermarkPanelProps } from './dockview/reactWatermarkPart';
export * from './paneview/paneview';
export * from './types';

View File

@ -20,7 +20,7 @@ import Link from '@docusaurus/Link';
- Provide a default React tab implementation to allow for simple changes to tab renderer without rewritting the entire tab
- Override the default tab in `ReactDockview` with the `defaultTabComponent` prop
- Group controls renderer [#138](https://github.com/mathuo/dockview/pull/138)
- Provide the `groupControlComponent` prop in `ReactDockview` to create custom control components for groups. <Link to="../../docs/components/dockview/#group-controls-panel">Go</Link>
- Provide the `groupControlComponent` prop in `ReactDockview` to create custom control components for groups.
## 🛠 Miscs

View File

@ -0,0 +1,20 @@
---
slug: dockview-1.7.6-release
title: Dockview 1.7.6
tags: [release]
---
# Release Notes
Please reference to docs @ [dockview.dev](https://dockview.dev).
## 🚀 Features
- Touch support for resize handles [#278](https://github.com/mathuo/dockview/pull/278)
## 🛠 Miscs
- Internal cleanup [#275](https://github.com/mathuo/dockview/pull/275)
- iframe docs [#273](https://github.com/mathuo/dockview/pull/273)
## 🔥 Breaking changes

View File

@ -0,0 +1,23 @@
---
slug: dockview-1.8.0-release
title: Dockview 1.8.0
tags: [release]
---
# Release Notes
Please reference to docs @ [dockview.dev](https://dockview.dev).
## 🚀 Features
- Support for Floating Groups [#262](https://github.com/mathuo/dockview/pull/262)
- Left hand header changes [#264](https://github.com/mathuo/dockview/pull/264)
- Retain layout size [#285](https://github.com/mathuo/dockview/pull/285)
- Expose `removePanel` [#293](https://github.com/mathuo/dockview/issues/293)
- Additional themes
## 🛠 Miscs
## 🔥 Breaking changes
- `groupControlComponent` renamed to `rightHeaderActionsComponent` [#264](https://github.com/mathuo/dockview/pull/264)

View File

@ -0,0 +1,17 @@
---
slug: dockview-1.8.2-release
title: Dockview 1.8.2
tags: [release]
---
# Release Notes
Please reference to docs @ [dockview.dev](https://dockview.dev).
## 🚀 Features
## 🛠 Miscs
- Fix regression related to external dnd events [#311](https://github.com/mathuo/dockview/issues/311)
## 🔥 Breaking changes

View File

@ -2,6 +2,7 @@
"label": "Components",
"collapsible": true,
"collapsed": false,
"position": 2,
"link": {
"type": "generated-index",
"title": "Components"

View File

@ -2,10 +2,7 @@
description: Dockview Documentation
---
import {
Container,
MultiFrameworkContainer,
} from '@site/src/components/ui/container';
import { MultiFrameworkContainer } from '@site/src/components/ui/container';
import Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
@ -18,7 +15,7 @@ import DockviewConstraints from '@site/sandboxes/constraints-dockview/src/app';
import DndDockview from '@site/sandboxes/dnd-dockview/src/app';
import NestedDockview from '@site/sandboxes/nested-dockview/src/app';
import EventsDockview from '@site/sandboxes/events-dockview/src/app';
import DockviewGroupControl from '@site/sandboxes/groupcontrol-dockview/src/app';
import DockviewGroupControl from '@site/sandboxes/headeractions-dockview/src/app';
import CustomHeadersDockview from '@site/sandboxes/customheader-dockview/src/app';
import DockviewNative from '@site/sandboxes/fullwidthtab-dockview/src/app';
import DockviewNative2 from '@site/sandboxes/nativeapp-dockview/src/app';
@ -28,6 +25,7 @@ import DockviewExternalDnd from '@site/sandboxes/externaldnd-dockview/src/app';
import DockviewResizeContainer from '@site/sandboxes/resizecontainer-dockview/src/app';
import DockviewTabheight from '@site/sandboxes/tabheight-dockview/src/app';
import DockviewWithIFrames from '@site/sandboxes/iframe-dockview/src/app';
import DockviewFloating from '@site/sandboxes/floatinggroup-dockview/src/app';
import { attach as attachDockviewVanilla } from '@site/sandboxes/javascript/vanilla-dockview/src/app';
import { attach as attachSimpleDockview } from '@site/sandboxes/javascript/simple-dockview/src/app';
@ -59,20 +57,21 @@ You can create a Dockview through the use of the `DockviewReact` component.
import { DockviewReact } from 'dockview';
```
| Property | Type | Optional | Default | Description |
| --------------------- | ------------------------------------ | -------- | --------- | ------------------------------------------------------------ |
| onReady | (event: SplitviewReadyEvent) => void | No | | |
| components | object | No | | |
| tabComponents | object | Yes | | |
| watermarkComponent | object | Yes | | |
| hideBorders | boolean | Yes | false | |
| className | string | Yes | '' | |
| disableAutoResizing | boolean | Yes | false | See <Link to="../basics/#auto-resizing">Auto Resizing</Link> |
| onDidDrop | Event | Yes | false | |
| showDndOverlay | Event | Yes | false | |
| defaultTabComponent | object | Yes | | |
| groupControlComponent | object | Yes | | |
| singleTabMode | 'fullwidth' \| 'default' | Yes | 'default' | |
| Property | Type | Optional | Default | Description |
| --------------------------- | ------------------------------------ | -------- | --------- | ----------- |
| onReady | (event: SplitviewReadyEvent) => void | No | | |
| components | object | No | | |
| tabComponents | object | Yes | | |
| watermarkComponent | object | Yes | | |
| hideBorders | boolean | Yes | false | |
| className | string | Yes | '' | |
| disableAutoResizing | boolean | Yes | false | |
| onDidDrop | Event | Yes | false | |
| showDndOverlay | Event | Yes | false | |
| defaultTabComponent | object | Yes | | |
| leftHeaderActionsComponent | object | Yes | | |
| rightHeaderActionsComponent | object | Yes | | |
| singleTabMode | 'fullwidth' \| 'default' | Yes | 'default' | |
## Dockview API
@ -93,44 +92,44 @@ const onReady = (event: DockviewReadyEvent) => {
};
```
| 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` | |
| | | |
| 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 |
| 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` | |
| | | |
| updateOptions | `(options:SplitviewComponentUpdateOptions): void` | |
| focus | `(): void` | |
| layout | `(width: number, height:number): void` | |
| fromJSON | `(data: SerializedDockview): void` | |
| toJSON | `(): SerializedDockview` | |
| clear | `(): void` | Clears the current layout |
## Dockview Panel API
@ -166,6 +165,25 @@ const MyComponent = (props: IDockviewPanelProps<{ title: string }>) => {
| close | `(): void` | |
| setTitle | `(title: string): void` | |
## Theme
As well as importing the `dockview` stylesheet you must provide a class-based theme somewhere in your application. For example.
```tsx
// Providing a theme directly through the DockviewReact component props
<DockviewReact className="dockview-theme-dark" />
// Providing a theme somewhere in the DOM tree
<div className="dockview-theme-dark">
<div>
{/**... */}
<DockviewReact />
</div>
</div>
```
You can find more details on theming <Link to="../theme">here</Link>.
## Layout Persistance
Layouts are loaded and saved via to `fromJSON` and `toJSON` methods on the Dockview api.
@ -218,9 +236,10 @@ const onReady = (event: DockviewReadyEvent) => {
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.
<Container sandboxId="layout-dockview">
<DockviewPersistance />
</Container>
<MultiFrameworkContainer
sandboxId="layout-dockview"
react={DockviewPersistance}
/>
## Resizing
@ -249,17 +268,16 @@ props.api.group.api.setSize({
You can see an example invoking both approaches below.
<Container sandboxId="resize-dockview">
<ResizeDockview />
</Container>
<MultiFrameworkContainer sandboxId="resize-dockview" react={ResizeDockview} />
### Container Resizing
The component will automatically resize to it's container.
<Container sandboxId="resizecontainer-dockview">
<DockviewResizeContainer />
</Container>
<MultiFrameworkContainer
sandboxId="resizecontainer-dockview"
react={DockviewResizeContainer}
/>
## Watermark
@ -267,9 +285,10 @@ When the dockview is empty you may want to display some fallback content, this i
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.
<Container sandboxId="watermark-dockview">
<DockviewWatermark />
</Container>
<MultiFrameworkContainer
sandboxId="watermark-dockview"
react={DockviewWatermark}
/>
## Drag And Drop
@ -346,9 +365,7 @@ return (
);
```
<Container sandboxId="dnd-dockview">
<DndDockview />
</Container>
<MultiFrameworkContainer sandboxId="dnd-dockview" react={DndDockview} />
### Third Party Dnd Libraries
@ -356,9 +373,39 @@ This shows a simple example of a third-party library used inside a panel that re
and drop functionalities. This examples serves to show that `dockview` doesn't interfer with
any drag and drop logic for other controls.
<Container>
<DockviewExternalDnd />
</Container>
<MultiFrameworkContainer
sandboxId="externaldnd-dockview"
react={DockviewExternalDnd}
/>
## Floating Groups
Dockview has built-in support for floating groups. Each floating container can contain a single group with many panels
and you can have as many floating containers as needed. You cannot dock multiple groups together in the same floating container.
Floating groups can be interacted with whilst holding the `shift` key activating the `event.shiftKey` boolean property on `KeyboardEvent` events.
> Float an existing tab by holding `shift` whilst interacting with the tab
<img style={{ width: '60%' }} src={useBaseUrl('/img/float_add.svg')} />
> Move a floating tab by holding `shift` whilst moving the cursor or dragging the empty
> header space
<img style={{ width: '60%' }} src={useBaseUrl('/img/float_move.svg')} />
> Move an entire floating group by holding `shift` whilst dragging the empty header space
<img style={{ width: '60%' }} src={useBaseUrl('/img/float_group.svg')} />
Floating groups can be programatically added through the dockview `api` method `api.addFloatingGroup(...)` and you can check whether
a group is floating via the `group.api.isFloating` property. See examples for full code.
<MultiFrameworkContainer
height={600}
sandboxId="floatinggroup-dockview"
react={DockviewFloating}
/>
## Panels
@ -436,6 +483,23 @@ const panel2 = api.addPanel({
});
```
To add a floating panel you should include the `floating` variable which can be either a `boolean` or an object defining it's bounds.
These bounds are relative to the dockview component.
```ts
const panel1 = api.addPanel({
id: 'panel_2',
component: 'default',
floating: true,
});
const panel2 = api.addPanel({
id: 'panel_2',
component: 'default',
floating: { x: 10, y: 10, width: 300, height: 300 },
});
```
### Update Panel
You can programatically update the `params` passed through to the panel through the panal api using `api.updateParameters`.
@ -470,6 +534,36 @@ panel.api.updateParameters({
});
```
### Move panel
You can programatically move a panel using the panel `api`.
```ts
panel.api.moveTo({ group, position, index });
```
An equivalent method for moving groups is avaliable on the group `api`.
```ts
const group = panel.api.group;
group.api.moveTo({ group, position });
```
### Remove panel
You can programatically remove a panel using the panel `api`.
```ts
panel.api.close();
```
Given a reference to the panel you can also use the component `api` to remove it.
```ts
const panel = api.getPanel('myPanel');
api.removePanel(panel);
```
### Panel Rendering
By default `DockviewReact` only adds to the DOM those panels that are visible,
@ -524,9 +618,10 @@ 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.
<Container sandboxId="rendering-dockview">
<RenderingDockview renderVisibleOnly={false} />
</Container>
<MultiFrameworkContainer
sandboxId="rendering-dockview"
react={RenderingDockview}
/>
## Headers
@ -580,9 +675,10 @@ As a simple example the below attaches a custom event handler for the context me
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.
<Container sandboxId="customheader-dockview">
<CustomHeadersDockview />
</Container>
<MultiFrameworkContainer
sandboxId="customheader-dockview"
react={CustomHeadersDockview}
/>
### Default Tab Title
@ -605,9 +701,10 @@ api.setTitle('my_new_custom_title');
> Note this only works when using the default tab implementation.
<Container sandboxId="updatetitle-dockview">
<DockviewSetTitle />
</Container>
<MultiFrameworkContainer
sandboxId="updatetitle-dockview"
react={DockviewSetTitle}
/>
### Custom Tab Title
@ -683,22 +780,22 @@ panel.group.locked = true;
### Group Controls Panel
`DockviewReact` accepts a prop `groupControlComponent` which expects a React component whos props are `IDockviewGroupControlProps`.
This control will be rendered inside the header bar on the right hand side for each group of tabs.
`DockviewReact` accepts `leftHeaderActionsComponent` and `rightHeaderActionsComponent` which expect a React component with props `IDockviewHeaderActionsProps`.
These controls are rendered of the left and right side of the space to the right of the tabs in the header bar.
```tsx
const Component: React.FunctionComponent<IDockviewGroupControlProps> = () => {
const Component: React.FunctionComponent<IDockviewHeaderActionsProps> = () => {
return <div>{'...'}</div>;
};
return <DockviewReact {...props} groupControlComponent={Component} />;
return <DockviewReact {...props} leftHeaderActionsComponent={Component} rightHeaderActionsComponent={...} />;
```
As a simple example the below uses the `groupControlComponent` to render a small control that indicates whether the group
is active and which panel is active in that group.
```tsx
const GroupControlComponent = (props: IDockviewGroupControlProps) => {
const RightHeaderActionsComponent = (props: IDockviewHeaderActionsProps) => {
const isGroupActive = props.isGroupActive;
const activePanel = props.activePanel;
@ -720,9 +817,10 @@ const GroupControlComponent = (props: IDockviewGroupControlProps) => {
};
```
<Container sandboxId="groupcontrol-dockview">
<DockviewGroupControl />
</Container>
<MultiFrameworkContainer
sandboxId="groupcontrol-dockview"
react={DockviewGroupControl}
/>
### Constraints
@ -736,9 +834,11 @@ api.group.api.setConstraints(...)
> If you specific a constraint on a group and move a panel within that group to another group it will no
> longer be subject to those constraints since those constraints were on the group and not on the individual panel.
<Container height={500} sandboxId="constraints-dockview">
<DockviewConstraints />
</Container>
<MultiFrameworkContainer
height={500}
sandboxId="constraints-dockview"
react={DockviewConstraints}
/>
## iFrames
@ -759,17 +859,21 @@ The visibility of these hoisted elements is then controlled through some exposed
You should open this example in CodeSandbox using the provided link to understand the code and make use of this implemention if required.
<Container sandboxId="iframe-dockview" height={600}>
<DockviewWithIFrames />
</Container>
<MultiFrameworkContainer
sandboxId="iframe-dockview"
height={600}
react={DockviewWithIFrames}
/>
## Events
A simple example showing events fired by `dockviewz that can be interacted with.
<Container height={600} sandboxId="events-dockview">
<EventsDockview />
</Container>
<MultiFrameworkContainer
height={600}
sandboxId="events-dockview"
react={EventsDockview}
/>
## Advanced Examples
@ -778,25 +882,8 @@ A simple example showing events fired by `dockviewz that can be interacted with.
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`.
<Container sandboxId="nested-dockview">
<NestedDockview />
</Container>
<MultiFrameworkContainer sandboxId="nested-dockview" react={NestedDockview} />
### Window-like mananger with tabs
<DockviewNative2 />
## Vanilla JS
> Note: This section is experimental and support for Vanilla JS is a work in progress.
The `dockview` package contains `ReactJS` wrappers for the core library.
The core library is published as an independant package under the name `dockview-core` which you can install standalone.
> When using `dockview` there is no need to also install `dockview-core`.
> `dockview-core` is a dependency of `dockview` and automatically installed during the installation process of `dockview` via `npm install dockview`.
<Container
sandboxId="typescript/vanilla-dockview"
injectVanillaJS={attachDockviewVanilla}
/>

View File

@ -27,15 +27,15 @@ import Link from '@docusaurus/Link';
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> |
| Property | Type | Optional | Default | Description |
| ------------------- | ------------------------------------ | -------- | ---------------------- | ----------- |
| onReady | (event: SplitviewReadyEvent) => void | No | | |
| components | object | No | | |
| orientation | Orientation | Yes | Orientation.HORIZONTAL | |
| proportionalLayout | boolean | Yes | true | |
| hideBorders | boolean | Yes | false | |
| className | string | Yes | '' | |
| disableAutoResizing | boolean | Yes | false | > |
## Gridview API
@ -78,9 +78,9 @@ const onReady = (event: GridviewReadyEvent) => {
| | | |
| 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> |
| layout | `(width: number, height:number): void` | |
| fromJSON | `(data: SerializedGridview): void` | |
| toJSON | `(): SerializedGridview` | |
| clear | `(): void` | Clears the current layout |
## Gridview Panel API

View File

@ -106,15 +106,15 @@ You can create a Paneview through the use of the `ReactPaneview` component.
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 | | |
| Property | Type | Optional | Default | Description |
| ------------------- | ------------------------------------ | -------- | ------- | ----------- |
| onReady | (event: SplitviewReadyEvent) => void | No | | |
| components | object | No | | |
| headerComponents | object | Yes | | |
| className | string | Yes | '' | |
| disableAutoResizing | boolean | Yes | false | |
| disableDnd | boolean | Yes | false | |
| onDidDrop | Event | Yes | | |
## Paneview API
@ -156,9 +156,9 @@ const onReady = (event: GridviewReadyEvent) => {
| 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> |
| layout | `(width: number, height:number): void` | |
| fromJSON | `(data: SerializedPaneview): void` | |
| toJSON | `(): SerializedPaneview` | |
| clear | `(): void` | Clears the current layout |
## Paneview Panel API

View File

@ -85,15 +85,15 @@ 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> |
| 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` | |
| hideBorders | `boolean` | Yes | `false` | Hide the borders between panels |
| className | `string` | Yes | `''` | Attaches a classname |
| disableAutoResizing | `boolean` | Yes | `false` | |
## Splitview API
@ -135,9 +135,9 @@ const onReady = (event: SplitviewReadyEvent) => {
| | |
| 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> |
| layout | `(width: number, height:number): void` | |
| fromJSON | `(data: SerializedSplitview): void` | |
| toJSON | `(): SerializedSplitview` | |
| clear | `(): void` | Clears the current layout |
## Splitview Panel API

View File

@ -0,0 +1,52 @@
---
sidebar_position: 3
description: Contributing
---
# Contributing
# Project description
Dockview is a layout manager library designed to provide a complete layouting solution.
It is written in plain TypeScript and can be used without any framework although
an extensive React wrapper has always and will always be provided for those using the React framework.
The project is hosted on GitHub and developed within a Monorepo powered by [Lerna](https://github.com/lerna/lerna).
It is developed using the `yarn` package manager since at the time of creation `yarn` was far superior when it came to managing monorepos.
The Monorepo contains three packages:
#### packages/dockview-core
The core project is entirely written in plain TypeScript without any frameworks or dependencies and it's source-code can be found
within the `dockview-core` package which is also published to npm.
#### packages/dockview
A complete collection of React components for use through the React framework to use dockview seamlessly
and is published to npm. It depends explicitly on `dockview-core` so there is no need to additionally install `dockview-core`.
> Dockview was originally a React-only library which is why the React version maintains the name `dockview` after
> splitting the core logic into a seperate package named `dockview-core`.
#### packages/docs
This package contains the code for this documentation website and examples hosted through **CodeSandbox**. It is **not** a published package on npm.
# Run the project locally
1. After you have cloned the project from GitHub run `yarn` at the root of the project which will install all project dependencies.
2. In order build `packages/dockview-core` then `packages/dockview`.
3. Run the docs website through `npm run start` in the `packages/docs` directory and go to _http://localhost:3000_ which
will now be running the local copy of `dockview` that you have just built.
### Examples
All examples can be found under [**packages/docs/sandboxes**](https://github.com/mathuo/dockview/tree/master/packages/docs/sandboxes).
Each example is an independently runnable example through **CodeSandbox**.
Through the documentation you will see links to runnable **CodeSandbox** examples.
## FAQ
#### Are there any plans to publish wrapper libraries for other frameworks such as Angular and Vue?
Currently no but this is open for contributors to try.

View File

@ -1,23 +1,18 @@
---
sidebar_position: 0
description: A zero dependency layout manager built for React
description: A zero dependency layout manager supporting ReactJS and Vanilla TypeScript
---
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/sandboxes/simple-dockview/src/app';
import Link from '@docusaurus/Link';
# 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:
@ -33,53 +28,11 @@ depending on your solution this might be:
@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>
<Link to="./components/dockview">
<h2>Dockview</h2>
</Link>
<div
style={{
@ -92,58 +45,47 @@ Splitview
<SimpleDockview />
</div>
```tsx
import {
DockviewReact,
DockviewReadyEvent,
PanelCollection,
IDockviewPanelProps,
IDockviewPanelHeaderProps,
} from 'dockview';
<Link to="./components/splitview">
<h2>Splitview</h2>
</Link>
const components: PanelCollection<IDockviewPanelProps> = {
default: (props: IDockviewPanelProps<{ someProps: string }>) => {
return <div>{props.params.someProps}</div>;
},
};
<div
style={{
height: '100px',
backgroundColor: 'rgb(30,30,30)',
color: 'white',
margin: '20px 0px',
}}
>
<SimpleSplitview />
</div>
const headers: PanelCollection<IDockviewPanelHeaderProps> = {
customTab: (props: IDockviewPanelHeaderProps) => {
return (
<div>
<span>{props.api.title}</span>
<span onClick={() => props.api.close()}>{'[x]'}</span>
</div>
);
},
};
<Link to="./components/gridview">
<h2>Gridview</h2>
</Link>
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' },
});
};
<div
style={{
height: '300px',
backgroundColor: 'rgb(30,30,30)',
color: 'white',
margin: '20px 0px',
}}
>
<SimpleGridview />
</div>
return (
<DockviewReact
components={components}
tabComponents={headers} // optional headers renderer
onReady={onReady}
/>
);
};
```
<Link to="./components/paneview">
<h2>Paneview</h2>
</Link>
<div
style={{
height: '300px',
backgroundColor: 'rgb(30,30,30)',
color: 'white',
margin: '20px 0px',
}}
>
<SimplePaneview />
</div>

View File

@ -1,29 +1,31 @@
---
sidebar_position: 3
sidebar_position: 1
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
`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 should be imported at some point in your application
```css
```css title="Example import with .css file"
@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` comes with a number of themes which are all CSS classes and can be found [here](https://github.com/mathuo/dockview/blob/master/packages/dockview-core/src/theme.scss).
To use a `dockview` theme the CSS must encapsulate the component. The current list of themes is:
- `.dockview-theme-light`
- `.dockview-theme-dark`
- `.dockview-theme-abyss`
- `dockview-theme-dark`
- `dockview-theme-light`
- `dockview-theme-vs`
- `dockview-theme-abyss`
- `dockview-theme-dracula`
- `dockview-theme-replit`
## Customizing Theme
@ -60,9 +62,9 @@ and are free to build your own themes based on these css properties.
| --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:
For 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
```css title="Additional CSS to show a bottom border on active groups"
.groupview {
&.active-group {
> .tabs-and-actions-container {
@ -76,14 +78,3 @@ As an example if you wanted to add a bottom border to the tab container for an a
}
}
```
<div
style={{
height: '300px',
backgroundColor: 'rgb(30,30,30)',
color: 'white',
margin: '20px 0px',
}}
>
<CustomCSSDockview />
</div>

View File

@ -11,7 +11,8 @@ console.log(`isCI: ${process.env.CI}`);
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'Dockview',
tagline: 'A zero dependency layout manager built for React',
tagline:
'A zero dependency layout manager supporting ReactJS and Vanilla TypeScript',
url: 'https://dockview.dev',
baseUrl: process.env.CI ? `/` : '/',
onBrokenLinks: 'throw',
@ -39,13 +40,24 @@ const config = {
'docusaurus-plugin-sass',
(context, options) => {
return {
name: 'webpack',
name: 'custom-webpack',
configureWebpack: (config, isServer, utils) => {
return {
// externals: ['react', 'react-dom'],
devtool: 'source-map',
module: {
rules: [
{
test: /\.js$/,
enforce: 'pre',
use: ['source-map-loader'],
},
],
},
resolve: {
...config.resolve,
alias: {
...config.resolve.alias,
react: path.join(
__dirname,
'../../node_modules',
@ -57,9 +69,6 @@ const config = {
'react-dom'
),
},
fallback: {
timers: false,
},
},
};
},
@ -141,6 +150,11 @@ const config = {
label: 'Docs',
},
{ to: '/blog', label: 'Blog', position: 'left' },
{
to: 'https://dockview.dev/typedocs',
label: 'TSDoc',
position: 'left',
},
{
type: 'docsVersionDropdown',
position: 'right',

View File

@ -1,6 +1,6 @@
{
"name": "dockview-docs",
"version": "1.7.5",
"version": "1.8.2",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
@ -22,12 +22,13 @@
"@minoru/react-dnd-treeview": "^3.4.3",
"axios": "^1.3.3",
"clsx": "^1.2.1",
"dockview": "^1.7.5",
"dockview": "^1.8.2",
"prism-react-renderer": "^1.3.5",
"react": "^18.2.0",
"react-dnd": "^16.0.1",
"react-dom": "^18.2.0",
"recoil": "^0.7.6",
"source-map-loader": "^4.0.1",
"uuid": "^9.0.0",
"xml2js": "^0.4.23"
},

View File

@ -1,6 +1,5 @@
import {
DockviewApi,
DockviewMutableDisposable,
DockviewReact,
DockviewReadyEvent,
GridConstraintChangeEvent,
@ -101,7 +100,7 @@ const components = {
},
};
const App = () => {
const App = (props: { theme?: string }) => {
const [api, setApi] = React.useState<DockviewApi>();
const onReady = (event: DockviewReadyEvent) => {
@ -141,7 +140,7 @@ const App = () => {
<DockviewReact
onReady={onReady}
components={components}
className="dockview-theme-abyss "
className={`${props.theme || 'dockview-theme-abyss'}`}
/>
);
};

View File

@ -31,10 +31,8 @@ const headerComponents = {
},
};
const CustomHeadersDockview = () => {
const CustomHeadersDockview = (props: { theme?: string }) => {
const onReady = (event: DockviewReadyEvent) => {
const d = localStorage.getItem('test');
event.api.addPanel({
id: 'panel_1',
component: 'default',
@ -116,7 +114,7 @@ const CustomHeadersDockview = () => {
components={components}
defaultTabComponent={headerComponents.default}
onReady={onReady}
className="dockview-theme-abyss"
className={`${props.theme || 'dockview-theme-abyss'}`}
/>
);
};

View File

@ -4,7 +4,7 @@ import {
DockviewReadyEvent,
IDockviewPanelHeaderProps,
IDockviewPanelProps,
IDockviewGroupControlProps,
IDockviewHeaderActionsProps,
} from 'dockview';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
@ -134,7 +134,7 @@ const groupControlsComponents = {
},
};
const GroupControls = (props: IDockviewGroupControlProps) => {
const RightControls = (props: IDockviewHeaderActionsProps) => {
const Component = React.useMemo(() => {
if (!props.isGroupActive || !props.activePanel) {
return null;
@ -161,7 +161,37 @@ const GroupControls = (props: IDockviewGroupControlProps) => {
);
};
const DockviewDemo = () => {
let counter = 0;
const LeftControls = (props: IDockviewHeaderActionsProps) => {
const onClick = () => {
props.containerApi.addPanel({
id: `id_${Date.now().toString()}`,
component: 'default',
title: `Tab ${counter++}`,
position: {
referenceGroup: props.group,
},
});
};
return (
<div
className="group-control"
style={{
display: 'flex',
alignItems: 'center',
padding: '0px 8px',
height: '100%',
color: 'var(--dv-activegroup-visiblepanel-tab-color)',
}}
>
<Icon onClick={onClick} icon="add" />
</div>
);
};
const DockviewDemo = (props: { theme?: string }) => {
const onReady = (event: DockviewReadyEvent) => {
event.api.addPanel({
id: 'panel_1',
@ -190,14 +220,12 @@ const DockviewDemo = () => {
title: 'Panel 5',
position: { referencePanel: 'panel_4', direction: 'within' },
});
const panel6 = event.api.addPanel({
event.api.addPanel({
id: 'panel_6',
component: 'default',
title: 'Panel 6',
position: { referencePanel: 'panel_4', direction: 'below' },
});
panel6.group.locked = true;
panel6.group.header.hidden = true;
event.api.addPanel({
id: 'panel_7',
component: 'default',
@ -211,7 +239,19 @@ const DockviewDemo = () => {
position: { referencePanel: 'panel_7', direction: 'within' },
});
event.api.addGroup();
event.api.addPanel({
id: 'panel_9',
component: 'default',
title: 'Panel 9',
floating: { width: 450, height: 250 },
});
event.api.addPanel({
id: 'panel_10',
component: 'default',
title: 'Panel 10',
position: { referencePanel: 'panel_9' },
});
event.api.getPanel('panel_1')!.api.setActive();
};
@ -220,9 +260,10 @@ const DockviewDemo = () => {
<DockviewReact
components={components}
defaultTabComponent={headerComponents.default}
groupControlComponent={GroupControls}
rightHeaderActionsComponent={RightControls}
leftHeaderActionsComponent={LeftControls}
onReady={onReady}
className="dockview-theme-abyss"
className={props.theme || 'dockview-theme-abyss'}
/>
);
};

View File

@ -41,7 +41,7 @@ const DraggableElement = () => (
</span>
);
const DndDockview = (props: { renderVisibleOnly: boolean }) => {
const DndDockview = (props: { renderVisibleOnly: boolean; theme?: string }) => {
const onReady = (event: DockviewReadyEvent) => {
event.api.addPanel({
id: 'panel_1',
@ -106,7 +106,7 @@ const DndDockview = (props: { renderVisibleOnly: boolean }) => {
<DockviewReact
components={components}
onReady={onReady}
className="dockview-theme-abyss"
className={`${props.theme || 'dockview-theme-abyss'}`}
onDidDrop={onDidDrop}
showDndOverlay={showDndOverlay}
/>

View File

@ -73,7 +73,7 @@ const components = {
},
};
const DockviewDemo2 = () => {
const DockviewDemo2 = (props: { theme?: string }) => {
const onReady = (event: GridviewReadyEvent) => {
event.api.addPanel({
id: 'panes',
@ -111,7 +111,7 @@ const DockviewDemo2 = () => {
<GridviewReact
onReady={onReady}
components={components}
className="dockview-theme-abyss"
className={`${props.theme || 'dockview-theme-abyss'}`}
/>
);
};

View File

@ -14,7 +14,7 @@ const components = {
},
};
const EventsDockview = () => {
const EventsDockview = (props: { theme?: string }) => {
const [lines, setLines] = React.useState<Line[]>([]);
const [checked, setChecked] = React.useState<boolean>(false);
@ -230,7 +230,6 @@ const EventsDockview = () => {
},
},
activeGroup: '80',
options: {},
});
return;
}
@ -331,7 +330,7 @@ const EventsDockview = () => {
<DockviewReact
components={components}
onReady={onReady}
className="dockview-theme-abyss"
className={`${props.theme || 'dockview-theme-abyss'}`}
/>
</div>
<div style={{ flexGrow: 1, paddingTop: '5px' }}>

View File

@ -1,3 +0,0 @@
.externaldnd-dockview {
color: white;
}

View File

@ -7,7 +7,6 @@ import * as React from 'react';
import TreeComponent from './treeview';
import { getBackendOptions, MultiBackend } from '@minoru/react-dnd-treeview';
import { DndProvider } from 'react-dnd';
import './app.scss';
const components = {
default: (props: IDockviewPanelProps<{ title: string }>) => {
@ -26,7 +25,7 @@ const components = {
},
};
export const App: React.FC = () => {
export const App: React.FC = (props: { theme?: string }) => {
const onReady = (event: DockviewReadyEvent) => {
const panel = event.api.addPanel({
id: 'panel_1',
@ -97,7 +96,7 @@ export const App: React.FC = () => {
<DockviewReact
components={components}
onReady={onReady}
className="dockview-theme-abyss externaldnd-dockview"
className={`${props.theme || 'dockview-theme-abyss'}`}
/>
</DndProvider>
);

View File

@ -1,5 +1,5 @@
{
"name": "groupcontrol-dockview",
"name": "floatinggroup-dockview",
"description": "",
"keywords": [
"dockview"
@ -29,4 +29,4 @@
"not ie <= 11",
"not op_mini all"
]
}
}

View File

@ -0,0 +1,278 @@
import {
DockviewApi,
DockviewGroupPanel,
DockviewReact,
DockviewReadyEvent,
IDockviewHeaderActionsProps,
IDockviewPanelProps,
SerializedDockview,
} from 'dockview';
import * as React from 'react';
import { Icon } from './utils';
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',
});
const panel4 = api.addPanel({
id: 'panel_4',
component: 'default',
floating: true,
});
api.addPanel({
id: 'panel_5',
component: 'default',
floating: false,
position: { referencePanel: panel4 },
});
api.addPanel({
id: 'panel_6',
component: 'default',
});
}
let panelCount = 0;
function addPanel(api: DockviewApi) {
api.addPanel({
id: (++panelCount).toString(),
title: `Tab ${panelCount}`,
component: 'default',
});
}
function addFloatingPanel2(api: DockviewApi) {
api.addPanel({
id: (++panelCount).toString(),
title: `Tab ${panelCount}`,
component: 'default',
floating: { width: 250, height: 150, x: 50, y: 50 },
});
}
function safeParse<T>(value: any): T | null {
try {
return JSON.parse(value) as T;
} catch (err) {
return null;
}
}
const useLocalStorage = <T,>(
key: string
): [T | null, (setter: T | null) => void] => {
const [state, setState] = React.useState<T | null>(
safeParse(localStorage.getItem(key))
);
React.useEffect(() => {
const _state = localStorage.getItem('key');
try {
if (_state !== null) {
setState(JSON.parse(_state));
}
} catch (err) {
//
}
}, [key]);
return [
state,
(_state: T | null) => {
if (_state === null) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify(_state));
setState(_state);
}
},
];
};
export const DockviewPersistance = (props: { theme?: string }) => {
const [api, setApi] = React.useState<DockviewApi>();
const [layout, setLayout] =
useLocalStorage<SerializedDockview>('floating.layout');
const [disableFloatingGroups, setDisableFloatingGroups] =
React.useState<boolean>(false);
const load = (api: DockviewApi) => {
api.clear();
if (layout) {
try {
api.fromJSON(layout);
} catch (err) {
console.error(err);
api.clear();
loadDefaultLayout(api);
}
} else {
loadDefaultLayout(api);
}
};
const onReady = (event: DockviewReadyEvent) => {
load(event.api);
setApi(event.api);
};
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
}}
>
<div style={{ height: '25px' }}>
<button
onClick={() => {
if (api) {
setLayout(api.toJSON());
}
}}
>
Save
</button>
<button
onClick={() => {
if (api) {
load(api);
}
}}
>
Load
</button>
<button
onClick={() => {
api!.clear();
setLayout(null);
}}
>
Clear
</button>
<button
onClick={() => {
addFloatingPanel2(api!);
}}
>
Add Floating Group
</button>
<button
onClick={() => {
setDisableFloatingGroups((x) => !x);
}}
>
{`${
disableFloatingGroups ? 'Enable' : 'Disable'
} floating groups`}
</button>
</div>
<div
style={{
flexGrow: 1,
}}
>
<DockviewReact
onReady={onReady}
components={components}
watermarkComponent={Watermark}
leftHeaderActionsComponent={LeftComponent}
rightHeaderActionsComponent={RightComponent}
disableFloatingGroups={disableFloatingGroups}
className={`${props.theme || 'dockview-theme-abyss'}`}
/>
</div>
</div>
);
};
const LeftComponent = (props: IDockviewHeaderActionsProps) => {
const onClick = () => {
addPanel(props.containerApi);
};
return (
<div style={{ height: '100%', color: 'white', padding: '0px 4px' }}>
<Icon onClick={onClick} icon={'add'} />
</div>
);
};
const RightComponent = (props: IDockviewHeaderActionsProps) => {
const [floating, setFloating] = React.useState<boolean>(
props.api.isFloating
);
React.useEffect(() => {
const disposable = props.group.api.onDidFloatingStateChange((event) => [
setFloating(event.isFloating),
]);
return () => {
disposable.dispose();
};
}, [props.group.api]);
const onClick = () => {
if (floating) {
const group = props.containerApi.addGroup();
props.group.api.moveTo({ group });
} else {
props.containerApi.addFloatingGroup(props.group);
}
};
return (
<div style={{ height: '100%', color: 'white', padding: '0px 4px' }}>
<Icon
onClick={onClick}
icon={floating ? 'jump_to_element' : 'back_to_tab'}
/>
</div>
);
};
export default DockviewPersistance;
const Watermark = () => {
return <div style={{ color: 'white', padding: '8px' }}>watermark</div>;
};

View File

@ -0,0 +1,30 @@
import * as React from 'react';
export const Icon = (props: {
icon: string;
title?: string;
onClick?: (event: React.MouseEvent) => void;
}) => {
return (
<div
title={props.title}
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '30px',
height: '100%',
fontSize: '18px',
}}
onClick={props.onClick}
>
<span
style={{ fontSize: 'inherit', cursor: 'pointer' }}
className="material-symbols-outlined"
>
{props.icon}
</span>
</div>
);
};

View File

@ -47,7 +47,7 @@ const tabComponents = {
},
};
const DockviewNative = () => {
const DockviewNative = (props: { theme?: string }) => {
const onReady = (event: DockviewReadyEvent) => {
const panel1 = event.api.addPanel({
id: 'panel_1',
@ -91,7 +91,7 @@ const DockviewNative = () => {
onReady={onReady}
components={components}
tabComponents={tabComponents}
className="dockview-theme-abyss"
className={`${props.theme || 'dockview-theme-abyss'}`}
singleTabMode="fullwidth"
/>
);

View File

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

View File

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

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