mirror of
https://github.com/mathuo/dockview
synced 2025-02-15 12:55:44 +00:00
Merge branch 'master' of https://github.com/mathuo/dockview into 281-type-hints-for-panel-parameters
This commit is contained in:
commit
141b2beaf3
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -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
|
||||
|
8
.github/workflows/deploy-docs.yml
vendored
8
.github/workflows/deploy-docs.yml
vendored
@ -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
|
||||
|
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@ -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') }}
|
||||
|
@ -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/
|
||||
|
@ -3,7 +3,7 @@
|
||||
"packages/*"
|
||||
],
|
||||
"useWorkspaces": true,
|
||||
"version": "1.7.5",
|
||||
"version": "1.8.2",
|
||||
"npmClient": "yarn",
|
||||
"command": {
|
||||
"publish": {
|
||||
|
@ -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/
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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');
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
156
packages/dockview-core/src/__tests__/dnd/overlay.spec.ts
Normal file
156
packages/dockview-core/src/__tests__/dnd/overlay.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
@ -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
|
@ -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', () => {
|
@ -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);
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load Diff
@ -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>, []>(
|
||||
() => {
|
21
packages/dockview-core/src/__tests__/dom.spec.ts
Normal file
21
packages/dockview-core/src/__tests__/dom.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -96,7 +96,7 @@ describe('componentFactory', () => {
|
||||
|
||||
expect(component).toHaveBeenCalled();
|
||||
|
||||
expect(componentResult instanceof component);
|
||||
expect(componentResult instanceof component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
54
packages/dockview-core/src/api/dockviewGroupPanelApi.ts
Normal file
54
packages/dockview-core/src/api/dockviewGroupPanelApi.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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'),
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
122
packages/dockview-core/src/dnd/overlay.scss
Normal file
122
packages/dockview-core/src/dnd/overlay.scss
Normal 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;
|
||||
}
|
||||
}
|
484
packages/dockview-core/src/dnd/overlay.ts
Normal file
484
packages/dockview-core/src/dnd/overlay.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
})
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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(() => {
|
||||
|
@ -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;
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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() };
|
||||
};
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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/
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
|
@ -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(),
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
@ -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';
|
||||
|
@ -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
|
||||
|
||||
|
20
packages/docs/blog/2023-06-18-dockview-1.7.6.md
Normal file
20
packages/docs/blog/2023-06-18-dockview-1.7.6.md
Normal 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
|
23
packages/docs/blog/2023-07-23-dockview-1.8.0.md
Normal file
23
packages/docs/blog/2023-07-23-dockview-1.8.0.md
Normal 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)
|
17
packages/docs/blog/2023-07-24-dockview-1.8.2.md
Normal file
17
packages/docs/blog/2023-07-24-dockview-1.8.2.md
Normal 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
|
@ -2,6 +2,7 @@
|
||||
"label": "Components",
|
||||
"collapsible": true,
|
||||
"collapsed": false,
|
||||
"position": 2,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"title": "Components"
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
52
packages/docs/docs/contributing.mdx
Normal file
52
packages/docs/docs/contributing.mdx
Normal 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.
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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'}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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'}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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'}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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' }}>
|
||||
|
@ -1,3 +0,0 @@
|
||||
.externaldnd-dockview {
|
||||
color: white;
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "groupcontrol-dockview",
|
||||
"name": "floatinggroup-dockview",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
"dockview"
|
||||
@ -29,4 +29,4 @@
|
||||
"not ie <= 11",
|
||||
"not op_mini all"
|
||||
]
|
||||
}
|
||||
}
|
278
packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx
Normal file
278
packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx
Normal 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>;
|
||||
};
|
30
packages/docs/sandboxes/floatinggroup-dockview/src/utils.tsx
Normal file
30
packages/docs/sandboxes/floatinggroup-dockview/src/utils.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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"
|
||||
/>
|
||||
);
|
||||
|
32
packages/docs/sandboxes/headeractions-dockview/package.json
Normal file
32
packages/docs/sandboxes/headeractions-dockview/package.json
Normal 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"
|
||||
]
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user