Compare commits

..

No commits in common. "master" and "v1.13.1" have entirely different histories.

351 changed files with 11492 additions and 19718 deletions

View File

@ -1,8 +1,6 @@
{ {
"packages": [ "packages": [
"packages/dockview-core", "packages/dockview-core",
"packages/dockview-vue",
"packages/dockview-react",
"packages/dockview" "packages/dockview"
], ],
"sandboxes": [ "sandboxes": [

View File

@ -26,10 +26,6 @@ jobs:
working-directory: packages/dockview-core working-directory: packages/dockview-core
- run: npm run build - run: npm run build
working-directory: packages/dockview working-directory: packages/dockview
- run: npm run build
working-directory: packages/dockview-vue
- run: npm run build
working-directory: packages/dockview-react
- run: npm run build - run: npm run build
working-directory: packages/docs working-directory: packages/docs
- run: npm run docs - run: npm run docs

View File

@ -27,7 +27,7 @@ jobs:
- run: npm run build - run: npm run build
- run: npm run test:cov - run: npm run test:cov
- name: SonarCloud Scan - name: SonarCloud Scan
uses: sonarsource/sonarqube-scan-action@v5 uses: SonarSource/sonarcloud-github-action@master
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

View File

@ -35,12 +35,6 @@ jobs:
- name: Publish dockview - name: Publish dockview
run: npm publish --provenance run: npm publish --provenance
working-directory: packages/dockview working-directory: packages/dockview
- name: Publish dockview-vue
run: npm publish --provenance
working-directory: packages/dockview-vue
- name: Publish dockview-react
run: npm publish --provenance
working-directory: packages/dockview-react
publish-experimental: publish-experimental:
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -70,9 +64,3 @@ jobs:
- name: Publish dockview - name: Publish dockview
run: npm publish --provenance --tag experimental run: npm publish --provenance --tag experimental
working-directory: packages/dockview working-directory: packages/dockview
- name: Publish dockview-vue
run: npm publish --provenance --tag experimental
working-directory: packages/dockview-vue
- name: Publish dockview-react
run: npm publish --provenance --tag experimental
working-directory: packages/dockview-react

View File

@ -1,18 +1,18 @@
<div align="center"> <div align="center">
<h1>dockview</h1> <h1>dockview</h1>
<p>Zero dependency layout manager supporting tabs, groups, grids and splitviews. Supports React, Vue and Vanilla TypeScript</p> <p>Zero dependency layout manager supporting tabs, groups, grids and splitviews with ReactJS support written in TypeScript</p>
</div> </div>
--- ---
[![npm version](https://badge.fury.io/js/dockview-core.svg)](https://www.npmjs.com/package/dockview-core) [![npm version](https://badge.fury.io/js/dockview.svg)](https://www.npmjs.com/package/dockview)
[![npm](https://img.shields.io/npm/dm/dockview-core)](https://www.npmjs.com/package/dockview-core) [![npm](https://img.shields.io/npm/dm/dockview)](https://www.npmjs.com/package/dockview)
[![CI Build](https://github.com/mathuo/dockview/workflows/CI/badge.svg)](https://github.com/mathuo/dockview/actions?query=workflow%3ACI) [![CI Build](https://github.com/mathuo/dockview/workflows/CI/badge.svg)](https://github.com/mathuo/dockview/actions?query=workflow%3ACI)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=mathuo_dockview&metric=coverage)](https://sonarcloud.io/summary/overall?id=mathuo_dockview) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=mathuo_dockview&metric=coverage)](https://sonarcloud.io/summary/overall?id=mathuo_dockview)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=mathuo_dockview&metric=alert_status)](https://sonarcloud.io/summary/overall?id=mathuo_dockview) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=mathuo_dockview&metric=alert_status)](https://sonarcloud.io/summary/overall?id=mathuo_dockview)
[![Bundle Phobia](https://badgen.net/bundlephobia/minzip/dockview-core)](https://bundlephobia.com/result?p=dockview-core) [![Bundle Phobia](https://badgen.net/bundlephobia/minzip/dockview)](https://bundlephobia.com/result?p=dockview)
## ##
@ -35,4 +35,24 @@ Please see the website: https://dockview.dev
- Transparent builds and Code Analysis - Transparent builds and Code Analysis
- Security at mind - verifed publishing and builds through GitHub Actions - Security at mind - verifed publishing and builds through GitHub Actions
Want to verify our builds? Go [here](https://www.npmjs.com/package/dockview#user-content-provenance). Want to verify our builds? Go [here](https://www.npmjs.com/package/dockview#Provenance).
## Quick start
Dockview has a peer dependency on `react >= 16.8.0` and `react-dom >= 16.8.0`. You can install dockview from [npm](https://www.npmjs.com/package/dockview).
```
npm install --save dockview
```
Within your project you must import or reference the stylesheet at `dockview/dist/styles/dockview.css` and attach a theme.
```css
@import '~dockview/dist/styles/dockview.css';
```
You should also attach a dockview theme to an element containing your components. For example:
```html
<body classname="dockview-theme-dark"></body>
```

View File

@ -1,8 +0,0 @@
# Reporting a Vulnerability
- Dockview is an entirely open source project.
- All build and publication scripts use public Github Action files found [here](https://github.com/mathuo/dockview/tree/master/.github/workflows).
- All npm publications are verified through the use of [provenance statements](https://docs.npmjs.com/generating-provenance-statements/).
- All builds are scanned with SonarCube and outputs can be found [here](https://sonarcloud.io/summary/overall?id=mathuo_dockview).
If you believe you have found a security or vulnerability issue please send a complete example to github.mathuo@gmail.com where it will be investigated.

View File

@ -2,7 +2,7 @@
"packages": [ "packages": [
"packages/*" "packages/*"
], ],
"version": "4.2.5", "version": "1.13.1",
"npmClient": "yarn", "npmClient": "yarn",
"command": { "command": {
"publish": { "publish": {

View File

@ -16,7 +16,7 @@
"packages/*" "packages/*"
], ],
"scripts": { "scripts": {
"build": "lerna run build --scope '{dockview-core,dockview,dockview-vue,dockview-react}'", "build": "lerna run build --scope '{dockview-core,dockview}'",
"clean": "lerna run clean", "clean": "lerna run clean",
"docs": "typedoc", "docs": "typedoc",
"generate-docs": "node scripts/docs.mjs", "generate-docs": "node scripts/docs.mjs",
@ -58,7 +58,7 @@
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"jest-sonar-reporter": "^2.0.0", "jest-sonar-reporter": "^2.0.0",
"jsdom": "^23.0.1", "jsdom": "^23.0.1",
"lerna": "^8.2.1", "lerna": "^8.0.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
@ -77,5 +77,8 @@
}, },
"engines": { "engines": {
"node": ">=18.0" "node": ">=18.0"
},
"dependencies": {
"ag-grid-vue3": "^31.1.1"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "dockview-angular", "name": "dockview-angular",
"version": "4.2.5", "version": "1.13.1",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews", "description": "Zero dependency layout manager supporting tabs, grids and splitviews",
"keywords": [ "keywords": [
"splitview", "splitview",
@ -54,6 +54,6 @@
"test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview --coverage" "test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview --coverage"
}, },
"dependencies": { "dependencies": {
"dockview-core": "^4.2.5" "dockview-core": "^1.13.1"
} }
} }

View File

@ -1,7 +1,7 @@
<div align="center"> <div align="center">
<h1>dockview</h1> <h1>dockview</h1>
<p>Zero dependency layout manager supporting tabs, groups, grids and splitviews. Supports React, Vue and Vanilla TypeScript</p> <p>Zero dependency layout manager supporting tabs, groups, grids and splitviews written in TypeScript</p>
</div> </div>
@ -36,3 +36,23 @@ Please see the website: https://dockview.dev
- Security at mind - verifed publishing and builds through GitHub Actions - Security at mind - verifed publishing and builds through GitHub Actions
Want to verify our builds? Go [here](https://www.npmjs.com/package/dockview#Provenance). Want to verify our builds? Go [here](https://www.npmjs.com/package/dockview#Provenance).
## Quick start
Dockview has a peer dependency on `react >= 16.8.0` and `react-dom >= 16.8.0`. You can install dockview from [npm](https://www.npmjs.com/package/dockview-core).
```
npm install --save dockview-core
```
Within your project you must import or reference the stylesheet at `dockview-core/dist/styles/dockview.css` and attach a theme.
```css
@import '~dockview-core/dist/styles/dockview.css';
```
You should also attach a dockview theme to an element containing your components. For example:
```html
<body classname="dockview-theme-dark"></body>
```

View File

@ -1,6 +1,6 @@
{ {
"name": "dockview-core", "name": "dockview-core",
"version": "4.2.5", "version": "1.13.1",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews", "description": "Zero dependency layout manager supporting tabs, grids and splitviews",
"keywords": [ "keywords": [
"splitview", "splitview",

View File

@ -1,28 +1,23 @@
import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel'; import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { import {
TabPartInitParameters, GroupPanelPartInitParameters,
IContentRenderer, IContentRenderer,
ITabRenderer, ITabRenderer,
} from '../../dockview/types'; } from '../../dockview/types';
import { PanelUpdateEvent } from '../../panel/types'; import { PanelUpdateEvent } from '../../panel/types';
import { TabLocation } from '../../dockview/framework';
export class DockviewPanelModelMock implements IDockviewPanelModel { export class DockviewPanelModelMock implements IDockviewPanelModel {
constructor( constructor(
readonly contentComponent: string, readonly contentComponent: string,
readonly content: IContentRenderer, readonly content: IContentRenderer,
readonly tabComponent: string, readonly tabComponent?: string,
readonly tab: ITabRenderer readonly tab?: ITabRenderer
) { ) {
// //
} }
createTabRenderer(tabLocation: TabLocation): ITabRenderer { init(params: GroupPanelPartInitParameters): void {
return this.tab;
}
init(params: TabPartInitParameters): void {
// //
} }

View File

@ -1,45 +0,0 @@
import { fromPartial } from '@total-typescript/shoehorn';
export function setupMockWindow() {
const listeners: Record<string, (() => void)[]> = {};
let width = 1000;
let height = 2000;
return fromPartial<Window>({
addEventListener: (type: string, listener: () => void) => {
if (!listeners[type]) {
listeners[type] = [];
}
listeners[type].push(listener);
if (type === 'load') {
listener();
}
},
removeEventListener: (type: string, listener: () => void) => {
if (listeners[type]) {
const index = listeners[type].indexOf(listener);
if (index > -1) {
listeners[type].splice(index, 1);
}
}
},
dispatchEvent: (event: Event) => {
const items = listeners[event.type];
if (!items) {
return;
}
items.forEach((item) => item());
},
document: document,
close: () => {
listeners['beforeunload']?.forEach((f) => f());
},
get innerWidth() {
return width++;
},
get innerHeight() {
return height++;
},
});
}

View File

@ -44,30 +44,3 @@ export function createOffsetDragOverEvent(params: {
export function exhaustMicrotaskQueue(): Promise<void> { export function exhaustMicrotaskQueue(): Promise<void> {
return new Promise<void>((resolve) => resolve()); return new Promise<void>((resolve) => resolve());
} }
export const mockGetBoundingClientRect = ({
left,
top,
height,
width,
}: {
left: number;
top: number;
height: number;
width: number;
}) => {
const result = {
left,
top,
height,
width,
right: left + width,
bottom: top + height,
x: left,
y: top,
};
return {
...result,
toJSON: () => result,
};
};

View File

@ -9,8 +9,7 @@ describe('groupPanelApi', () => {
const accessor = fromPartial<DockviewComponent>({ const accessor = fromPartial<DockviewComponent>({
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
options: {}, options: { parentElement: document.createElement('div') },
onDidOptionsChange: jest.fn(),
}); });
const panelMock = jest.fn<DockviewPanel, []>(() => { const panelMock = jest.fn<DockviewPanel, []>(() => {
@ -50,8 +49,7 @@ describe('groupPanelApi', () => {
const accessor = fromPartial<DockviewComponent>({ const accessor = fromPartial<DockviewComponent>({
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
options: {}, options: { parentElement: document.createElement('div') },
onDidOptionsChange: jest.fn(),
}); });
const groupViewPanel = new DockviewGroupPanel( const groupViewPanel = new DockviewGroupPanel(
@ -83,8 +81,7 @@ describe('groupPanelApi', () => {
const accessor = fromPartial<DockviewComponent>({ const accessor = fromPartial<DockviewComponent>({
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
options: {}, options: { parentElement: document.createElement('div') },
onDidOptionsChange: jest.fn(),
}); });
const groupViewPanel = new DockviewGroupPanel( const groupViewPanel = new DockviewGroupPanel(

View File

@ -70,8 +70,8 @@ describe('abstractDragHandler', () => {
expect(span.style.pointerEvents).toBeFalsy(); expect(span.style.pointerEvents).toBeFalsy();
fireEvent.dragEnd(element); fireEvent.dragEnd(element);
expect(iframe.style.pointerEvents).toBe(''); expect(iframe.style.pointerEvents).toBe('auto');
expect(webview.style.pointerEvents).toBe(''); expect(webview.style.pointerEvents).toBe('auto');
expect(span.style.pointerEvents).toBeFalsy(); expect(span.style.pointerEvents).toBeFalsy();
handler.dispose(); handler.dispose();
@ -114,8 +114,8 @@ describe('abstractDragHandler', () => {
expect(span.style.pointerEvents).toBeFalsy(); expect(span.style.pointerEvents).toBeFalsy();
handler.dispose(); handler.dispose();
expect(iframe.style.pointerEvents).toBe(''); expect(iframe.style.pointerEvents).toBe('auto');
expect(webview.style.pointerEvents).toBe(''); expect(webview.style.pointerEvents).toBe('auto');
expect(span.style.pointerEvents).toBeFalsy(); expect(span.style.pointerEvents).toBeFalsy();
}); });
@ -172,7 +172,7 @@ describe('abstractDragHandler', () => {
const event = new Event('dragstart'); const event = new Event('dragstart');
const spy = jest.spyOn(event, 'preventDefault'); const spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event); fireEvent(element, event);
expect(spy).toHaveBeenCalledTimes(0); expect(spy).toBeCalledTimes(0);
handler.dispose(); handler.dispose();
}); });

View File

@ -16,10 +16,10 @@ describe('droptarget', () => {
beforeEach(() => { beforeEach(() => {
element = document.createElement('div'); element = document.createElement('div');
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 200); jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 200);
}); });
test('that dragover events are marked', () => { test('that dragover events are marked', () => {
@ -53,7 +53,7 @@ describe('droptarget', () => {
fireEvent.dragOver(element); fireEvent.dragOver(element);
const target = element.querySelector( const target = element.querySelector(
'.dv-drop-target-dropzone' '.drop-target-dropzone'
) as HTMLElement; ) as HTMLElement;
fireEvent.drop(target); fireEvent.drop(target);
expect(position).toBe('center'); expect(position).toBe('center');
@ -61,7 +61,7 @@ describe('droptarget', () => {
const event = new Event('dragover'); const event = new Event('dragover');
(event as any)['__dockview_droptarget_event_is_used__'] = true; (event as any)['__dockview_droptarget_event_is_used__'] = true;
fireEvent(element, event); fireEvent(element, event);
expect(element.querySelector('.dv-drop-target-dropzone')).toBeNull(); expect(element.querySelector('.drop-target-dropzone')).toBeNull();
}); });
test('directionToPosition', () => { test('directionToPosition', () => {
@ -102,7 +102,7 @@ describe('droptarget', () => {
fireEvent.dragOver(element); fireEvent.dragOver(element);
const target = element.querySelector( const target = element.querySelector(
'.dv-drop-target-dropzone' '.drop-target-dropzone'
) as HTMLElement; ) as HTMLElement;
fireEvent.drop(target); fireEvent.drop(target);
expect(position).toBe('center'); expect(position).toBe('center');
@ -124,7 +124,7 @@ describe('droptarget', () => {
fireEvent.dragOver(element); fireEvent.dragOver(element);
const target = element.querySelector( const target = element.querySelector(
'.dv-drop-target-dropzone' '.drop-target-dropzone'
) as HTMLElement; ) as HTMLElement;
jest.spyOn(target, 'clientHeight', 'get').mockImplementation(() => 100); jest.spyOn(target, 'clientHeight', 'get').mockImplementation(() => 100);
@ -155,12 +155,12 @@ describe('droptarget', () => {
fireEvent.dragOver(element); fireEvent.dragOver(element);
let viewQuery = element.querySelectorAll( let viewQuery = element.querySelectorAll(
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection' '.drop-target > .drop-target-dropzone > .drop-target-selection'
); );
expect(viewQuery.length).toBe(1); expect(viewQuery.length).toBe(1);
const target = element.querySelector( const target = element.querySelector(
'.dv-drop-target-dropzone' '.drop-target-dropzone'
) as HTMLElement; ) as HTMLElement;
jest.spyOn(target, 'clientHeight', 'get').mockImplementation(() => 100); jest.spyOn(target, 'clientHeight', 'get').mockImplementation(() => 100);
@ -187,13 +187,13 @@ describe('droptarget', () => {
} }
viewQuery = element.querySelectorAll( viewQuery = element.querySelectorAll(
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection' '.drop-target > .drop-target-dropzone > .drop-target-selection'
); );
expect(viewQuery.length).toBe(1); expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe('left'); expect(droptarget.state).toBe('left');
check( check(
element element
.getElementsByClassName('dv-drop-target-selection') .getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement, .item(0) as HTMLDivElement,
{ {
top: '0px', top: '0px',
@ -209,13 +209,13 @@ describe('droptarget', () => {
); );
viewQuery = element.querySelectorAll( viewQuery = element.querySelectorAll(
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection' '.drop-target > .drop-target-dropzone > .drop-target-selection'
); );
expect(viewQuery.length).toBe(1); expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe('top'); expect(droptarget.state).toBe('top');
check( check(
element element
.getElementsByClassName('dv-drop-target-selection') .getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement, .item(0) as HTMLDivElement,
{ {
top: '0px', top: '0px',
@ -231,13 +231,13 @@ describe('droptarget', () => {
); );
viewQuery = element.querySelectorAll( viewQuery = element.querySelectorAll(
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection' '.drop-target > .drop-target-dropzone > .drop-target-selection'
); );
expect(viewQuery.length).toBe(1); expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe('bottom'); expect(droptarget.state).toBe('bottom');
check( check(
element element
.getElementsByClassName('dv-drop-target-selection') .getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement, .item(0) as HTMLDivElement,
{ {
top: '50%', top: '50%',
@ -253,13 +253,13 @@ describe('droptarget', () => {
); );
viewQuery = element.querySelectorAll( viewQuery = element.querySelectorAll(
'.dv-drop-target > .dv-drop-target-dropzone > .dv-drop-target-selection' '.drop-target > .drop-target-dropzone > .drop-target-selection'
); );
expect(viewQuery.length).toBe(1); expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe('right'); expect(droptarget.state).toBe('right');
check( check(
element element
.getElementsByClassName('dv-drop-target-selection') .getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement, .item(0) as HTMLDivElement,
{ {
top: '0px', top: '0px',
@ -276,14 +276,14 @@ describe('droptarget', () => {
expect( expect(
( (
element element
.getElementsByClassName('dv-drop-target-selection') .getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement .item(0) as HTMLDivElement
).style.transform ).style.transform
).toBe(''); ).toBe('');
fireEvent.dragLeave(target); fireEvent.dragLeave(target);
expect(droptarget.state).toBe('center'); expect(droptarget.state).toBe('center');
viewQuery = element.querySelectorAll('.dv-drop-target'); viewQuery = element.querySelectorAll('.drop-target');
expect(viewQuery.length).toBe(0); expect(viewQuery.length).toBe(0);
}); });

View File

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

View File

@ -1,4 +1,5 @@
import { fireEvent } from '@testing-library/dom'; import { fireEvent } from '@testing-library/dom';
import { Emitter, Event } from '../../../../events';
import { ContentContainer } from '../../../../dockview/components/panel/content'; import { ContentContainer } from '../../../../dockview/components/panel/content';
import { import {
GroupPanelPartInitParameters, GroupPanelPartInitParameters,
@ -9,9 +10,9 @@ import { PanelUpdateEvent } from '../../../../panel/types';
import { IDockviewPanel } from '../../../../dockview/dockviewPanel'; import { IDockviewPanel } from '../../../../dockview/dockviewPanel';
import { IDockviewPanelModel } from '../../../../dockview/dockviewPanelModel'; import { IDockviewPanelModel } from '../../../../dockview/dockviewPanelModel';
import { DockviewComponent } from '../../../../dockview/dockviewComponent'; import { DockviewComponent } from '../../../../dockview/dockviewComponent';
import { OverlayRenderContainer } from '../../../../overlayRenderContainer';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanelModel'; import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanelModel';
import { OverlayRenderContainer } from '../../../../overlay/overlayRenderContainer';
class TestContentRenderer class TestContentRenderer
extends CompositeDisposable extends CompositeDisposable
@ -57,8 +58,7 @@ describe('contentContainer', () => {
const disposable = new CompositeDisposable(); const disposable = new CompositeDisposable();
const overlayRenderContainer = new OverlayRenderContainer( const overlayRenderContainer = new OverlayRenderContainer(
document.createElement('div'), document.createElement('div')
fromPartial<DockviewComponent>({})
); );
const cut = new ContentContainer( const cut = new ContentContainer(

View File

@ -8,7 +8,6 @@ import { DockviewGroupPanel } from '../../../dockview/dockviewGroupPanel';
import { DockviewGroupPanelModel } from '../../../dockview/dockviewGroupPanelModel'; import { DockviewGroupPanelModel } from '../../../dockview/dockviewGroupPanelModel';
import { Tab } from '../../../dockview/components/tab/tab'; import { Tab } from '../../../dockview/components/tab/tab';
import { IDockviewPanel } from '../../../dockview/dockviewPanel'; import { IDockviewPanel } from '../../../dockview/dockviewPanel';
import { fromPartial } from '@total-typescript/shoehorn';
describe('tab', () => { describe('tab', () => {
test('that empty tab has inactive-tab class', () => { test('that empty tab has inactive-tab class', () => {
@ -21,7 +20,7 @@ describe('tab', () => {
new groupMock() new groupMock()
); );
expect(cut.element.className).toBe('dv-tab dv-inactive-tab'); expect(cut.element.className).toBe('tab inactive-tab');
}); });
test('that active tab has active-tab class', () => { test('that active tab has active-tab class', () => {
@ -35,10 +34,10 @@ describe('tab', () => {
); );
cut.setActive(true); cut.setActive(true);
expect(cut.element.className).toBe('dv-tab dv-active-tab'); expect(cut.element.className).toBe('tab active-tab');
cut.setActive(false); cut.setActive(false);
expect(cut.element.className).toBe('dv-tab dv-inactive-tab'); expect(cut.element.className).toBe('tab inactive-tab');
}); });
test('that an external event does not render a drop target and calls through to the group model', () => { test('that an external event does not render a drop target and calls through to the group model', () => {
@ -47,10 +46,15 @@ describe('tab', () => {
id: 'testcomponentid', id: 'testcomponentid',
}; };
}); });
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = fromPartial<DockviewGroupPanelModel>({ const groupView = new groupviewMock() as DockviewGroupPanelModel;
canDisplayOverlay: jest.fn(),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => { const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return { return {
@ -68,33 +72,38 @@ describe('tab', () => {
groupPanel groupPanel
); );
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation( jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation( jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
() => 100 () => 100
); );
fireEvent.dragEnter(cut.element); fireEvent.dragEnter(cut.element);
fireEvent.dragOver(cut.element); fireEvent.dragOver(cut.element);
expect(groupView.canDisplayOverlay).toHaveBeenCalled(); expect(groupView.canDisplayOverlay).toBeCalled();
expect( expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(0); ).toBe(0);
}); });
test('that if you drag over yourself a drop target is shown', () => { test('that if you drag over yourself no drop target is shown', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => { const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return { return {
id: 'testcomponentid', id: 'testcomponentid',
}; };
}); });
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = fromPartial<DockviewGroupPanelModel>({ const groupView = new groupviewMock() as DockviewGroupPanelModel;
canDisplayOverlay: jest.fn(),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => { const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return { return {
@ -112,10 +121,10 @@ describe('tab', () => {
groupPanel groupPanel
); );
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation( jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation( jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
() => 100 () => 100
); );
@ -127,11 +136,11 @@ describe('tab', () => {
fireEvent.dragEnter(cut.element); fireEvent.dragEnter(cut.element);
fireEvent.dragOver(cut.element); fireEvent.dragOver(cut.element);
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0); expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
expect( expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(1); ).toBe(0);
}); });
test('that if you drag over another tab a drop target is shown', () => { test('that if you drag over another tab a drop target is shown', () => {
@ -166,10 +175,10 @@ describe('tab', () => {
groupPanel groupPanel
); );
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation( jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation( jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
() => 100 () => 100
); );
@ -184,7 +193,7 @@ describe('tab', () => {
expect(groupView.canDisplayOverlay).toBeCalledTimes(0); expect(groupView.canDisplayOverlay).toBeCalledTimes(0);
expect( expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(1); ).toBe(1);
}); });
@ -220,10 +229,10 @@ describe('tab', () => {
groupPanel groupPanel
); );
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation( jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation( jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
() => 100 () => 100
); );
@ -244,7 +253,7 @@ describe('tab', () => {
expect(groupView.canDisplayOverlay).toBeCalledTimes(1); expect(groupView.canDisplayOverlay).toBeCalledTimes(1);
expect( expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(0); ).toBe(0);
}); });
@ -280,10 +289,10 @@ describe('tab', () => {
groupPanel groupPanel
); );
jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation( jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation( jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation(
() => 100 () => 100
); );
@ -304,7 +313,7 @@ describe('tab', () => {
expect(groupView.canDisplayOverlay).toBeCalledTimes(1); expect(groupView.canDisplayOverlay).toBeCalledTimes(1);
expect( expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(0); ).toBe(0);
}); });
}); });

View File

@ -1,63 +0,0 @@
import { DockviewApi } from '../../../../api/component.api';
import { DockviewPanelApi, TitleEvent } from '../../../../api/dockviewPanelApi';
import { DefaultTab } from '../../../../dockview/components/tab/defaultTab';
import { fromPartial } from '@total-typescript/shoehorn';
import { Emitter } from '../../../../events';
import { fireEvent } from '@testing-library/dom';
describe('defaultTab', () => {
test('that title updates', () => {
const cut = new DefaultTab();
let el = cut.element.querySelector('.dv-default-tab-content');
expect(el).toBeTruthy();
expect(el!.textContent).toBe('');
const onDidTitleChange = new Emitter<TitleEvent>();
const api = fromPartial<DockviewPanelApi>({
onDidTitleChange: onDidTitleChange.event,
});
const containerApi = fromPartial<DockviewApi>({});
cut.init({
api,
containerApi,
params: {},
title: 'title_abc',
});
el = cut.element.querySelector('.dv-default-tab-content');
expect(el).toBeTruthy();
expect(el!.textContent).toBe('title_abc');
onDidTitleChange.fire({ title: 'title_def' });
expect(el!.textContent).toBe('title_def');
});
test('that click closes tab', () => {
const cut = new DefaultTab();
const api = fromPartial<DockviewPanelApi>({
onDidTitleChange: jest.fn(),
close: jest.fn(),
});
const containerApi = fromPartial<DockviewApi>({});
cut.init({
api,
containerApi,
params: {},
title: 'title_abc',
});
let el = cut.element.querySelector('.dv-default-tab-action');
fireEvent.pointerDown(el!);
expect(api.close).toHaveBeenCalledTimes(0);
fireEvent.click(el!);
expect(api.close).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,66 +0,0 @@
import { Tabs } from '../../../../dockview/components/titlebar/tabs';
import { fromPartial } from '@total-typescript/shoehorn';
import { DockviewGroupPanel } from '../../../../dockview/dockviewGroupPanel';
import { DockviewComponent } from '../../../../dockview/dockviewComponent';
describe('tabs', () => {
describe('disableCustomScrollbars', () => {
test('enabled by default', () => {
const cut = new Tabs(
fromPartial<DockviewGroupPanel>({}),
fromPartial<DockviewComponent>({
options: {},
}),
{
showTabsOverflowControl: true,
}
);
expect(
cut.element.querySelectorAll(
'.dv-scrollable > .dv-tabs-container'
).length
).toBe(1);
});
test('enabled when disabled flag is false', () => {
const cut = new Tabs(
fromPartial<DockviewGroupPanel>({}),
fromPartial<DockviewComponent>({
options: {
scrollbars: 'custom',
},
}),
{
showTabsOverflowControl: true,
}
);
expect(
cut.element.querySelectorAll(
'.dv-scrollable > .dv-tabs-container'
).length
).toBe(1);
});
test('disabled when disabled flag is true', () => {
const cut = new Tabs(
fromPartial<DockviewGroupPanel>({}),
fromPartial<DockviewComponent>({
options: {
scrollbars: 'native',
},
}),
{
showTabsOverflowControl: true,
}
);
expect(
cut.element.querySelectorAll(
'.dv-scrollable > .dv-tabs-container'
).length
).toBe(0);
});
});
});

View File

@ -10,15 +10,13 @@ import { fireEvent } from '@testing-library/dom';
import { TestPanel } from '../../dockviewGroupPanelModel.spec'; import { TestPanel } from '../../dockviewGroupPanelModel.spec';
import { IDockviewPanel } from '../../../../dockview/dockviewPanel'; import { IDockviewPanel } from '../../../../dockview/dockviewPanel';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { DockviewPanelApi } from '../../../../api/dockviewPanelApi';
describe('tabsContainer', () => { describe('tabsContainer', () => {
test('that an external event does not render a drop target and calls through to the group mode', () => { test('that an external event does not render a drop target and calls through to the group mode', () => {
const accessor = fromPartial<DockviewComponent>({ const accessor = fromPartial<DockviewComponent>({
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
options: {}, options: { parentElement: document.createElement('div') },
onDidOptionsChange: jest.fn(),
}); });
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>( const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -42,17 +40,17 @@ describe('tabsContainer', () => {
const cut = new TabsContainer(accessor, groupPanel); const cut = new TabsContainer(accessor, groupPanel);
const emptySpace = cut.element const emptySpace = cut.element
.getElementsByClassName('dv-void-container') .getElementsByClassName('void-container')
.item(0) as HTMLElement; .item(0);
if (!emptySpace!) { if (!emptySpace!) {
fail('element not found'); fail('element not found');
} }
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation( jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation( jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
() => 100 () => 100
); );
@ -62,7 +60,7 @@ describe('tabsContainer', () => {
expect(groupView.canDisplayOverlay).toHaveBeenCalled(); expect(groupView.canDisplayOverlay).toHaveBeenCalled();
expect( expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(0); ).toBe(0);
}); });
@ -71,18 +69,18 @@ describe('tabsContainer', () => {
id: 'testcomponentid', id: 'testcomponentid',
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
options: {}, options: { parentElement: document.createElement('div') },
onDidOptionsChange: jest.fn(),
}); });
const dropTargetContainer = document.createElement('div'); const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = fromPartial<DockviewGroupPanelModel>({ const groupView = new groupviewMock() as DockviewGroupPanelModel;
canDisplayOverlay: jest.fn(),
// dropTargetContainer: new DropTargetAnchorContainer(
// dropTargetContainer
// ),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => { const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return { return {
@ -97,17 +95,17 @@ describe('tabsContainer', () => {
const cut = new TabsContainer(accessor, groupPanel); const cut = new TabsContainer(accessor, groupPanel);
const emptySpace = cut.element const emptySpace = cut.element
.getElementsByClassName('dv-void-container') .getElementsByClassName('void-container')
.item(0) as HTMLElement; .item(0);
if (!emptySpace!) { if (!emptySpace!) {
fail('element not found'); fail('element not found');
} }
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation( jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation( jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
() => 100 () => 100
); );
@ -128,12 +126,8 @@ describe('tabsContainer', () => {
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0); expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
expect( expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(1); ).toBe(1);
// expect(
// dropTargetContainer.getElementsByClassName('dv-drop-target-anchor')
// .length
// ).toBe(1);
}); });
test('that dropping over the empty space should render a drop target', () => { test('that dropping over the empty space should render a drop target', () => {
@ -141,8 +135,7 @@ describe('tabsContainer', () => {
id: 'testcomponentid', id: 'testcomponentid',
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
options: {}, options: { parentElement: document.createElement('div') },
onDidOptionsChange: jest.fn(),
}); });
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>( const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -171,17 +164,17 @@ describe('tabsContainer', () => {
cut.openPanel(new TestPanel('panel2', jest.fn() as any)); cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element const emptySpace = cut.element
.getElementsByClassName('dv-void-container') .getElementsByClassName('void-container')
.item(0) as HTMLElement; .item(0);
if (!emptySpace!) { if (!emptySpace!) {
fail('element not found'); fail('element not found');
} }
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation( jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation( jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
() => 100 () => 100
); );
@ -196,7 +189,7 @@ describe('tabsContainer', () => {
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0); expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
expect( expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(1); ).toBe(1);
}); });
@ -205,8 +198,7 @@ describe('tabsContainer', () => {
id: 'testcomponentid', id: 'testcomponentid',
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
options: {}, options: { parentElement: document.createElement('div') },
onDidOptionsChange: jest.fn(),
}); });
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>( const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -235,17 +227,17 @@ describe('tabsContainer', () => {
cut.openPanel(new TestPanel('panel2', jest.fn() as any)); cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element const emptySpace = cut.element
.getElementsByClassName('dv-void-container') .getElementsByClassName('void-container')
.item(0) as HTMLElement; .item(0);
if (!emptySpace!) { if (!emptySpace!) {
fail('element not found'); fail('element not found');
} }
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation( jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation( jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
() => 100 () => 100
); );
@ -260,7 +252,7 @@ describe('tabsContainer', () => {
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0); expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0);
expect( expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(1); ).toBe(1);
}); });
@ -269,8 +261,7 @@ describe('tabsContainer', () => {
id: 'testcomponentid', id: 'testcomponentid',
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
options: {}, options: { parentElement: document.createElement('div') },
onDidOptionsChange: jest.fn(),
}); });
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>( const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -298,17 +289,17 @@ describe('tabsContainer', () => {
cut.openPanel(new TestPanel('panel2', jest.fn() as any)); cut.openPanel(new TestPanel('panel2', jest.fn() as any));
const emptySpace = cut.element const emptySpace = cut.element
.getElementsByClassName('dv-void-container') .getElementsByClassName('void-container')
.item(0) as HTMLElement; .item(0);
if (!emptySpace!) { if (!emptySpace!) {
fail('element not found'); fail('element not found');
} }
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation( jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation( jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
() => 100 () => 100
); );
@ -329,7 +320,7 @@ describe('tabsContainer', () => {
expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(1); expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(1);
expect( expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length cut.element.getElementsByClassName('drop-target-dropzone').length
).toBe(0); ).toBe(0);
}); });
@ -338,8 +329,7 @@ describe('tabsContainer', () => {
id: 'testcomponentid', id: 'testcomponentid',
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
options: {}, options: { parentElement: document.createElement('div') },
onDidOptionsChange: jest.fn(),
}); });
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -351,7 +341,7 @@ describe('tabsContainer', () => {
const cut = new TabsContainer(accessor, groupPanel); const cut = new TabsContainer(accessor, groupPanel);
let query = cut.element.querySelectorAll( let query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-left-actions-container' '.tabs-and-actions-container > .left-actions-container'
); );
expect(query.length).toBe(1); expect(query.length).toBe(1);
@ -364,7 +354,7 @@ describe('tabsContainer', () => {
cut.setLeftActionsElement(left); cut.setLeftActionsElement(left);
query = cut.element.querySelectorAll( query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-left-actions-container' '.tabs-and-actions-container > .left-actions-container'
); );
expect(query.length).toBe(1); expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe( expect(query[0].children.item(0)?.className).toBe(
@ -379,7 +369,7 @@ describe('tabsContainer', () => {
cut.setLeftActionsElement(left2); cut.setLeftActionsElement(left2);
query = cut.element.querySelectorAll( query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-left-actions-container' '.tabs-and-actions-container > .left-actions-container'
); );
expect(query.length).toBe(1); expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe( expect(query[0].children.item(0)?.className).toBe(
@ -391,7 +381,7 @@ describe('tabsContainer', () => {
cut.setLeftActionsElement(undefined); cut.setLeftActionsElement(undefined);
query = cut.element.querySelectorAll( query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-left-actions-container' '.tabs-and-actions-container > .left-actions-container'
); );
expect(query.length).toBe(1); expect(query.length).toBe(1);
@ -403,8 +393,7 @@ describe('tabsContainer', () => {
id: 'testcomponentid', id: 'testcomponentid',
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
options: {}, options: { parentElement: document.createElement('div') },
onDidOptionsChange: jest.fn(),
}); });
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -416,7 +405,7 @@ describe('tabsContainer', () => {
const cut = new TabsContainer(accessor, groupPanel); const cut = new TabsContainer(accessor, groupPanel);
let query = cut.element.querySelectorAll( let query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-right-actions-container' '.tabs-and-actions-container > .right-actions-container'
); );
expect(query.length).toBe(1); expect(query.length).toBe(1);
@ -429,7 +418,7 @@ describe('tabsContainer', () => {
cut.setRightActionsElement(right); cut.setRightActionsElement(right);
query = cut.element.querySelectorAll( query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-right-actions-container' '.tabs-and-actions-container > .right-actions-container'
); );
expect(query.length).toBe(1); expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe( expect(query[0].children.item(0)?.className).toBe(
@ -444,7 +433,7 @@ describe('tabsContainer', () => {
cut.setRightActionsElement(right2); cut.setRightActionsElement(right2);
query = cut.element.querySelectorAll( query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-right-actions-container' '.tabs-and-actions-container > .right-actions-container'
); );
expect(query.length).toBe(1); expect(query.length).toBe(1);
expect(query[0].children.item(0)?.className).toBe( expect(query[0].children.item(0)?.className).toBe(
@ -456,7 +445,7 @@ describe('tabsContainer', () => {
cut.setRightActionsElement(undefined); cut.setRightActionsElement(undefined);
query = cut.element.querySelectorAll( query = cut.element.querySelectorAll(
'.dv-tabs-and-actions-container > .dv-right-actions-container' '.tabs-and-actions-container > .right-actions-container'
); );
expect(query.length).toBe(1); expect(query.length).toBe(1);
@ -465,13 +454,11 @@ describe('tabsContainer', () => {
test('that a tab will become floating when clicked if not floating and shift is selected', () => { test('that a tab will become floating when clicked if not floating and shift is selected', () => {
const accessor = fromPartial<DockviewComponent>({ const accessor = fromPartial<DockviewComponent>({
options: {}, options: { parentElement: document.createElement('div') },
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
element: document.createElement('div'), element: document.createElement('div'),
addFloatingGroup: jest.fn(), addFloatingGroup: jest.fn(),
doSetGroupActive: jest.fn(),
onDidOptionsChange: jest.fn(),
}); });
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -484,7 +471,7 @@ describe('tabsContainer', () => {
const cut = new TabsContainer(accessor, groupPanel); const cut = new TabsContainer(accessor, groupPanel);
const container = cut.element.querySelector('.dv-void-container')!; const container = cut.element.querySelector('.void-container')!;
expect(container).toBeTruthy(); expect(container).toBeTruthy();
jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation( jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation(
@ -499,20 +486,22 @@ describe('tabsContainer', () => {
return { top: 10, left: 20, width: 0, height: 0 } as any; return { top: 10, left: 20, width: 0, height: 0 } as any;
}); });
const event = new KeyboardEvent('pointerdown', { shiftKey: true }); const event = new KeyboardEvent('mousedown', { shiftKey: true });
const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault'); const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault');
fireEvent(container, event); fireEvent(container, event);
expect(accessor.doSetGroupActive).toHaveBeenCalledWith(groupPanel); expect(accessor.addFloatingGroup).toHaveBeenCalledWith(
expect(accessor.addFloatingGroup).toHaveBeenCalledWith(groupPanel, { groupPanel,
x: 100, {
y: 60, x: 100,
inDragMode: true, y: 60,
}); },
{ inDragMode: true }
);
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(1); expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(1);
expect(eventPreventDefaultSpy).toHaveBeenCalledTimes(1); expect(eventPreventDefaultSpy).toHaveBeenCalledTimes(1);
const event2 = new KeyboardEvent('pointerdown', { shiftKey: false }); const event2 = new KeyboardEvent('mousedown', { shiftKey: false });
const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault'); const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(container, event2); fireEvent(container, event2);
@ -522,13 +511,11 @@ describe('tabsContainer', () => {
test('that a tab that is already floating cannot be floated again', () => { test('that a tab that is already floating cannot be floated again', () => {
const accessor = fromPartial<DockviewComponent>({ const accessor = fromPartial<DockviewComponent>({
options: {}, options: { parentElement: document.createElement('div') },
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
element: document.createElement('div'), element: document.createElement('div'),
addFloatingGroup: jest.fn(), addFloatingGroup: jest.fn(),
doSetGroupActive: jest.fn(),
onDidOptionsChange: jest.fn(),
}); });
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -541,7 +528,7 @@ describe('tabsContainer', () => {
const cut = new TabsContainer(accessor, groupPanel); const cut = new TabsContainer(accessor, groupPanel);
const container = cut.element.querySelector('.dv-void-container')!; const container = cut.element.querySelector('.void-container')!;
expect(container).toBeTruthy(); expect(container).toBeTruthy();
jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation( jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation(
@ -556,15 +543,14 @@ describe('tabsContainer', () => {
return { top: 10, left: 20, width: 0, height: 0 } as any; return { top: 10, left: 20, width: 0, height: 0 } as any;
}); });
const event = new KeyboardEvent('pointerdown', { shiftKey: true }); const event = new KeyboardEvent('mousedown', { shiftKey: true });
const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault'); const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault');
fireEvent(container, event); fireEvent(container, event);
expect(accessor.doSetGroupActive).toHaveBeenCalledWith(groupPanel);
expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(0); expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(0);
expect(eventPreventDefaultSpy).toHaveBeenCalledTimes(0); expect(eventPreventDefaultSpy).toHaveBeenCalledTimes(0);
const event2 = new KeyboardEvent('pointerdown', { shiftKey: false }); const event2 = new KeyboardEvent('mousedown', { shiftKey: false });
const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault'); const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(container, event2); fireEvent(container, event2);
@ -574,13 +560,12 @@ describe('tabsContainer', () => {
test('that selecting a tab with shift down will move that tab into a new floating group', () => { test('that selecting a tab with shift down will move that tab into a new floating group', () => {
const accessor = fromPartial<DockviewComponent>({ const accessor = fromPartial<DockviewComponent>({
options: {}, options: { parentElement: document.createElement('div') },
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
element: document.createElement('div'), element: document.createElement('div'),
addFloatingGroup: jest.fn(), addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(), getGroupPanel: jest.fn(),
onDidOptionsChange: jest.fn(),
}); });
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -610,10 +595,10 @@ describe('tabsContainer', () => {
const panel = createPanel('test_id'); const panel = createPanel('test_id');
cut.openPanel(panel); cut.openPanel(panel);
const el = cut.element.querySelector('.dv-tab')!; const el = cut.element.querySelector('.tab')!;
expect(el).toBeTruthy(); expect(el).toBeTruthy();
const event = new KeyboardEvent('pointerdown', { shiftKey: true }); const event = new KeyboardEvent('mousedown', { shiftKey: true });
const preventDefaultSpy = jest.spyOn(event, 'preventDefault'); const preventDefaultSpy = jest.spyOn(event, 'preventDefault');
fireEvent(el, event); fireEvent(el, event);
@ -631,13 +616,12 @@ describe('tabsContainer', () => {
test('pre header actions', () => { test('pre header actions', () => {
const accessor = fromPartial<DockviewComponent>({ const accessor = fromPartial<DockviewComponent>({
options: {}, options: { parentElement: document.createElement('div') },
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
element: document.createElement('div'), element: document.createElement('div'),
addFloatingGroup: jest.fn(), addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(), getGroupPanel: jest.fn(),
onDidOptionsChange: jest.fn(),
}); });
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -670,14 +654,14 @@ describe('tabsContainer', () => {
const panel = new panelMock('test_id'); const panel = new panelMock('test_id');
cut.openPanel(panel); cut.openPanel(panel);
let result = cut.element.querySelector('.dv-pre-actions-container'); let result = cut.element.querySelector('.pre-actions-container');
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0); expect(result!.childNodes.length).toBe(0);
const actions = document.createElement('div'); const actions = document.createElement('div');
cut.setPrefixActionsElement(actions); cut.setPrefixActionsElement(actions);
result = cut.element.querySelector('.dv-pre-actions-container'); result = cut.element.querySelector('.pre-actions-container');
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1); expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(actions); expect(result!.childNodes.item(0)).toBe(actions);
@ -685,27 +669,26 @@ describe('tabsContainer', () => {
const updatedActions = document.createElement('div'); const updatedActions = document.createElement('div');
cut.setPrefixActionsElement(updatedActions); cut.setPrefixActionsElement(updatedActions);
result = cut.element.querySelector('.dv-pre-actions-container'); result = cut.element.querySelector('.pre-actions-container');
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1); expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(updatedActions); expect(result!.childNodes.item(0)).toBe(updatedActions);
cut.setPrefixActionsElement(undefined); cut.setPrefixActionsElement(undefined);
result = cut.element.querySelector('.dv-pre-actions-container'); result = cut.element.querySelector('.pre-actions-container');
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0); expect(result!.childNodes.length).toBe(0);
}); });
test('left header actions', () => { test('left header actions', () => {
const accessor = fromPartial<DockviewComponent>({ const accessor = fromPartial<DockviewComponent>({
options: {}, options: { parentElement: document.createElement('div') },
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
element: document.createElement('div'), element: document.createElement('div'),
addFloatingGroup: jest.fn(), addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(), getGroupPanel: jest.fn(),
onDidOptionsChange: jest.fn(),
}); });
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -738,14 +721,14 @@ describe('tabsContainer', () => {
const panel = new panelMock('test_id'); const panel = new panelMock('test_id');
cut.openPanel(panel); cut.openPanel(panel);
let result = cut.element.querySelector('.dv-left-actions-container'); let result = cut.element.querySelector('.left-actions-container');
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0); expect(result!.childNodes.length).toBe(0);
const actions = document.createElement('div'); const actions = document.createElement('div');
cut.setLeftActionsElement(actions); cut.setLeftActionsElement(actions);
result = cut.element.querySelector('.dv-left-actions-container'); result = cut.element.querySelector('.left-actions-container');
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1); expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(actions); expect(result!.childNodes.item(0)).toBe(actions);
@ -753,27 +736,26 @@ describe('tabsContainer', () => {
const updatedActions = document.createElement('div'); const updatedActions = document.createElement('div');
cut.setLeftActionsElement(updatedActions); cut.setLeftActionsElement(updatedActions);
result = cut.element.querySelector('.dv-left-actions-container'); result = cut.element.querySelector('.left-actions-container');
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1); expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(updatedActions); expect(result!.childNodes.item(0)).toBe(updatedActions);
cut.setLeftActionsElement(undefined); cut.setLeftActionsElement(undefined);
result = cut.element.querySelector('.dv-left-actions-container'); result = cut.element.querySelector('.left-actions-container');
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0); expect(result!.childNodes.length).toBe(0);
}); });
test('right header actions', () => { test('right header actions', () => {
const accessor = fromPartial<DockviewComponent>({ const accessor = fromPartial<DockviewComponent>({
options: {}, options: { parentElement: document.createElement('div') },
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
element: document.createElement('div'), element: document.createElement('div'),
addFloatingGroup: jest.fn(), addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(), getGroupPanel: jest.fn(),
onDidOptionsChange: jest.fn(),
}); });
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => { const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
@ -806,14 +788,14 @@ describe('tabsContainer', () => {
const panel = new panelMock('test_id'); const panel = new panelMock('test_id');
cut.openPanel(panel); cut.openPanel(panel);
let result = cut.element.querySelector('.dv-right-actions-container'); let result = cut.element.querySelector('.right-actions-container');
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0); expect(result!.childNodes.length).toBe(0);
const actions = document.createElement('div'); const actions = document.createElement('div');
cut.setRightActionsElement(actions); cut.setRightActionsElement(actions);
result = cut.element.querySelector('.dv-right-actions-container'); result = cut.element.querySelector('.right-actions-container');
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1); expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(actions); expect(result!.childNodes.item(0)).toBe(actions);
@ -821,47 +803,15 @@ describe('tabsContainer', () => {
const updatedActions = document.createElement('div'); const updatedActions = document.createElement('div');
cut.setRightActionsElement(updatedActions); cut.setRightActionsElement(updatedActions);
result = cut.element.querySelector('.dv-right-actions-container'); result = cut.element.querySelector('.right-actions-container');
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(1); expect(result!.childNodes.length).toBe(1);
expect(result!.childNodes.item(0)).toBe(updatedActions); expect(result!.childNodes.item(0)).toBe(updatedActions);
cut.setRightActionsElement(undefined); cut.setRightActionsElement(undefined);
result = cut.element.querySelector('.dv-right-actions-container'); result = cut.element.querySelector('.right-actions-container');
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(result!.childNodes.length).toBe(0); expect(result!.childNodes.length).toBe(0);
}); });
test('class dv-single-tab is present when only one tab exists`', () => {
const cut = new TabsContainer(
fromPartial<DockviewComponent>({
options: {},
onDidOptionsChange: jest.fn(),
}),
fromPartial<DockviewGroupPanel>({})
);
expect(cut.element.classList.contains('dv-single-tab')).toBeFalsy();
const panel1 = new TestPanel(
'panel_1',
fromPartial<DockviewPanelApi>({})
);
cut.openPanel(panel1);
expect(cut.element.classList.contains('dv-single-tab')).toBeTruthy();
const panel2 = new TestPanel(
'panel_2',
fromPartial<DockviewPanelApi>({})
);
cut.openPanel(panel2);
expect(cut.element.classList.contains('dv-single-tab')).toBeFalsy();
cut.closePanel(panel1);
expect(cut.element.classList.contains('dv-single-tab')).toBeTruthy();
cut.closePanel(panel2);
expect(cut.element.classList.contains('dv-single-tab')).toBeFalsy();
});
}); });

View File

@ -1,20 +0,0 @@
import { VoidContainer } from '../../../../dockview/components/titlebar/voidContainer';
import { fromPartial } from '@total-typescript/shoehorn';
import { DockviewComponent } from '../../../../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../../../../dockview/dockviewGroupPanel';
import { fireEvent } from '@testing-library/dom';
describe('voidContainer', () => {
test('that `pointerDown` triggers activation', () => {
const accessor = fromPartial<DockviewComponent>({
doSetGroupActive: jest.fn(),
});
const group = fromPartial<DockviewGroupPanel>({});
const cut = new VoidContainer(accessor, group);
expect(accessor.doSetGroupActive).not.toHaveBeenCalled();
fireEvent.pointerDown(cut.element);
expect(accessor.doSetGroupActive).toHaveBeenCalledWith(group);
});
});

View File

@ -0,0 +1,27 @@
import { DockviewApi } from '../../../../api/component.api';
import { Watermark } from '../../../../dockview/components/watermark/watermark';
describe('watermark', () => {
test('that the group is closed when the close button is clicked', () => {
const cut = new Watermark();
const mockApi = jest.fn<Partial<DockviewApi>, any[]>(() => {
return {
removeGroup: jest.fn(),
};
});
const api = <DockviewApi>new mockApi();
const group = jest.fn() as any;
cut.init({ containerApi: api });
cut.updateParentGroup(group, true);
const closeEl = cut.element.querySelector('.close-action')!;
expect(closeEl).toBeTruthy();
closeEl.dispatchEvent(new Event('click'));
expect(api.removeGroup).toHaveBeenCalledWith(group);
});
});

View File

@ -1,190 +0,0 @@
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { fromPartial } from '@total-typescript/shoehorn';
import { GroupOptions } from '../../dockview/dockviewGroupPanelModel';
import { DockviewPanel, IDockviewPanel } from '../../dockview/dockviewPanel';
import { DockviewPanelModelMock } from '../__mocks__/mockDockviewPanelModel';
import { IContentRenderer, ITabRenderer } from '../../dockview/types';
import { OverlayRenderContainer } from '../../overlay/overlayRenderContainer';
import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel';
import { ContentContainer } from '../../dockview/components/panel/content';
describe('dockviewGroupPanel', () => {
test('default minimum/maximium width/height', () => {
const accessor = fromPartial<DockviewComponent>({
onDidActivePanelChange: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const options = fromPartial<GroupOptions>({});
const cut = new DockviewGroupPanel(accessor, 'test_id', options);
expect(cut.minimumWidth).toBe(100);
expect(cut.minimumHeight).toBe(100);
expect(cut.maximumHeight).toBe(Number.MAX_SAFE_INTEGER);
expect(cut.maximumWidth).toBe(Number.MAX_SAFE_INTEGER);
});
test('that onDidActivePanelChange is configured at inline', () => {
const accessor = fromPartial<DockviewComponent>({
onDidActivePanelChange: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
api: {},
renderer: 'always',
overlayRenderContainer: {
attach: jest.fn(),
detatch: jest.fn(),
},
doSetGroupActive: jest.fn(),
onDidOptionsChange: jest.fn(),
});
const options = fromPartial<GroupOptions>({});
const cut = new DockviewGroupPanel(accessor, 'test_id', options);
let counter = 0;
cut.api.onDidActivePanelChange((event) => {
counter++;
});
cut.model.openPanel(
fromPartial<IDockviewPanel>({
updateParentGroup: jest.fn(),
view: {
tab: { element: document.createElement('div') },
content: new ContentContainer(accessor, cut.model),
},
api: {
renderer: 'onlyWhenVisible',
onDidTitleChange: jest.fn(),
onDidParametersChange: jest.fn(),
},
layout: jest.fn(),
runEvents: jest.fn(),
})
);
expect(counter).toBe(1);
});
test('group constraints', () => {
const accessor = fromPartial<DockviewComponent>({
onDidActivePanelChange: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
doSetGroupActive: jest.fn(),
overlayRenderContainer: fromPartial<OverlayRenderContainer>({
attach: jest.fn(),
detatch: jest.fn(),
}),
options: {},
onDidOptionsChange: jest.fn(),
});
const options = fromPartial<GroupOptions>({});
const cut = new DockviewGroupPanel(accessor, 'test_id', options);
cut.api.setConstraints({
minimumHeight: 10,
maximumHeight: 100,
minimumWidth: 20,
maximumWidth: 200,
});
// initial constraints
expect(cut.minimumWidth).toBe(20);
expect(cut.minimumHeight).toBe(10);
expect(cut.maximumHeight).toBe(100);
expect(cut.maximumWidth).toBe(200);
const panelModel = new DockviewPanelModelMock(
'content_component',
fromPartial<IContentRenderer>({
element: document.createElement('div'),
}),
'tab_component',
fromPartial<ITabRenderer>({
element: document.createElement('div'),
})
);
const panel = new DockviewPanel(
'panel_id',
'component_id',
undefined,
accessor,
accessor.api,
cut,
panelModel,
{
renderer: 'onlyWhenVisible',
minimumWidth: 21,
minimumHeight: 11,
maximumHeight: 101,
maximumWidth: 201,
}
);
cut.model.openPanel(panel);
// active panel constraints
expect(cut.minimumWidth).toBe(21);
expect(cut.minimumHeight).toBe(11);
expect(cut.maximumHeight).toBe(101);
expect(cut.maximumWidth).toBe(201);
const panel2 = new DockviewPanel(
'panel_id',
'component_id',
undefined,
accessor,
accessor.api,
cut,
panelModel,
{
renderer: 'onlyWhenVisible',
minimumWidth: 22,
minimumHeight: 12,
maximumHeight: 102,
maximumWidth: 202,
}
);
cut.model.openPanel(panel2);
// active panel constraints
expect(cut.minimumWidth).toBe(22);
expect(cut.minimumHeight).toBe(12);
expect(cut.maximumHeight).toBe(102);
expect(cut.maximumWidth).toBe(202);
const panel3 = new DockviewPanel(
'panel_id',
'component_id',
undefined,
accessor,
accessor.api,
cut,
panelModel,
{
renderer: 'onlyWhenVisible',
}
);
cut.model.openPanel(panel3);
// active panel without specified constraints so falls back to group constraints
expect(cut.minimumWidth).toBe(20);
expect(cut.minimumHeight).toBe(10);
expect(cut.maximumHeight).toBe(100);
expect(cut.maximumWidth).toBe(200);
});
});

View File

@ -9,22 +9,40 @@ import {
} from '../../dockview/types'; } from '../../dockview/types';
import { PanelUpdateEvent, Parameters } from '../../panel/types'; import { PanelUpdateEvent, Parameters } from '../../panel/types';
import { import {
DockviewGroupLocation,
DockviewGroupPanelModel, DockviewGroupPanelModel,
GroupOptions, GroupOptions,
} from '../../dockview/dockviewGroupPanelModel'; } from '../../dockview/dockviewGroupPanelModel';
import { fireEvent } from '@testing-library/dom'; import { fireEvent } from '@testing-library/dom';
import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer'; import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer';
import { CompositeDisposable } from '../../lifecycle'; import { CompositeDisposable } from '../../lifecycle';
import { DockviewPanelApi } from '../../api/dockviewPanelApi'; import {
ActiveGroupEvent,
DockviewPanelApi,
GroupChangedEvent,
RendererChangedEvent,
} from '../../api/dockviewPanelApi';
import { IDockviewPanel } from '../../dockview/dockviewPanel'; import { IDockviewPanel } from '../../dockview/dockviewPanel';
import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel'; import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { WatermarkRendererInitParameters } from '../../dockview/types'; import { WatermarkRendererInitParameters } from '../../dockview/types';
import { createOffsetDragOverEvent } from '../__test_utils__/utils'; import { createOffsetDragOverEvent } from '../__test_utils__/utils';
import { OverlayRenderContainer } from '../../overlay/overlayRenderContainer'; import {
import { Emitter } from '../../events'; DockviewPanelRenderer,
OverlayRenderContainer,
} from '../../overlayRenderContainer';
import { DockviewGroupPanelFloatingChangeEvent } from '../../api/dockviewGroupPanelApi';
import { SizeEvent } from '../../api/gridviewPanelApi';
import {
PanelDimensionChangeEvent,
FocusEvent,
VisibilityEvent,
ActiveEvent,
WillFocusEvent,
} from '../../api/panelApi';
import { Position } from '../../dnd/droptarget';
import { Emitter, Event } from '../../events';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { TabLocation } from '../../dockview/framework';
enum GroupChangeKind2 { enum GroupChangeKind2 {
ADD_PANEL, ADD_PANEL,
@ -37,16 +55,12 @@ class TestModel implements IDockviewPanelModel {
readonly contentComponent: string; readonly contentComponent: string;
readonly tab: ITabRenderer; readonly tab: ITabRenderer;
constructor(readonly id: string) { constructor(id: string) {
this.content = new TestHeaderPart(id); this.content = new TestHeaderPart(id);
this.contentComponent = id; this.contentComponent = id;
this.tab = new TestContentPart(id); this.tab = new TestContentPart(id);
} }
createTabRenderer(tabLocation: TabLocation): ITabRenderer {
return new TestHeaderPart(this.id);
}
update(event: PanelUpdateEvent): void { update(event: PanelUpdateEvent): void {
// //
} }
@ -102,6 +116,10 @@ class Watermark implements IWatermarkRenderer {
return {}; return {};
} }
updateParentGroup() {
//
}
dispose() { dispose() {
// //
} }
@ -258,7 +276,7 @@ describe('dockviewGroupPanelModel', () => {
}); });
dockview = fromPartial<DockviewComponent>({ dockview = fromPartial<DockviewComponent>({
options: {}, options: { parentElement: document.createElement('div') },
createWatermarkComponent: () => new Watermark(), createWatermarkComponent: () => new Watermark(),
doSetGroupActive: jest.fn(), doSetGroupActive: jest.fn(),
id: 'dockview-1', id: 'dockview-1',
@ -267,10 +285,8 @@ describe('dockviewGroupPanelModel', () => {
onDidAddPanel: () => ({ dispose: jest.fn() }), onDidAddPanel: () => ({ dispose: jest.fn() }),
onDidRemovePanel: () => ({ dispose: jest.fn() }), onDidRemovePanel: () => ({ dispose: jest.fn() }),
overlayRenderContainer: new OverlayRenderContainer( overlayRenderContainer: new OverlayRenderContainer(
document.createElement('div'), document.createElement('div')
fromPartial<DockviewComponent>({})
), ),
onDidOptionsChange: () => ({ dispose: jest.fn() }),
}); });
groupview = new DockviewGroupPanel(dockview, 'groupview-1', options); groupview = new DockviewGroupPanel(dockview, 'groupview-1', options);
@ -479,12 +495,12 @@ describe('dockviewGroupPanelModel', () => {
test('default', () => { test('default', () => {
let viewQuery = groupview.element.querySelectorAll( let viewQuery = groupview.element.querySelectorAll(
'.dv-groupview > .dv-tabs-and-actions-container' '.groupview > .tabs-and-actions-container'
); );
expect(viewQuery).toBeTruthy(); expect(viewQuery).toBeTruthy();
viewQuery = groupview.element.querySelectorAll( viewQuery = groupview.element.querySelectorAll(
'.dv-groupview > .dv-content-container' '.groupview > .content-container'
); );
expect(viewQuery).toBeTruthy(); expect(viewQuery).toBeTruthy();
}); });
@ -500,18 +516,19 @@ describe('dockviewGroupPanelModel', () => {
groupview.model.closeAllPanels(); groupview.model.closeAllPanels();
expect(removePanelMock).toHaveBeenCalledWith(panel1, undefined); expect(removePanelMock).toBeCalledWith(panel1);
expect(removePanelMock).toHaveBeenCalledWith(panel2, undefined); expect(removePanelMock).toBeCalledWith(panel2);
expect(removePanelMock).toHaveBeenCalledWith(panel3, undefined); expect(removePanelMock).toBeCalledWith(panel3);
}); });
test('closeAllPanels with no panels', () => { test('closeAllPanels with no panels', () => {
groupview.model.closeAllPanels(); groupview.model.closeAllPanels();
expect(removeGroupMock).toHaveBeenCalledWith(groupview); expect(removeGroupMock).toBeCalledWith(groupview);
}); });
test('that group is set on panel during onDidAddPanel event', () => { test('that group is set on panel during onDidAddPanel event', () => {
const cut = new DockviewComponent(document.createElement('div'), { const cut = new DockviewComponent({
parentElement: document.createElement('div'),
createComponent(options) { createComponent(options) {
switch (options.name) { switch (options.name) {
case 'component': case 'component':
@ -531,19 +548,17 @@ describe('dockviewGroupPanelModel', () => {
}); });
test('toJSON() default', () => { test('toJSON() default', () => {
const dockviewComponent = new DockviewComponent( const dockviewComponent = new DockviewComponent({
document.createElement('div'), parentElement: document.createElement('div'),
{ createComponent(options) {
createComponent(options) { switch (options.name) {
switch (options.name) { case 'component':
case 'component': return new TestContentPart(options.id);
return new TestContentPart(options.id); default:
default: throw new Error(`unsupported`);
throw new Error(`unsupported`); }
} },
}, });
}
);
const cut = new DockviewGroupPanelModel( const cut = new DockviewGroupPanelModel(
document.createElement('div'), document.createElement('div'),
@ -561,19 +576,17 @@ describe('dockviewGroupPanelModel', () => {
}); });
test('toJSON() locked and hideHeader', () => { test('toJSON() locked and hideHeader', () => {
const dockviewComponent = new DockviewComponent( const dockviewComponent = new DockviewComponent({
document.createElement('div'), parentElement: document.createElement('div'),
{ createComponent(options) {
createComponent(options) { switch (options.name) {
switch (options.name) { case 'component':
case 'component': return new TestContentPart(options.id);
return new TestContentPart(options.id); default:
default: throw new Error(`unsupported`);
throw new Error(`unsupported`); }
} },
}, });
}
);
const cut = new DockviewGroupPanelModel( const cut = new DockviewGroupPanelModel(
document.createElement('div'), document.createElement('div'),
@ -596,19 +609,17 @@ describe('dockviewGroupPanelModel', () => {
}); });
test("that openPanel with skipSetActive doesn't set panel to active", () => { test("that openPanel with skipSetActive doesn't set panel to active", () => {
const dockviewComponent = new DockviewComponent( const dockviewComponent = new DockviewComponent({
document.createElement('div'), parentElement: document.createElement('div'),
{ createComponent(options) {
createComponent(options) { switch (options.name) {
switch (options.name) { case 'component':
case 'component': return new TestContentPart(options.id);
return new TestContentPart(options.id); default:
default: throw new Error(`unsupported`);
throw new Error(`unsupported`); }
} },
}, });
}
);
const groupviewContainer = document.createElement('div'); const groupviewContainer = document.createElement('div');
const cut = new DockviewGroupPanelModel( const cut = new DockviewGroupPanelModel(
@ -619,7 +630,7 @@ describe('dockviewGroupPanelModel', () => {
null as any null as any
); );
const contentContainer = groupviewContainer const contentContainer = groupviewContainer
.getElementsByClassName('dv-content-container') .getElementsByClassName('content-container')
.item(0)!.childNodes; .item(0)!.childNodes;
const panel1 = new TestPanel('id_1', panelApi); const panel1 = new TestPanel('id_1', panelApi);
@ -648,11 +659,12 @@ describe('dockviewGroupPanelModel', () => {
test('that should not show drop target is external event', () => { test('that should not show drop target is external event', () => {
const accessor = fromPartial<DockviewComponent>({ const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid', id: 'testcomponentid',
options: {}, options: {
parentElement: document.createElement('div'),
},
getPanel: jest.fn(), getPanel: jest.fn(),
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
onDidOptionsChange: jest.fn(),
}); });
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>( const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -690,13 +702,13 @@ describe('dockviewGroupPanelModel', () => {
}); });
const element = container const element = container
.getElementsByClassName('dv-content-container') .getElementsByClassName('content-container')
.item(0)! as HTMLElement; .item(0)!;
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100); jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
fireEvent.dragEnter(element); fireEvent.dragEnter(element);
fireEvent.dragOver(element); fireEvent.dragOver(element);
@ -704,18 +716,19 @@ describe('dockviewGroupPanelModel', () => {
expect(counter).toBe(1); expect(counter).toBe(1);
expect( expect(
element.getElementsByClassName('dv-drop-target-dropzone').length element.getElementsByClassName('drop-target-dropzone').length
).toBe(0); ).toBe(0);
}); });
test('that the .locked behaviour is as', () => { test('that the .locked behaviour is as', () => {
const accessor = fromPartial<DockviewComponent>({ const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid', id: 'testcomponentid',
options: {}, options: {
parentElement: document.createElement('div'),
},
getPanel: jest.fn(), getPanel: jest.fn(),
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
onDidOptionsChange: jest.fn(),
}); });
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>( const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -751,13 +764,13 @@ describe('dockviewGroupPanelModel', () => {
}); });
const element = container const element = container
.getElementsByClassName('dv-content-container') .getElementsByClassName('content-container')
.item(0)! as HTMLElement; .item(0)!;
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100); jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
function run(value: number) { function run(value: number) {
fireEvent.dragEnter(element); fireEvent.dragEnter(element);
@ -771,7 +784,7 @@ describe('dockviewGroupPanelModel', () => {
cut.locked = false; cut.locked = false;
run(10); run(10);
expect( expect(
element.getElementsByClassName('dv-drop-target-dropzone').length element.getElementsByClassName('drop-target-dropzone').length
).toBe(1); ).toBe(1);
fireEvent.dragEnd(element); fireEvent.dragEnd(element);
@ -779,7 +792,7 @@ describe('dockviewGroupPanelModel', () => {
cut.locked = 'no-drop-target'; cut.locked = 'no-drop-target';
run(10); run(10);
expect( expect(
element.getElementsByClassName('dv-drop-target-dropzone').length element.getElementsByClassName('drop-target-dropzone').length
).toBe(0); ).toBe(0);
fireEvent.dragEnd(element); fireEvent.dragEnd(element);
@ -787,7 +800,7 @@ describe('dockviewGroupPanelModel', () => {
cut.locked = true; cut.locked = true;
run(10); run(10);
expect( expect(
element.getElementsByClassName('dv-drop-target-dropzone').length element.getElementsByClassName('drop-target-dropzone').length
).toBe(1); ).toBe(1);
fireEvent.dragEnd(element); fireEvent.dragEnd(element);
@ -795,29 +808,35 @@ describe('dockviewGroupPanelModel', () => {
cut.locked = true; cut.locked = true;
run(25); run(25);
expect( expect(
element.getElementsByClassName('dv-drop-target-dropzone').length element.getElementsByClassName('drop-target-dropzone').length
).toBe(0); ).toBe(0);
fireEvent.dragEnd(element); fireEvent.dragEnd(element);
}); });
test('that should show drop target if dropping on self', () => { test('that should not show drop target if dropping on self', () => {
const accessor = fromPartial<DockviewComponent>({ const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid', id: 'testcomponentid',
options: {}, options: {
parentElement: document.createElement('div'),
},
getPanel: jest.fn(), getPanel: jest.fn(),
doSetGroupActive: jest.fn(), doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
overlayRenderContainer: new OverlayRenderContainer( overlayRenderContainer: new OverlayRenderContainer(
document.createElement('div'), document.createElement('div')
fromPartial<DockviewComponent>({})
), ),
onDidOptionsChange: jest.fn(),
}); });
const groupView = fromPartial<DockviewGroupPanelModel>({ const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
canDisplayOverlay: jest.fn(), () => {
}); return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => { const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return { return {
@ -844,13 +863,13 @@ describe('dockviewGroupPanelModel', () => {
cut.openPanel(new TestPanel('panel1', panelApi)); cut.openPanel(new TestPanel('panel1', panelApi));
const element = container const element = container
.getElementsByClassName('dv-content-container') .getElementsByClassName('content-container')
.item(0)! as HTMLElement; .item(0)!;
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100); jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
LocalSelectionTransfer.getInstance().setData( LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')], [new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')],
@ -863,23 +882,23 @@ describe('dockviewGroupPanelModel', () => {
expect(counter).toBe(0); expect(counter).toBe(0);
expect( expect(
element.getElementsByClassName('dv-drop-target-dropzone').length element.getElementsByClassName('drop-target-dropzone').length
).toBe(1); ).toBe(0);
}); });
test('that should allow drop when dropping on self for same component id', () => { test('that should not allow drop when dropping on self for same component id', () => {
const accessor = fromPartial<DockviewComponent>({ const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid', id: 'testcomponentid',
options: {}, options: {
parentElement: document.createElement('div'),
},
getPanel: jest.fn(), getPanel: jest.fn(),
doSetGroupActive: jest.fn(), doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
overlayRenderContainer: new OverlayRenderContainer( overlayRenderContainer: new OverlayRenderContainer(
document.createElement('div'), document.createElement('div')
fromPartial<DockviewComponent>({})
), ),
onDidOptionsChange: jest.fn(),
}); });
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>( const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -918,13 +937,13 @@ describe('dockviewGroupPanelModel', () => {
cut.openPanel(new TestPanel('panel2', panelApi)); cut.openPanel(new TestPanel('panel2', panelApi));
const element = container const element = container
.getElementsByClassName('dv-content-container') .getElementsByClassName('content-container')
.item(0) as HTMLElement; .item(0)!;
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100); jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
LocalSelectionTransfer.getInstance().setData( LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')], [new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')],
@ -937,23 +956,23 @@ describe('dockviewGroupPanelModel', () => {
expect(counter).toBe(0); expect(counter).toBe(0);
expect( expect(
element.getElementsByClassName('dv-drop-target-dropzone').length element.getElementsByClassName('drop-target-dropzone').length
).toBe(1); ).toBe(0);
}); });
test('that should not allow drop when not dropping for different component id', () => { test('that should not allow drop when not dropping for different component id', () => {
const accessor = fromPartial<DockviewComponent>({ const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid', id: 'testcomponentid',
options: {}, options: {
parentElement: document.createElement('div'),
},
getPanel: jest.fn(), getPanel: jest.fn(),
doSetGroupActive: jest.fn(), doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
overlayRenderContainer: new OverlayRenderContainer( overlayRenderContainer: new OverlayRenderContainer(
document.createElement('div'), document.createElement('div')
fromPartial<DockviewComponent>({})
), ),
onDidOptionsChange: jest.fn(),
}); });
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>( const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
@ -992,13 +1011,13 @@ describe('dockviewGroupPanelModel', () => {
cut.openPanel(new TestPanel('panel2', panelApi)); cut.openPanel(new TestPanel('panel2', panelApi));
const element = container const element = container
.getElementsByClassName('dv-content-container') .getElementsByClassName('content-container')
.item(0) as HTMLElement; .item(0)!;
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
); );
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100); jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
LocalSelectionTransfer.getInstance().setData( LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('anothercomponentid', 'groupviewid', 'panel1')], [new PanelTransfer('anothercomponentid', 'groupviewid', 'panel1')],
@ -1011,7 +1030,7 @@ describe('dockviewGroupPanelModel', () => {
expect(counter).toBe(1); expect(counter).toBe(1);
expect( expect(
element.getElementsByClassName('dv-drop-target-dropzone').length element.getElementsByClassName('drop-target-dropzone').length
).toBe(0); ).toBe(0);
}); });
@ -1096,7 +1115,7 @@ describe('dockviewGroupPanelModel', () => {
container.getElementsByClassName('watermark-test-container').length container.getElementsByClassName('watermark-test-container').length
).toBe(0); ).toBe(0);
expect( expect(
container.getElementsByClassName('dv-tabs-and-actions-container') container.getElementsByClassName('tabs-and-actions-container')
.length .length
).toBe(1); ).toBe(1);

View File

@ -7,8 +7,24 @@ import { fromPartial } from '@total-typescript/shoehorn';
describe('dockviewPanel', () => { describe('dockviewPanel', () => {
test('update title', () => { test('update title', () => {
const api = fromPartial<DockviewApi>({}); const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
const accessor = fromPartial<DockviewComponent>({}); return {
onDidActiveChange: jest.fn(),
} as any;
});
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
};
});
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = fromPartial<DockviewGroupPanel>({ const group = fromPartial<DockviewGroupPanel>({
api: { api: {
onDidVisibilityChange: jest.fn(), onDidVisibilityChange: jest.fn(),
@ -16,11 +32,7 @@ describe('dockviewPanel', () => {
onDidActiveChange: jest.fn(), onDidActiveChange: jest.fn(),
}, },
}); });
const model = fromPartial<IDockviewPanelModel>({ const model = <IDockviewPanelModel>new panelModelMock();
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
});
const cut = new DockviewPanel( const cut = new DockviewPanel(
'fake-id', 'fake-id',
@ -55,8 +67,23 @@ describe('dockviewPanel', () => {
}); });
test('that .setTitle updates the title', () => { test('that .setTitle updates the title', () => {
const api = fromPartial<DockviewApi>({}); const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
const accessor = fromPartial<DockviewComponent>({}); return {
onDidActiveChange: jest.fn(),
} as any;
});
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
};
});
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = fromPartial<DockviewGroupPanel>({ const group = fromPartial<DockviewGroupPanel>({
api: { api: {
onDidVisibilityChange: jest.fn(), onDidVisibilityChange: jest.fn(),
@ -64,10 +91,7 @@ describe('dockviewPanel', () => {
onDidActiveChange: jest.fn(), onDidActiveChange: jest.fn(),
}, },
}); });
const model = fromPartial<IDockviewPanelModel>({ const model = <IDockviewPanelModel>new panelModelMock();
update: jest.fn(),
init: jest.fn(),
});
const cut = new DockviewPanel( const cut = new DockviewPanel(
'fake-id', 'fake-id',
@ -93,8 +117,22 @@ describe('dockviewPanel', () => {
}); });
test('dispose cleanup', () => { test('dispose cleanup', () => {
const api = fromPartial<DockviewApi>({}); const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
const accessor = fromPartial<DockviewComponent>({}); return {} as any;
});
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
};
});
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = fromPartial<DockviewGroupPanel>({ const group = fromPartial<DockviewGroupPanel>({
api: { api: {
onDidVisibilityChange: jest onDidVisibilityChange: jest
@ -108,11 +146,7 @@ describe('dockviewPanel', () => {
.mockReturnValue({ dispose: jest.fn() }), .mockReturnValue({ dispose: jest.fn() }),
}, },
}); });
const model = fromPartial<IDockviewPanelModel>({ const model = <IDockviewPanelModel>new panelModelMock();
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
});
const cut = new DockviewPanel( const cut = new DockviewPanel(
'fake-id', 'fake-id',
@ -135,8 +169,22 @@ describe('dockviewPanel', () => {
}); });
test('get params', () => { test('get params', () => {
const api = fromPartial<DockviewApi>({}); const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
const accessor = fromPartial<DockviewComponent>({}); return {} as any;
});
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
};
});
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = fromPartial<DockviewGroupPanel>({ const group = fromPartial<DockviewGroupPanel>({
api: { api: {
onDidVisibilityChange: jest.fn(), onDidVisibilityChange: jest.fn(),
@ -144,11 +192,7 @@ describe('dockviewPanel', () => {
onDidActiveChange: jest.fn(), onDidActiveChange: jest.fn(),
}, },
}); });
const model = fromPartial<IDockviewPanelModel>({ const model = <IDockviewPanelModel>new panelModelMock();
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
});
const cut = new DockviewPanel( const cut = new DockviewPanel(
'fake-id', 'fake-id',
@ -171,8 +215,22 @@ describe('dockviewPanel', () => {
}); });
test('setSize propagates to underlying group', () => { test('setSize propagates to underlying group', () => {
const api = fromPartial<DockviewApi>({}); const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
const accessor = fromPartial<DockviewComponent>({}); return {} as any;
});
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
};
});
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = fromPartial<DockviewGroupPanel>({ const group = fromPartial<DockviewGroupPanel>({
api: { api: {
onDidVisibilityChange: jest.fn(), onDidVisibilityChange: jest.fn(),
@ -181,11 +239,7 @@ describe('dockviewPanel', () => {
setSize: jest.fn(), setSize: jest.fn(),
}, },
}); });
const model = fromPartial<IDockviewPanelModel>({ const model = <IDockviewPanelModel>new panelModelMock();
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
});
const cut = new DockviewPanel( const cut = new DockviewPanel(
'fake-id', 'fake-id',
@ -202,16 +256,27 @@ describe('dockviewPanel', () => {
cut.api.setSize({ height: 123, width: 456 }); cut.api.setSize({ height: 123, width: 456 });
expect(group.api.setSize).toHaveBeenCalledWith({ expect(group.api.setSize).toBeCalledWith({ height: 123, width: 456 });
height: 123, expect(group.api.setSize).toBeCalledTimes(1);
width: 456,
});
expect(group.api.setSize).toHaveBeenCalledTimes(1);
}); });
test('updateParameter', () => { test('updateParameter', () => {
const api = fromPartial<DockviewApi>({}); const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
const accessor = fromPartial<DockviewComponent>({}); return {} as any;
});
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
});
const panelModelMock = jest.fn<Partial<IDockviewPanelModel>, []>(() => {
return {
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
};
});
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = fromPartial<DockviewGroupPanel>({ const group = fromPartial<DockviewGroupPanel>({
api: { api: {
onDidVisibilityChange: jest.fn(), onDidVisibilityChange: jest.fn(),
@ -219,11 +284,7 @@ describe('dockviewPanel', () => {
onDidActiveChange: jest.fn(), onDidActiveChange: jest.fn(),
}, },
}); });
const model = fromPartial<IDockviewPanelModel>({ const model = <IDockviewPanelModel>new panelModelMock();
update: jest.fn(),
init: jest.fn(),
dispose: jest.fn(),
});
const cut = new DockviewPanel( const cut = new DockviewPanel(
'fake-id', 'fake-id',
@ -244,9 +305,6 @@ describe('dockviewPanel', () => {
// update 'a' and add 'c' // update 'a' and add 'c'
cut.update({ params: { a: '-1', c: '3' } }); cut.update({ params: { a: '-1', c: '3' } });
expect(cut.params).toEqual({ a: '-1', b: '2', c: '3' }); expect(cut.params).toEqual({ a: '-1', b: '2', c: '3' });
expect(model.update).toHaveBeenCalledWith({
params: { a: '-1', b: '2', c: '3' },
});
cut.update({ params: { d: '4', e: '5', f: '6' } }); cut.update({ params: { d: '4', e: '5', f: '6' } });
expect(cut.params).toEqual({ expect(cut.params).toEqual({
@ -257,9 +315,6 @@ describe('dockviewPanel', () => {
e: '5', e: '5',
f: '6', f: '6',
}); });
expect(model.update).toHaveBeenCalledWith({
params: { a: '-1', b: '2', c: '3', d: '4', e: '5', f: '6' },
});
cut.update({ cut.update({
params: { params: {
@ -280,8 +335,5 @@ describe('dockviewPanel', () => {
g: '', g: '',
h: null, h: null,
}); });
expect(model.update).toHaveBeenCalledWith({
params: { a: '-1', b: '2', c: '3', d: '', e: null, g: '', h: null },
});
}); });
}); });

View File

@ -30,6 +30,7 @@ describe('dockviewGroupPanel', () => {
accessorMock = fromPartial<DockviewComponent>({ accessorMock = fromPartial<DockviewComponent>({
options: { options: {
parentElement: document.createElement('div'),
createComponent(options) { createComponent(options) {
switch (options.name) { switch (options.name) {
case 'contentComponent': case 'contentComponent':
@ -83,6 +84,7 @@ describe('dockviewGroupPanel', () => {
test('that the default tab is created', () => { test('that the default tab is created', () => {
accessorMock = fromPartial<DockviewComponent>({ accessorMock = fromPartial<DockviewComponent>({
options: { options: {
parentElement: document.createElement('div'),
createComponent(options) { createComponent(options) {
switch (options.name) { switch (options.name) {
case 'contentComponent': case 'contentComponent':
@ -115,6 +117,7 @@ describe('dockviewGroupPanel', () => {
test('that the provided default tab is chosen when no implementation is provided', () => { test('that the provided default tab is chosen when no implementation is provided', () => {
accessorMock = fromPartial<DockviewComponent>({ accessorMock = fromPartial<DockviewComponent>({
options: { options: {
parentElement: document.createElement('div'),
defaultTabComponent: 'tabComponent', defaultTabComponent: 'tabComponent',
createComponent(options) { createComponent(options) {
switch (options.name) { switch (options.name) {
@ -147,6 +150,7 @@ describe('dockviewGroupPanel', () => {
test('that is library default tab instance is created when no alternative exists', () => { test('that is library default tab instance is created when no alternative exists', () => {
accessorMock = fromPartial<DockviewComponent>({ accessorMock = fromPartial<DockviewComponent>({
options: { options: {
parentElement: document.createElement('div'),
createComponent(options) { createComponent(options) {
switch (options.name) { switch (options.name) {
case 'contentComponent': case 'contentComponent':
@ -170,6 +174,7 @@ describe('dockviewGroupPanel', () => {
test('that the default content is created', () => { test('that the default content is created', () => {
accessorMock = fromPartial<DockviewComponent>({ accessorMock = fromPartial<DockviewComponent>({
options: { options: {
parentElement: document.createElement('div'),
createComponent(options) { createComponent(options) {
switch (options.name) { switch (options.name) {
case 'contentComponent': case 'contentComponent':

View File

@ -1,5 +1,4 @@
import { import {
disableIframePointEvents,
isInDocument, isInDocument,
quasiDefaultPrevented, quasiDefaultPrevented,
quasiPreventDefault, quasiPreventDefault,
@ -46,38 +45,4 @@ describe('dom', () => {
expect(isInDocument(el2)).toBeTruthy(); expect(isInDocument(el2)).toBeTruthy();
}); });
test('disableIframePointEvents', () => {
const el1 = document.createElement('iframe');
const el2 = document.createElement('iframe');
const el3 = document.createElement('webview');
const el4 = document.createElement('webview');
document.body.appendChild(el1);
document.body.appendChild(el2);
document.body.appendChild(el3);
document.body.appendChild(el4);
el1.style.pointerEvents = 'inherit';
el3.style.pointerEvents = 'inherit';
expect(el1.style.pointerEvents).toBe('inherit');
expect(el2.style.pointerEvents).toBe('');
expect(el3.style.pointerEvents).toBe('inherit');
expect(el4.style.pointerEvents).toBe('');
const f = disableIframePointEvents();
expect(el1.style.pointerEvents).toBe('none');
expect(el2.style.pointerEvents).toBe('none');
expect(el3.style.pointerEvents).toBe('none');
expect(el4.style.pointerEvents).toBe('none');
f.release();
expect(el1.style.pointerEvents).toBe('inherit');
expect(el2.style.pointerEvents).toBe('');
expect(el3.style.pointerEvents).toBe('inherit');
expect(el4.style.pointerEvents).toBe('');
});
}); });

View File

@ -3,6 +3,7 @@ import {
Emitter, Emitter,
Event, Event,
addDisposableListener, addDisposableListener,
addDisposableWindowListener,
} from '../events'; } from '../events';
describe('events', () => { describe('events', () => {
@ -142,7 +143,7 @@ describe('events', () => {
expect(value).toBe(3); expect(value).toBe(3);
}); });
it('addDisposableListener with capture options', () => { it('addDisposableWindowListener with capture options', () => {
const element = { const element = {
addEventListener: jest.fn(), addEventListener: jest.fn(),
removeEventListener: jest.fn(), removeEventListener: jest.fn(),
@ -150,16 +151,16 @@ describe('events', () => {
const handler = jest.fn(); const handler = jest.fn();
const disposable = addDisposableListener( const disposable = addDisposableWindowListener(
element as any, element as any,
'pointerdown', 'mousedown',
handler, handler,
true true
); );
expect(element.addEventListener).toBeCalledTimes(1); expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith( expect(element.addEventListener).toHaveBeenCalledWith(
'pointerdown', 'mousedown',
handler, handler,
true true
); );
@ -170,13 +171,13 @@ describe('events', () => {
expect(element.addEventListener).toBeCalledTimes(1); expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1); expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith( expect(element.removeEventListener).toBeCalledWith(
'pointerdown', 'mousedown',
handler, handler,
true true
); );
}); });
it('addDisposableListener without capture options', () => { it('addDisposableWindowListener without capture options', () => {
const element = { const element = {
addEventListener: jest.fn(), addEventListener: jest.fn(),
removeEventListener: jest.fn(), removeEventListener: jest.fn(),
@ -184,15 +185,15 @@ describe('events', () => {
const handler = jest.fn(); const handler = jest.fn();
const disposable = addDisposableListener( const disposable = addDisposableWindowListener(
element as any, element as any,
'pointerdown', 'mousedown',
handler handler
); );
expect(element.addEventListener).toBeCalledTimes(1); expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith( expect(element.addEventListener).toHaveBeenCalledWith(
'pointerdown', 'mousedown',
handler, handler,
undefined undefined
); );
@ -203,7 +204,7 @@ describe('events', () => {
expect(element.addEventListener).toBeCalledTimes(1); expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1); expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith( expect(element.removeEventListener).toBeCalledWith(
'pointerdown', 'mousedown',
handler, handler,
undefined undefined
); );
@ -219,14 +220,14 @@ describe('events', () => {
const disposable = addDisposableListener( const disposable = addDisposableListener(
element as any, element as any,
'pointerdown', 'mousedown',
handler, handler,
true true
); );
expect(element.addEventListener).toBeCalledTimes(1); expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith( expect(element.addEventListener).toHaveBeenCalledWith(
'pointerdown', 'mousedown',
handler, handler,
true true
); );
@ -237,7 +238,7 @@ describe('events', () => {
expect(element.addEventListener).toBeCalledTimes(1); expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1); expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith( expect(element.removeEventListener).toBeCalledWith(
'pointerdown', 'mousedown',
handler, handler,
true true
); );
@ -253,13 +254,13 @@ describe('events', () => {
const disposable = addDisposableListener( const disposable = addDisposableListener(
element as any, element as any,
'pointerdown', 'mousedown',
handler handler
); );
expect(element.addEventListener).toBeCalledTimes(1); expect(element.addEventListener).toBeCalledTimes(1);
expect(element.addEventListener).toHaveBeenCalledWith( expect(element.addEventListener).toHaveBeenCalledWith(
'pointerdown', 'mousedown',
handler, handler,
undefined undefined
); );
@ -270,7 +271,7 @@ describe('events', () => {
expect(element.addEventListener).toBeCalledTimes(1); expect(element.addEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledTimes(1); expect(element.removeEventListener).toBeCalledTimes(1);
expect(element.removeEventListener).toBeCalledWith( expect(element.removeEventListener).toBeCalledWith(
'pointerdown', 'mousedown',
handler, handler,
undefined undefined
); );

View File

@ -17,9 +17,13 @@ class TestPanel implements IGridPanelView {
_onDidChange = new Emitter<IViewSize | undefined>(); _onDidChange = new Emitter<IViewSize | undefined>();
readonly onDidChange = this._onDidChange.event; readonly onDidChange = this._onDidChange.event;
isVisible: boolean = true; get isActive(): boolean {
isActive: boolean = true; return true;
params: Parameters = {}; }
get params(): Parameters {
return {};
}
constructor( constructor(
public readonly id: string, public readonly id: string,
@ -66,10 +70,8 @@ class TestPanel implements IGridPanelView {
} }
class ClassUnderTest extends BaseGrid<TestPanel> { class ClassUnderTest extends BaseGrid<TestPanel> {
readonly gridview = this.gridview; constructor(options: BaseGridOptions) {
super(options);
constructor(parentElement: HTMLElement, options: BaseGridOptions) {
super(parentElement, options);
} }
doRemoveGroup( doRemoveGroup(
@ -105,47 +107,9 @@ class ClassUnderTest extends BaseGrid<TestPanel> {
} }
describe('baseComponentGridview', () => { describe('baseComponentGridview', () => {
test('that the container is not removed when grid is disposed', () => {
const root = document.createElement('div');
const container = document.createElement('div');
root.appendChild(container);
const cut = new ClassUnderTest(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: true,
});
cut.dispose();
expect(container.parentElement).toBe(root);
});
test('that .layout(...) force flag works', () => {
const cut = new ClassUnderTest(document.createElement('div'), {
orientation: Orientation.HORIZONTAL,
proportionalLayout: true,
});
const spy = jest.spyOn(cut.gridview, 'layout');
cut.layout(100, 100);
expect(spy).toHaveBeenCalledTimes(1);
cut.layout(100, 100, false);
expect(spy).toHaveBeenCalledTimes(1);
cut.layout(100, 100, true);
expect(spy).toHaveBeenCalledTimes(2);
cut.layout(150, 150, false);
expect(spy).toHaveBeenCalledTimes(3);
cut.layout(150, 150, true);
expect(spy).toHaveBeenCalledTimes(4);
});
test('can add group', () => { test('can add group', () => {
const cut = new ClassUnderTest(document.createElement('div'), { const cut = new ClassUnderTest({
parentElement: document.createElement('div'),
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
proportionalLayout: true, proportionalLayout: true,
}); });

View File

@ -5,7 +5,6 @@ import {
IGridView, IGridView,
IViewSize, IViewSize,
SerializedGridview, SerializedGridview,
getGridLocation,
orthogonal, orthogonal,
} from '../../gridview/gridview'; } from '../../gridview/gridview';
import { Orientation, Sizing } from '../../splitview/splitview'; import { Orientation, Sizing } from '../../splitview/splitview';
@ -19,7 +18,7 @@ class MockGridview implements IGridView {
IViewSize | undefined IViewSize | undefined
>().event; >().event;
element: HTMLElement = document.createElement('div'); element: HTMLElement = document.createElement('div');
isVisible: boolean = true;
width: number = 0; width: number = 0;
height: number = 0; height: number = 0;
@ -1106,102 +1105,4 @@ describe('gridview', () => {
expect(gridview.hasMaximizedView()).toBeFalsy(); expect(gridview.hasMaximizedView()).toBeFalsy();
}); });
test('visibility check', () => {
const gridview = new Gridview(
true,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
const view1 = new MockGridview('1');
const view2 = new MockGridview('2');
const view3 = new MockGridview('3');
const view4 = new MockGridview('4');
const view5 = new MockGridview('5');
const view6 = new MockGridview('6');
gridview.addView(view1, Sizing.Distribute, [0]);
gridview.addView(view2, Sizing.Distribute, [1]);
gridview.addView(view3, Sizing.Distribute, [1, 1]);
gridview.addView(view4, Sizing.Distribute, [1, 1, 0]);
gridview.addView(view5, Sizing.Distribute, [1, 1, 0, 0]);
gridview.addView(view6, Sizing.Distribute, [1, 1, 0, 0, 0]);
/**
* _____________________________________________
* | | |
* | | 2 |
* | | |
* | 1 |_______________________|
* | | | 4 |
* | | 3 |_____________|
* | | | 5 | 6 |
* |_____________________|_________|______|______|
*/
function assertVisibility(visibility: boolean[]) {
expect(gridview.isViewVisible(getGridLocation(view1.element))).toBe(
visibility[0]
);
expect(gridview.isViewVisible(getGridLocation(view2.element))).toBe(
visibility[1]
);
expect(gridview.isViewVisible(getGridLocation(view3.element))).toBe(
visibility[2]
);
expect(gridview.isViewVisible(getGridLocation(view4.element))).toBe(
visibility[3]
);
expect(gridview.isViewVisible(getGridLocation(view5.element))).toBe(
visibility[4]
);
expect(gridview.isViewVisible(getGridLocation(view6.element))).toBe(
visibility[5]
);
}
// hide each view one by one
assertVisibility([true, true, true, true, true, true]);
gridview.setViewVisible(getGridLocation(view5.element), false);
assertVisibility([true, true, true, true, false, true]);
gridview.setViewVisible(getGridLocation(view4.element), false);
assertVisibility([true, true, true, false, false, true]);
gridview.setViewVisible(getGridLocation(view1.element), false);
assertVisibility([false, true, true, false, false, true]);
gridview.setViewVisible(getGridLocation(view2.element), false);
assertVisibility([false, false, true, false, false, true]);
gridview.setViewVisible(getGridLocation(view3.element), false);
assertVisibility([false, false, false, false, false, true]);
gridview.setViewVisible(getGridLocation(view6.element), false);
assertVisibility([false, false, false, false, false, false]);
// un-hide each view one by one
gridview.setViewVisible(getGridLocation(view1.element), true);
assertVisibility([true, false, false, false, false, false]);
gridview.setViewVisible(getGridLocation(view5.element), true);
assertVisibility([true, false, false, false, true, false]);
gridview.setViewVisible(getGridLocation(view6.element), true);
assertVisibility([true, false, false, false, true, true]);
gridview.setViewVisible(getGridLocation(view2.element), true);
assertVisibility([true, true, false, false, true, true]);
gridview.setViewVisible(getGridLocation(view3.element), true);
assertVisibility([true, true, true, false, true, true]);
gridview.setViewVisible(getGridLocation(view4.element), true);
assertVisibility([true, true, true, true, true, true]);
});
}); });

View File

@ -32,40 +32,12 @@ describe('gridview', () => {
container = document.createElement('div'); container = document.createElement('div');
}); });
test('update className', () => {
const gridview = new GridviewComponent(container, {
proportionalLayout: false,
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
className: 'test-a test-b',
});
expect(gridview.element.className).toBe('test-a test-b');
gridview.updateOptions({ className: 'test-b test-c' });
expect(gridview.element.className).toBe('test-b test-c');
});
test('added views are visible by default', () => { test('added views are visible by default', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false, proportionalLayout: false,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(800, 400); gridview.layout(800, 400);
@ -81,17 +53,11 @@ describe('gridview', () => {
}); });
test('remove panel', () => { test('remove panel', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false, proportionalLayout: false,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(800, 400); gridview.layout(800, 400);
@ -118,17 +84,11 @@ describe('gridview', () => {
}); });
test('active panel', () => { test('active panel', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false, proportionalLayout: false,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(800, 400); gridview.layout(800, 400);
@ -185,21 +145,13 @@ describe('gridview', () => {
}); });
test('deserialize and serialize a layout', () => { test('deserialize and serialize a layout', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false, proportionalLayout: false,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
expect(container.querySelectorAll('.dv-grid-view').length).toBe(1);
gridview.layout(800, 400); gridview.layout(800, 400);
gridview.fromJSON({ gridview.fromJSON({
grid: { grid: {
@ -244,9 +196,6 @@ describe('gridview', () => {
}, },
activePanel: 'panel_1', activePanel: 'panel_1',
}); });
expect(container.querySelectorAll('.dv-grid-view').length).toBe(1);
gridview.layout(800, 400, true); gridview.layout(800, 400, true);
const panel1 = gridview.getPanel('panel_1')!; const panel1 = gridview.getPanel('panel_1')!;
@ -324,17 +273,11 @@ describe('gridview', () => {
}); });
test('toJSON shouldnt fire any layout events', () => { test('toJSON shouldnt fire any layout events', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false, proportionalLayout: false,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(1000, 1000); gridview.layout(1000, 1000);
@ -367,17 +310,11 @@ describe('gridview', () => {
}); });
test('gridview events', () => { test('gridview events', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false, proportionalLayout: false,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(800, 400); gridview.layout(800, 400);
@ -497,17 +434,11 @@ describe('gridview', () => {
test('dispose of gridviewComponent', () => { test('dispose of gridviewComponent', () => {
expect(container.childNodes.length).toBe(0); expect(container.childNodes.length).toBe(0);
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false, proportionalLayout: false,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(800, 400); gridview.layout(800, 400);
@ -529,21 +460,15 @@ describe('gridview', () => {
gridview.dispose(); gridview.dispose();
expect(container.children.length).toBe(0); expect(container.childNodes.length).toBe(0);
}); });
test('#1/VERTICAL', () => { test('#1/VERTICAL', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(800, 400); gridview.layout(800, 400);
@ -598,17 +523,11 @@ describe('gridview', () => {
}); });
test('#2/HORIZONTAL', () => { test('#2/HORIZONTAL', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(800, 400); gridview.layout(800, 400);
@ -663,17 +582,11 @@ describe('gridview', () => {
}); });
test('#3/HORIZONTAL', () => { test('#3/HORIZONTAL', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(800, 400); gridview.layout(800, 400);
@ -746,17 +659,11 @@ describe('gridview', () => {
}); });
test('#4/HORIZONTAL', () => { test('#4/HORIZONTAL', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(800, 400); gridview.layout(800, 400);
@ -847,17 +754,11 @@ describe('gridview', () => {
}); });
test('#5/VERTICAL', () => { test('#5/VERTICAL', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(800, 400); gridview.layout(800, 400);
@ -948,17 +849,11 @@ describe('gridview', () => {
}); });
test('#5/VERTICAL/proportional/false', () => { test('#5/VERTICAL/proportional/false', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false, proportionalLayout: false,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(800, 400); gridview.layout(800, 400);
@ -1049,17 +944,11 @@ describe('gridview', () => {
}); });
test('#6/VERTICAL', () => { test('#6/VERTICAL', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(800, 400); gridview.layout(800, 400);
@ -1180,17 +1069,11 @@ describe('gridview', () => {
}); });
test('#7/VERTICAL layout first', () => { test('#7/VERTICAL layout first', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(800, 400); gridview.layout(800, 400);
@ -1311,17 +1194,11 @@ describe('gridview', () => {
}); });
test('#8/VERTICAL layout after', () => { test('#8/VERTICAL layout after', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(800, 400); gridview.layout(800, 400);
@ -1444,17 +1321,11 @@ describe('gridview', () => {
}); });
test('#9/HORIZONTAL', () => { test('#9/HORIZONTAL', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(800, 400); gridview.layout(800, 400);
@ -1575,17 +1446,11 @@ describe('gridview', () => {
}); });
test('#9/HORIZONTAL/proportional/false', () => { test('#9/HORIZONTAL/proportional/false', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false, proportionalLayout: false,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(800, 400); gridview.layout(800, 400);
@ -1706,17 +1571,11 @@ describe('gridview', () => {
}); });
test('#10/HORIZONTAL scale x:1.5 y:2', () => { test('#10/HORIZONTAL scale x:1.5 y:2', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.fromJSON({ gridview.fromJSON({
@ -1840,17 +1699,11 @@ describe('gridview', () => {
}); });
test('panel is disposed of when component is disposed', () => { test('panel is disposed of when component is disposed', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false, proportionalLayout: false,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(1000, 1000); gridview.layout(1000, 1000);
@ -1877,17 +1730,11 @@ describe('gridview', () => {
}); });
test('panel is disposed of when removed', () => { test('panel is disposed of when removed', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false, proportionalLayout: false,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(1000, 1000); gridview.layout(1000, 1000);
@ -1913,17 +1760,11 @@ describe('gridview', () => {
}); });
test('panel is disposed of when fromJSON is called', () => { test('panel is disposed of when fromJSON is called', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: false, proportionalLayout: false,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(1000, 1000); gridview.layout(1000, 1000);
@ -1958,17 +1799,11 @@ describe('gridview', () => {
test('fromJSON events should still fire', () => { test('fromJSON events should still fire', () => {
jest.useFakeTimers(); jest.useFakeTimers();
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
let addGroup: GridviewPanel[] = []; let addGroup: GridviewPanel[] = [];
@ -2087,17 +1922,11 @@ describe('gridview', () => {
test('that fromJSON layouts are resized to the current dimensions', async () => { test('that fromJSON layouts are resized to the current dimensions', async () => {
const container = document.createElement('div'); const container = document.createElement('div');
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(1600, 800); gridview.layout(1600, 800);
@ -2220,17 +2049,11 @@ describe('gridview', () => {
test('that a deep HORIZONTAL layout with fromJSON dimensions identical to the current dimensions loads', async () => { test('that a deep HORIZONTAL layout with fromJSON dimensions identical to the current dimensions loads', async () => {
const container = document.createElement('div'); const container = document.createElement('div');
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(6000, 5000); gridview.layout(6000, 5000);
@ -2502,17 +2325,11 @@ describe('gridview', () => {
test('that a deep VERTICAL layout with fromJSON dimensions identical to the current dimensions loads', async () => { test('that a deep VERTICAL layout with fromJSON dimensions identical to the current dimensions loads', async () => {
const container = document.createElement('div'); const container = document.createElement('div');
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
gridview.layout(5000, 6000); gridview.layout(5000, 6000);
@ -2782,20 +2599,14 @@ describe('gridview', () => {
}); });
test('that loading a corrupt layout throws an error and leaves a clean gridview behind', () => { test('that loading a corrupt layout throws an error and leaves a clean gridview behind', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error(`unsupported panel '${options.name}'`);
}
},
}); });
let el = gridview.element.querySelector('.dv-view-container'); let el = gridview.element.querySelector('.view-container');
expect(el).toBeTruthy(); expect(el).toBeTruthy();
expect(el!.childNodes.length).toBe(0); expect(el!.childNodes.length).toBe(0);
@ -2856,44 +2667,34 @@ describe('gridview', () => {
}, },
activePanel: 'panel_1', activePanel: 'panel_1',
}); });
}).toThrow("unsupported panel 'somethingBad'"); }).toThrow(
"Cannot create 'panel_1', no component 'somethingBad' provided"
);
expect(gridview.groups.length).toBe(0); expect(gridview.groups.length).toBe(0);
el = gridview.element.querySelector('.dv-view-container'); el = gridview.element.querySelector('.view-container');
expect(el).toBeTruthy(); expect(el).toBeTruthy();
expect(el!.childNodes.length).toBe(0); expect(el!.childNodes.length).toBe(0);
}); });
test('that disableAutoResizing is false by default', () => { test('that disableAutoResizing is false by default', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
}); });
expect(gridview.disableResizing).toBeFalsy(); expect(gridview.disableResizing).toBeFalsy();
}); });
test('that disableAutoResizing can be enabled', () => { test('that disableAutoResizing can be enabled', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
disableAutoResizing: true, disableAutoResizing: true,
}); });
@ -2901,17 +2702,11 @@ describe('gridview', () => {
}); });
test('that setVisible toggles visiblity', () => { test('that setVisible toggles visiblity', () => {
const gridview = new GridviewComponent(container, { const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true, proportionalLayout: true,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: { default: TestGridview },
switch (options.name) {
case 'default':
return new TestGridview(options.id, options.name);
default:
throw new Error('unsupported');
}
},
disableAutoResizing: true, disableAutoResizing: true,
}); });
gridview.layout(1000, 1000); gridview.layout(1000, 1000);

View File

@ -8,7 +8,6 @@ describe('gridviewPanel', () => {
onDidAddPanel: jest.fn(), onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(), onDidRemovePanel: jest.fn(),
options: {}, options: {},
onDidOptionsChange: jest.fn(),
} as any; } as any;
}); });

View File

@ -8,8 +8,10 @@ describe('math', () => {
expect(clamp(55, 40, 50)).toBe(50); expect(clamp(55, 40, 50)).toBe(50);
}); });
it('if min > max return min', () => { it('should throw an error if min > max', () => {
expect(clamp(55, 50, 40)).toBe(50); expect(() => clamp(55, 50, 40)).toThrow(
'50 > 40 is an invalid condition'
);
}); });
}); });

View File

@ -1,418 +0,0 @@
import { Overlay } from '../../overlay/overlay';
import { mockGetBoundingClientRect } from '../__test_utils__/utils';
describe('overlay', () => {
test('toJSON, top left', () => {
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 mockGetBoundingClientRect({
left: 80,
top: 100,
width: 40,
height: 50,
});
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return mockGetBoundingClientRect({
left: 20,
top: 30,
width: 100,
height: 100,
});
}
);
cut.setBounds();
expect(cut.toJSON()).toEqual({
top: 70,
left: 60,
width: 40,
height: 50,
});
cut.dispose();
});
test('toJSON, bottom right', () => {
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,
right: 10,
bottom: 20,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
jest.spyOn(
container.childNodes.item(0) as HTMLElement,
'getBoundingClientRect'
).mockImplementation(() => {
return mockGetBoundingClientRect({
left: 80,
top: 100,
width: 40,
height: 50,
});
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return mockGetBoundingClientRect({
left: 20,
top: 30,
width: 100,
height: 100,
});
}
);
cut.setBounds();
expect(cut.toJSON()).toEqual({
bottom: -20,
right: 0,
width: 40,
height: 50,
});
cut.dispose();
});
test('that out-of-bounds dimensions are fixed, top left', () => {
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 mockGetBoundingClientRect({
left: 80,
top: 100,
width: 40,
height: 50,
});
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return mockGetBoundingClientRect({
left: 20,
top: 30,
width: 100,
height: 100,
});
}
);
cut.setBounds();
expect(cut.toJSON()).toEqual({
top: 70,
left: 60,
width: 40,
height: 50,
});
cut.dispose();
});
test('that out-of-bounds dimensions are fixed, bottom right', () => {
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,
bottom: -1000,
right: -1000,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
jest.spyOn(
container.childNodes.item(0) as HTMLElement,
'getBoundingClientRect'
).mockImplementation(() => {
return mockGetBoundingClientRect({
left: 80,
top: 100,
width: 40,
height: 50,
});
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return mockGetBoundingClientRect({
left: 20,
top: 30,
width: 100,
height: 100,
});
}
);
cut.setBounds();
expect(cut.toJSON()).toEqual({
bottom: -20,
right: 0,
width: 40,
height: 50,
});
cut.dispose();
});
test('setBounds, top left', () => {
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 mockGetBoundingClientRect({
left: 300,
top: 400,
width: 200,
height: 100,
});
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return mockGetBoundingClientRect({
left: 0,
top: 0,
width: 1000,
height: 1000,
});
}
);
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');
cut.dispose();
});
test('setBounds, bottom right', () => {
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,
right: 0,
bottom: 0,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
const element: HTMLElement = container.querySelector(
'.dv-resize-container'
)!;
expect(element).toBeTruthy();
jest.spyOn(element, 'getBoundingClientRect').mockImplementation(() => {
return mockGetBoundingClientRect({
left: 500,
top: 500,
width: 200,
height: 100,
});
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return mockGetBoundingClientRect({
left: 0,
top: 0,
width: 1000,
height: 1000,
});
}
);
cut.setBounds({ height: 100, width: 200, right: 300, bottom: 400 });
expect(element.style.height).toBe('100px');
expect(element.style.width).toBe('200px');
expect(element.style.right).toBe('300px');
expect(element.style.bottom).toBe('400px');
cut.dispose();
});
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();
});
test('aria-level attributes and corresponding z-index', () => {
const container = document.createElement('div');
const content = document.createElement('div');
const createOverlay = () =>
new Overlay({
height: 500,
width: 500,
left: 100,
top: 200,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
const overlay1 = createOverlay();
const zIndexValue = (delta: number) =>
`calc(var(--dv-overlay-z-index, 999) + ${delta})`;
expect(overlay1.element.getAttribute('aria-level')).toBe('0');
expect(overlay1.element.style.zIndex).toBe(zIndexValue(0));
const overlay2 = createOverlay();
const overlay3 = createOverlay();
expect(overlay1.element.getAttribute('aria-level')).toBe('0');
expect(overlay2.element.getAttribute('aria-level')).toBe('1');
expect(overlay3.element.getAttribute('aria-level')).toBe('2');
expect(overlay1.element.style.zIndex).toBe(zIndexValue(0));
expect(overlay2.element.style.zIndex).toBe(zIndexValue(2));
expect(overlay3.element.style.zIndex).toBe(zIndexValue(4));
overlay2.bringToFront();
expect(overlay1.element.getAttribute('aria-level')).toBe('0');
expect(overlay2.element.getAttribute('aria-level')).toBe('2');
expect(overlay3.element.getAttribute('aria-level')).toBe('1');
expect(overlay1.element.style.zIndex).toBe(zIndexValue(0));
expect(overlay2.element.style.zIndex).toBe(zIndexValue(4));
expect(overlay3.element.style.zIndex).toBe(zIndexValue(2));
overlay1.bringToFront();
expect(overlay1.element.getAttribute('aria-level')).toBe('2');
expect(overlay2.element.getAttribute('aria-level')).toBe('1');
expect(overlay3.element.getAttribute('aria-level')).toBe('0');
expect(overlay1.element.style.zIndex).toBe(zIndexValue(4));
expect(overlay2.element.style.zIndex).toBe(zIndexValue(2));
expect(overlay3.element.style.zIndex).toBe(zIndexValue(0));
overlay2.dispose();
expect(overlay1.element.getAttribute('aria-level')).toBe('1');
expect(overlay3.element.getAttribute('aria-level')).toBe('0');
expect(overlay1.element.style.zIndex).toBe(zIndexValue(2));
expect(overlay3.element.style.zIndex).toBe(zIndexValue(0));
overlay1.dispose();
expect(overlay3.element.getAttribute('aria-level')).toBe('0');
expect(overlay3.element.style.zIndex).toBe(zIndexValue(0));
});
});

View File

@ -1,18 +1,14 @@
import { Droptarget } from '../../dnd/droptarget'; import { Droptarget } from '../dnd/droptarget';
import { IDockviewPanel } from '../../dockview/dockviewPanel'; import { IDockviewPanel } from '../dockview/dockviewPanel';
import { Emitter } from '../../events'; import { Emitter } from '../events';
import { import { IRenderable, OverlayRenderContainer } from '../overlayRenderContainer';
IRenderable,
OverlayRenderContainer,
} from '../../overlay/overlayRenderContainer';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { Writable, exhaustMicrotaskQueue } from '../__test_utils__/utils'; import { Writable, exhaustMicrotaskQueue } from './__test_utils__/utils';
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
describe('overlayRenderContainer', () => { describe('overlayRenderContainer', () => {
let referenceContainer: IRenderable; let referenceContainer: IRenderable;
let parentContainer: HTMLElement; let parentContainer: HTMLElement;
let cut: OverlayRenderContainer;
beforeEach(() => { beforeEach(() => {
parentContainer = document.createElement('div'); parentContainer = document.createElement('div');
@ -21,28 +17,22 @@ describe('overlayRenderContainer', () => {
element: document.createElement('div'), element: document.createElement('div'),
dropTarget: fromPartial<Droptarget>({}), dropTarget: fromPartial<Droptarget>({}),
}; };
cut = new OverlayRenderContainer(parentContainer);
}); });
test('that attach(...) and detach(...) mutate the DOM as expected', () => { test('that attach(...) and detach(...) mutate the DOM as expected', () => {
const cut = new OverlayRenderContainer(
parentContainer,
fromPartial<DockviewComponent>({})
);
const panelContentEl = document.createElement('div'); const panelContentEl = document.createElement('div');
const onDidVisibilityChange = new Emitter<any>(); const onDidVisibilityChange = new Emitter<any>();
const onDidDimensionsChange = new Emitter<any>(); const onDidDimensionsChange = new Emitter<any>();
const onDidLocationChange = new Emitter<any>();
const panel = fromPartial<IDockviewPanel>({ const panel = fromPartial<IDockviewPanel>({
api: { api: {
id: 'test_panel_id', id: 'test_panel_id',
onDidVisibilityChange: onDidVisibilityChange.event, onDidVisibilityChange: onDidVisibilityChange.event,
onDidDimensionsChange: onDidDimensionsChange.event, onDidDimensionsChange: onDidDimensionsChange.event,
onDidLocationChange: onDidLocationChange.event,
isVisible: true, isVisible: true,
location: { type: 'grid' },
}, },
view: { view: {
content: { content: {
@ -68,25 +58,17 @@ describe('overlayRenderContainer', () => {
}); });
test('add a view that is not currently in the DOM', async () => { test('add a view that is not currently in the DOM', async () => {
const cut = new OverlayRenderContainer(
parentContainer,
fromPartial<DockviewComponent>({})
);
const panelContentEl = document.createElement('div'); const panelContentEl = document.createElement('div');
const onDidVisibilityChange = new Emitter<any>(); const onDidVisibilityChange = new Emitter<any>();
const onDidDimensionsChange = new Emitter<any>(); const onDidDimensionsChange = new Emitter<any>();
const onDidLocationChange = new Emitter<any>();
const panel = fromPartial<IDockviewPanel>({ const panel = fromPartial<IDockviewPanel>({
api: { api: {
id: 'test_panel_id', id: 'test_panel_id',
onDidVisibilityChange: onDidVisibilityChange.event, onDidVisibilityChange: onDidVisibilityChange.event,
onDidDimensionsChange: onDidDimensionsChange.event, onDidDimensionsChange: onDidDimensionsChange.event,
onDidLocationChange: onDidLocationChange.event,
isVisible: true, isVisible: true,
location: { type: 'grid' },
}, },
view: { view: {
content: { content: {
@ -204,62 +186,4 @@ describe('overlayRenderContainer', () => {
referenceContainer.element.getBoundingClientRect referenceContainer.element.getBoundingClientRect
).toHaveBeenCalledTimes(3); ).toHaveBeenCalledTimes(3);
}); });
test('related z-index from `aria-level` set on floating panels', async () => {
const group = fromPartial<DockviewGroupPanel>({});
const element = document.createElement('div');
element.setAttribute('aria-level', '2');
const spy = jest.spyOn(element, 'getAttribute');
const accessor = fromPartial<DockviewComponent>({
floatingGroups: [
{
group,
overlay: {
element,
},
},
],
});
const cut = new OverlayRenderContainer(parentContainer, accessor);
const panelContentEl = document.createElement('div');
const onDidVisibilityChange = new Emitter<any>();
const onDidDimensionsChange = new Emitter<any>();
const onDidLocationChange = new Emitter<any>();
const panel = fromPartial<IDockviewPanel>({
api: {
id: 'test_panel_id',
onDidVisibilityChange: onDidVisibilityChange.event,
onDidDimensionsChange: onDidDimensionsChange.event,
onDidLocationChange: onDidLocationChange.event,
isVisible: true,
group,
location: { type: 'floating' },
},
view: {
content: {
element: panelContentEl,
},
},
group: {
api: {
location: { type: 'floating' },
},
},
});
cut.attach({ panel, referenceContainer });
await exhaustMicrotaskQueue();
expect(spy).toHaveBeenCalledWith('aria-level');
expect(panelContentEl.parentElement!.style.zIndex).toBe(
'calc(var(--dv-overlay-z-index, 999) + 5)'
);
});
}); });

View File

@ -0,0 +1,102 @@
import { createComponent } from '../../panel/componentFactory';
describe('componentFactory', () => {
describe('createComponent', () => {
test('valid component and framework component', () => {
const mock = jest.fn();
const mock2 = jest.fn();
expect(() =>
createComponent(
'id-1',
'component-1',
{ 'component-1': mock },
{ 'component-1': mock2 }
)
).toThrow(
"Cannot create 'id-1'. component 'component-1' registered as both a component and frameworkComponent"
);
});
test('valid framework component but no factory', () => {
const mock = jest.fn();
expect(() =>
createComponent(
'id-1',
'component-1',
{},
{ 'component-1': mock }
)
).toThrow(
"Cannot create 'id-1' for framework component 'component-1'. you must register a frameworkPanelWrapper to use framework components"
);
});
test('valid framework component', () => {
const component = jest.fn();
const createComponentFn = jest
.fn()
.mockImplementation(() => component);
const frameworkComponent = jest.fn();
expect(
createComponent(
'id-1',
'component-1',
{},
{ 'component-1': frameworkComponent },
{
createComponent: createComponentFn,
}
)
).toBe(component);
expect(createComponentFn).toHaveBeenCalledWith(
'id-1',
'component-1',
frameworkComponent
);
});
test('no valid component with fallback', () => {
const mock = jest.fn();
expect(
createComponent(
'id-1',
'component-1',
{},
{},
{
createComponent: () => null,
},
() => mock
)
).toBe(mock);
});
test('no valid component', () => {
expect(() =>
createComponent('id-1', 'component-1', {}, {})
).toThrow(
"Cannot create 'id-1', no component 'component-1' provided"
);
});
test('valid component', () => {
const component = jest.fn();
const componentResult = createComponent(
'id-1',
'component-1',
{ 'component-1': component },
{}
);
expect(component).toHaveBeenCalled();
expect(componentResult instanceof component).toBeTruthy();
});
});
});

View File

@ -1,10 +1,14 @@
import { CompositeDisposable } from '../../lifecycle'; import { CompositeDisposable } from '../../lifecycle';
import { Paneview } from '../../paneview/paneview'; import { Paneview } from '../../paneview/paneview';
import { IPanePart, PaneviewPanel } from '../../paneview/paneviewPanel'; import {
IPaneBodyPart,
IPaneHeaderPart,
PaneviewPanel,
} from '../../paneview/paneviewPanel';
import { Orientation } from '../../splitview/splitview'; import { Orientation } from '../../splitview/splitview';
class TestPanel extends PaneviewPanel { class TestPanel extends PaneviewPanel {
protected getBodyComponent(): IPanePart { protected getBodyComponent(): IPaneBodyPart {
return { return {
element: document.createElement('div'), element: document.createElement('div'),
update: () => { update: () => {
@ -19,7 +23,7 @@ class TestPanel extends PaneviewPanel {
}; };
} }
protected getHeaderComponent(): IPanePart { protected getHeaderComponent(): IPaneHeaderPart {
return { return {
element: document.createElement('div'), element: document.createElement('div'),
update: () => { update: () => {
@ -56,28 +60,22 @@ describe('paneview', () => {
paneview.onDidRemoveView((view) => removed.push(view)) paneview.onDidRemoveView((view) => removed.push(view))
); );
const view1 = new TestPanel({ const view1 = new TestPanel(
id: 'id', 'id',
component: 'component', 'component',
headerComponent: 'headerComponent', 'headerComponent',
orientation: Orientation.VERTICAL, Orientation.VERTICAL,
isExpanded: true, true,
isHeaderVisible: true, true
headerSize: 22, );
minimumBodySize: 0, const view2 = new TestPanel(
maximumBodySize: Number.MAX_SAFE_INTEGER, 'id2',
}); 'component',
const view2 = new TestPanel({ 'headerComponent',
id: 'id2', Orientation.VERTICAL,
component: 'component', true,
headerComponent: 'headerComponent', true
orientation: Orientation.VERTICAL, );
isExpanded: true,
isHeaderVisible: true,
headerSize: 22,
minimumBodySize: 0,
maximumBodySize: Number.MAX_SAFE_INTEGER,
});
expect(added.length).toBe(0); expect(added.length).toBe(0);
expect(removed.length).toBe(0); expect(removed.length).toBe(0);
@ -112,28 +110,22 @@ describe('paneview', () => {
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
}); });
const view1 = new TestPanel({ const view1 = new TestPanel(
id: 'id', 'id',
component: 'component', 'component',
headerComponent: 'headerComponent', 'headerComponent',
orientation: Orientation.VERTICAL, Orientation.VERTICAL,
isExpanded: true, true,
isHeaderVisible: true, true
headerSize: 22, );
minimumBodySize: 0, const view2 = new TestPanel(
maximumBodySize: Number.MAX_SAFE_INTEGER, 'id2',
}); 'component',
const view2 = new TestPanel({ 'headerComponent',
id: 'id2', Orientation.VERTICAL,
component: 'component', true,
headerComponent: 'headerComponent', true
orientation: Orientation.VERTICAL, );
isExpanded: true,
isHeaderVisible: true,
headerSize: 22,
minimumBodySize: 0,
maximumBodySize: Number.MAX_SAFE_INTEGER,
});
paneview.addPane(view1); paneview.addPane(view1);
paneview.addPane(view2); paneview.addPane(view2);

View File

@ -4,28 +4,19 @@ import { PanelUpdateEvent } from '../../panel/types';
import { PaneviewComponent } from '../../paneview/paneviewComponent'; import { PaneviewComponent } from '../../paneview/paneviewComponent';
import { import {
PaneviewPanel, PaneviewPanel,
IPanePart, IPaneBodyPart,
IPaneHeaderPart,
PanePanelComponentInitParameter, PanePanelComponentInitParameter,
} from '../../paneview/paneviewPanel'; } from '../../paneview/paneviewPanel';
import { Orientation } from '../../splitview/splitview'; import { Orientation } from '../../splitview/splitview';
class TestPanel extends PaneviewPanel { class TestPanel extends PaneviewPanel {
constructor(id: string, component: string) { constructor(id: string, component: string) {
super({ super(id, component, 'header', Orientation.VERTICAL, false, true);
id,
component,
headerComponent: 'header',
orientation: Orientation.VERTICAL,
isExpanded: false,
isHeaderVisible: true,
headerSize: 22,
minimumBodySize: 0,
maximumBodySize: Number.MAX_SAFE_INTEGER,
});
} }
getHeaderComponent() { getHeaderComponent() {
return new (class Header implements IPanePart { return new (class Header implements IPaneHeaderPart {
private _element: HTMLElement = document.createElement('div'); private _element: HTMLElement = document.createElement('div');
get element() { get element() {
@ -47,7 +38,7 @@ class TestPanel extends PaneviewPanel {
} }
getBodyComponent() { getBodyComponent() {
return new (class Header implements IPanePart { return new (class Header implements IPaneBodyPart {
private _element: HTMLElement = document.createElement('div'); private _element: HTMLElement = document.createElement('div');
get element() { get element() {
@ -69,7 +60,7 @@ class TestPanel extends PaneviewPanel {
} }
} }
describe('paneviewComponent', () => { describe('componentPaneview', () => {
let container: HTMLElement; let container: HTMLElement;
beforeEach(() => { beforeEach(() => {
@ -77,39 +68,13 @@ describe('paneviewComponent', () => {
container.className = 'container'; container.className = 'container';
}); });
test('that the container is not removed when grid is disposed', () => {
const root = document.createElement('div');
const container = document.createElement('div');
root.appendChild(container);
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
paneview.dispose();
expect(container.parentElement).toBe(root);
expect(container.children.length).toBe(0);
});
test('vertical panels', () => { test('vertical panels', () => {
const disposables = new CompositeDisposable(); const disposables = new CompositeDisposable();
const paneview = new PaneviewComponent(container, { const paneview = new PaneviewComponent({
createComponent: (options) => { parentElement: container,
switch (options.name) { components: {
case 'default': testPanel: TestPanel,
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
@ -117,12 +82,12 @@ describe('paneviewComponent', () => {
paneview.addPanel({ paneview.addPanel({
id: 'panel1', id: 'panel1',
component: 'default', component: 'testPanel',
title: 'Panel 1', title: 'Panel 1',
}); });
paneview.addPanel({ paneview.addPanel({
id: 'panel2', id: 'panel2',
component: 'default', component: 'testPanel',
title: 'Panel2', title: 'Panel2',
}); });
@ -179,19 +144,13 @@ describe('paneviewComponent', () => {
}); });
test('serialization', () => { test('serialization', () => {
const paneview = new PaneviewComponent(container, { const paneview = new PaneviewComponent({
createComponent: (options) => { parentElement: container,
switch (options.name) { components: {
case 'default': testPanel: TestPanel,
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
expect(container.querySelectorAll('.dv-pane-container').length).toBe(1);
paneview.fromJSON({ paneview.fromJSON({
size: 6, size: 6,
views: [ views: [
@ -199,7 +158,7 @@ describe('paneviewComponent', () => {
size: 1, size: 1,
data: { data: {
id: 'panel1', id: 'panel1',
component: 'default', component: 'testPanel',
title: 'Panel 1', title: 'Panel 1',
}, },
expanded: true, expanded: true,
@ -208,7 +167,7 @@ describe('paneviewComponent', () => {
size: 2, size: 2,
data: { data: {
id: 'panel2', id: 'panel2',
component: 'default', component: 'testPanel',
title: 'Panel 2', title: 'Panel 2',
}, },
expanded: false, expanded: false,
@ -217,15 +176,13 @@ describe('paneviewComponent', () => {
size: 3, size: 3,
data: { data: {
id: 'panel3', id: 'panel3',
component: 'default', component: 'testPanel',
title: 'Panel 3', title: 'Panel 3',
}, },
}, },
], ],
}); });
expect(container.querySelectorAll('.dv-pane-container').length).toBe(1);
paneview.layout(400, 800); paneview.layout(400, 800);
const panel1 = paneview.getPanel('panel1'); const panel1 = paneview.getPanel('panel1');
@ -265,57 +222,53 @@ describe('paneviewComponent', () => {
size: 756, size: 756,
data: { data: {
id: 'panel1', id: 'panel1',
component: 'default', component: 'testPanel',
title: 'Panel 1', title: 'Panel 1',
}, },
expanded: true, expanded: true,
headerSize: 22, minimumSize: 100,
}, },
{ {
size: 22, size: 22,
data: { data: {
id: 'panel2', id: 'panel2',
component: 'default', component: 'testPanel',
title: 'Panel 2', title: 'Panel 2',
}, },
expanded: false, expanded: false,
headerSize: 22, minimumSize: 100,
}, },
{ {
size: 22, size: 22,
data: { data: {
id: 'panel3', id: 'panel3',
component: 'default', component: 'testPanel',
title: 'Panel 3', title: 'Panel 3',
}, },
expanded: false, expanded: false,
headerSize: 22, minimumSize: 100,
}, },
], ],
}); });
}); });
test('toJSON shouldnt fire any layout events', () => { test('toJSON shouldnt fire any layout events', () => {
const paneview = new PaneviewComponent(container, { const paneview = new PaneviewComponent({
createComponent: (options) => { parentElement: container,
switch (options.name) { components: {
case 'default': testPanel: TestPanel,
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
paneview.layout(1000, 1000); paneview.layout(1000, 1000);
paneview.addPanel({ paneview.addPanel({
id: 'panel1', id: 'panel1',
component: 'default', component: 'testPanel',
title: 'Panel 1', title: 'Panel 1',
}); });
paneview.addPanel({ paneview.addPanel({
id: 'panel2', id: 'panel2',
component: 'default', component: 'testPanel',
title: 'Panel 2', title: 'Panel 2',
}); });
@ -329,15 +282,13 @@ describe('paneviewComponent', () => {
disposable.dispose(); disposable.dispose();
}); });
test('panel is disposed of when component is disposed', () => { test('dispose of paneviewComponent', () => {
const paneview = new PaneviewComponent(container, { expect(container.childNodes.length).toBe(0);
createComponent: (options) => {
switch (options.name) { const paneview = new PaneviewComponent({
case 'default': parentElement: container,
return new TestPanel(options.id, options.name); components: {
default: testPanel: TestPanel,
throw new Error('unsupported');
}
}, },
}); });
@ -345,12 +296,40 @@ describe('paneviewComponent', () => {
paneview.addPanel({ paneview.addPanel({
id: 'panel1', id: 'panel1',
component: 'default', component: 'testPanel',
title: 'Panel 1', title: 'Panel 1',
}); });
paneview.addPanel({ paneview.addPanel({
id: 'panel2', id: 'panel2',
component: 'default', component: 'testPanel',
title: 'Panel 2',
});
expect(container.childNodes.length).toBeGreaterThan(0);
paneview.dispose();
expect(container.childNodes.length).toBe(0);
});
test('panel is disposed of when component is disposed', () => {
const paneview = new PaneviewComponent({
parentElement: container,
components: {
testPanel: TestPanel,
},
});
paneview.layout(1000, 1000);
paneview.addPanel({
id: 'panel1',
component: 'testPanel',
title: 'Panel 1',
});
paneview.addPanel({
id: 'panel2',
component: 'testPanel',
title: 'Panel 2', title: 'Panel 2',
}); });
@ -367,14 +346,10 @@ describe('paneviewComponent', () => {
}); });
test('panel is disposed of when removed', () => { test('panel is disposed of when removed', () => {
const paneview = new PaneviewComponent(container, { const paneview = new PaneviewComponent({
createComponent: (options) => { parentElement: container,
switch (options.name) { components: {
case 'default': testPanel: TestPanel,
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
@ -382,12 +357,12 @@ describe('paneviewComponent', () => {
paneview.addPanel({ paneview.addPanel({
id: 'panel1', id: 'panel1',
component: 'default', component: 'testPanel',
title: 'Panel 1', title: 'Panel 1',
}); });
paneview.addPanel({ paneview.addPanel({
id: 'panel2', id: 'panel2',
component: 'default', component: 'testPanel',
title: 'Panel 2', title: 'Panel 2',
}); });
@ -404,14 +379,10 @@ describe('paneviewComponent', () => {
}); });
test('panel is disposed of when fromJSON is called', () => { test('panel is disposed of when fromJSON is called', () => {
const paneview = new PaneviewComponent(container, { const paneview = new PaneviewComponent({
createComponent: (options) => { parentElement: container,
switch (options.name) { components: {
case 'default': testPanel: TestPanel,
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
@ -419,12 +390,12 @@ describe('paneviewComponent', () => {
paneview.addPanel({ paneview.addPanel({
id: 'panel1', id: 'panel1',
component: 'default', component: 'testPanel',
title: 'Panel 1', title: 'Panel 1',
}); });
paneview.addPanel({ paneview.addPanel({
id: 'panel2', id: 'panel2',
component: 'default', component: 'testPanel',
title: 'Panel 2', title: 'Panel 2',
}); });
@ -441,14 +412,10 @@ describe('paneviewComponent', () => {
}); });
test('that fromJSON layouts are resized to the current dimensions', async () => { test('that fromJSON layouts are resized to the current dimensions', async () => {
const paneview = new PaneviewComponent(container, { const paneview = new PaneviewComponent({
createComponent: (options) => { parentElement: container,
switch (options.name) { components: {
case 'default': testPanel: TestPanel,
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
@ -461,17 +428,16 @@ describe('paneviewComponent', () => {
size: 1, size: 1,
data: { data: {
id: 'panel1', id: 'panel1',
component: 'default', component: 'testPanel',
title: 'Panel 1', title: 'Panel 1',
}, },
minimumSize: 100,
expanded: true, expanded: true,
}, },
{ {
size: 2, size: 2,
data: { data: {
id: 'panel2', id: 'panel2',
component: 'default', component: 'testPanel',
title: 'Panel 2', title: 'Panel 2',
}, },
expanded: true, expanded: true,
@ -480,7 +446,7 @@ describe('paneviewComponent', () => {
size: 3, size: 3,
data: { data: {
id: 'panel3', id: 'panel3',
component: 'default', component: 'testPanel',
title: 'Panel 3', title: 'Panel 3',
}, },
expanded: true, expanded: true,
@ -496,46 +462,41 @@ describe('paneviewComponent', () => {
size: 122, size: 122,
data: { data: {
id: 'panel1', id: 'panel1',
component: 'default', component: 'testPanel',
title: 'Panel 1', title: 'Panel 1',
}, },
expanded: true, expanded: true,
minimumSize: 100, minimumSize: 100,
headerSize: 22,
}, },
{ {
size: 22, size: 122,
data: { data: {
id: 'panel2', id: 'panel2',
component: 'default', component: 'testPanel',
title: 'Panel 2', title: 'Panel 2',
}, },
expanded: true, expanded: true,
headerSize: 22, minimumSize: 100,
}, },
{ {
size: 456, size: 356,
data: { data: {
id: 'panel3', id: 'panel3',
component: 'default', component: 'testPanel',
title: 'Panel 3', title: 'Panel 3',
}, },
expanded: true, expanded: true,
headerSize: 22, minimumSize: 100,
}, },
], ],
}); });
}); });
test('that disableAutoResizing is false by default', () => { test('that disableAutoResizing is false by default', () => {
const paneview = new PaneviewComponent(container, { const paneview = new PaneviewComponent({
createComponent: (options) => { parentElement: container,
switch (options.name) { components: {
case 'default': testPanel: TestPanel,
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
@ -543,14 +504,10 @@ describe('paneviewComponent', () => {
}); });
test('that disableAutoResizing can be enabled', () => { test('that disableAutoResizing can be enabled', () => {
const paneview = new PaneviewComponent(container, { const paneview = new PaneviewComponent({
createComponent: (options) => { parentElement: container,
switch (options.name) { components: {
case 'default': testPanel: TestPanel,
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
disableAutoResizing: true, disableAutoResizing: true,
}); });
@ -559,14 +516,10 @@ describe('paneviewComponent', () => {
}); });
test('that setVisible toggles visiblity', () => { test('that setVisible toggles visiblity', () => {
const paneview = new PaneviewComponent(container, { const paneview = new PaneviewComponent({
createComponent: (options) => { parentElement: container,
switch (options.name) { components: {
case 'default': default: TestPanel,
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
disableAutoResizing: true, disableAutoResizing: true,
}); });
@ -595,25 +548,4 @@ describe('paneviewComponent', () => {
expect(panel1.api.isVisible).toBeTruthy(); expect(panel1.api.isVisible).toBeTruthy();
expect(panel2.api.isVisible).toBeTruthy(); expect(panel2.api.isVisible).toBeTruthy();
}); });
test('update className', () => {
const paneview = new PaneviewComponent(container, {
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
disableAutoResizing: true,
className: 'test-a test-b',
});
expect(paneview.element.className).toBe('test-a test-b');
paneview.updateOptions({ className: 'test-b test-c' });
expect(paneview.element.className).toBe('test-b test-c');
});
}); });

View File

@ -96,7 +96,7 @@ describe('splitview', () => {
expect(splitview.orientation).toBe(Orientation.HORIZONTAL); expect(splitview.orientation).toBe(Orientation.HORIZONTAL);
const viewQuery = container.querySelectorAll( const viewQuery = container.querySelectorAll(
'.dv-split-view-container dv-horizontal' '.split-view-container horizontal'
); );
expect(viewQuery).toBeTruthy(); expect(viewQuery).toBeTruthy();
@ -111,7 +111,7 @@ describe('splitview', () => {
expect(splitview.orientation).toBe(Orientation.VERTICAL); expect(splitview.orientation).toBe(Orientation.VERTICAL);
const viewQuery = container.querySelectorAll( const viewQuery = container.querySelectorAll(
'.dv-split-view-container dv-vertical' '.split-view-container vertical'
); );
expect(viewQuery).toBeTruthy(); expect(viewQuery).toBeTruthy();
@ -128,48 +128,48 @@ describe('splitview', () => {
splitview.addView(new Testview(50, 50)); splitview.addView(new Testview(50, 50));
let viewQuery = container.querySelectorAll( let viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view' '.split-view-container > .view-container > .view'
); );
expect(viewQuery.length).toBe(3); expect(viewQuery.length).toBe(3);
let sashQuery = container.querySelectorAll( let sashQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-sash-container > .dv-sash' '.split-view-container > .sash-container > .sash'
); );
expect(sashQuery.length).toBe(2); expect(sashQuery.length).toBe(2);
splitview.removeView(2); splitview.removeView(2);
viewQuery = container.querySelectorAll( viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view' '.split-view-container > .view-container > .view'
); );
expect(viewQuery.length).toBe(2); expect(viewQuery.length).toBe(2);
sashQuery = container.querySelectorAll( sashQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-sash-container > .dv-sash' '.split-view-container > .sash-container > .sash'
); );
expect(sashQuery.length).toBe(1); expect(sashQuery.length).toBe(1);
splitview.removeView(0); splitview.removeView(0);
viewQuery = container.querySelectorAll( viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view' '.split-view-container > .view-container > .view'
); );
expect(viewQuery.length).toBe(1); expect(viewQuery.length).toBe(1);
sashQuery = container.querySelectorAll( sashQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-sash-container > .dv-sash' '.split-view-container > .sash-container > .sash'
); );
expect(sashQuery.length).toBe(0); expect(sashQuery.length).toBe(0);
splitview.removeView(0); splitview.removeView(0);
viewQuery = container.querySelectorAll( viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view' '.split-view-container > .view-container > .view'
); );
expect(viewQuery.length).toBe(0); expect(viewQuery.length).toBe(0);
sashQuery = container.querySelectorAll( sashQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-sash-container > .dv-sash' '.split-view-container > .sash-container > .sash'
); );
expect(sashQuery.length).toBe(0); expect(sashQuery.length).toBe(0);
@ -188,14 +188,14 @@ describe('splitview', () => {
splitview.addView(view2); splitview.addView(view2);
let viewQuery = container.querySelectorAll( let viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view.visible' '.split-view-container > .view-container > .view.visible'
); );
expect(viewQuery.length).toBe(2); expect(viewQuery.length).toBe(2);
splitview.setViewVisible(1, false); splitview.setViewVisible(1, false);
viewQuery = container.querySelectorAll( viewQuery = container.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view.visible' '.split-view-container > .view-container > .view.visible'
); );
expect(viewQuery.length).toBe(1); expect(viewQuery.length).toBe(1);
@ -619,7 +619,7 @@ describe('splitview', () => {
); );
const sashElement = container const sashElement = container
.getElementsByClassName('dv-sash') .getElementsByClassName('sash')
.item(0) as HTMLElement; .item(0) as HTMLElement;
// validate the expected state before drag // validate the expected state before drag
@ -772,130 +772,4 @@ describe('splitview', () => {
view1.fireChangeEvent({ size: 300 }); view1.fireChangeEvent({ size: 300 });
expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]); expect([view1.size, view2.size, view3.size]).toEqual([300, 300, 300]);
}); });
test('that margins are applied to view sizing', () => {
const splitview = new Splitview(container, {
orientation: Orientation.HORIZONTAL,
proportionalLayout: false,
margin: 24,
});
splitview.layout(924, 500);
const view1 = new Testview(0, 1000);
const view2 = new Testview(0, 1000);
const view3 = new Testview(0, 1000);
const view4 = new Testview(0, 1000);
splitview.addView(view1);
expect([view1.size]).toEqual([924]);
splitview.addView(view2);
expect([view1.size, view2.size]).toEqual([450, 450]); // 450 + 24 + 450 = 924
splitview.addView(view3);
expect([view1.size, view2.size, view3.size]).toEqual([292, 292, 292]); // 292 + 24 + 292 + 24 + 292 = 924
splitview.addView(view4);
expect([view1.size, view2.size, view3.size, view4.size]).toEqual([
213, 213, 213, 213,
]); // 213 + 24 + 213 + 24 + 213 + 24 + 213 = 924
let viewQuery = Array.from(
container
.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view'
)
.entries()
)
.map(([i, e]) => e as HTMLElement)
.map((e) => ({
left: e.style.left,
top: e.style.top,
height: e.style.height,
width: e.style.width,
}));
let sashQuery = Array.from(
container
.querySelectorAll(
'.dv-split-view-container > .dv-sash-container > .dv-sash'
)
.entries()
)
.map(([i, e]) => e as HTMLElement)
.map((e) => ({
left: e.style.left,
top: e.style.top,
}));
// check HTMLElement positions since these are the ones that really matter
expect(viewQuery).toEqual([
{ left: '0px', top: '', width: '213px', height: '' },
// 213 + 24 = 237
{ left: '237px', top: '', width: '213px', height: '' },
// 237 + 213 + 24 = 474
{ left: '474px', top: '', width: '213px', height: '' },
// 474 + 213 + 24 = 474
{ left: '711px', top: '', width: '213px', height: '' },
// 711 + 213 = 924
]);
// 924 / 4 = 231 view size
// 231 - (24*3/4) = 213 margin adjusted view size
// 213 - 4/2 + 24/2 = 223
expect(sashQuery).toEqual([
// 213 - 4/2 + 24/2 = 223
{ left: '223px', top: '0px' },
// 213 + 24 + 213 = 450
// 450 - 4/2 + 24/2 = 460
{ left: '460px', top: '0px' },
// 213 + 24 + 213 + 24 + 213 = 687
// 687 - 4/2 + 24/2 = 697
{ left: '697px', top: '0px' },
]);
splitview.setViewVisible(0, false);
viewQuery = Array.from(
container
.querySelectorAll(
'.dv-split-view-container > .dv-view-container > .dv-view'
)
.entries()
)
.map(([i, e]) => e as HTMLElement)
.map((e) => ({
left: e.style.left,
top: e.style.top,
height: e.style.height,
width: e.style.width,
}));
sashQuery = Array.from(
container
.querySelectorAll(
'.dv-split-view-container > .dv-sash-container > .dv-sash'
)
.entries()
)
.map(([i, e]) => e as HTMLElement)
.map((e) => ({
left: e.style.left,
top: e.style.top,
}));
expect(viewQuery).toEqual([
{ left: '0px', top: '', width: '0px', height: '' },
{ left: '0px', top: '', width: '215px', height: '' },
{ left: '239px', top: '', width: '215px', height: '' },
{ left: '478px', top: '', width: '446px', height: '' },
]);
expect(sashQuery).toEqual([
{ left: '0px', top: '0px' },
{ left: '225px', top: '0px' },
{ left: '464px', top: '0px' },
]);
});
}); });

View File

@ -26,52 +26,25 @@ describe('componentSplitview', () => {
container.className = 'container'; container.className = 'container';
}); });
test('that the container is not removed when grid is disposed', () => {
const root = document.createElement('div');
const container = document.createElement('div');
root.appendChild(container);
const splitview = new SplitviewComponent(container, {
orientation: Orientation.VERTICAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
});
splitview.dispose();
expect(container.parentElement).toBe(root);
expect(container.children.length).toBe(0);
});
test('event leakage', () => { test('event leakage', () => {
Emitter.setLeakageMonitorEnabled(true); Emitter.setLeakageMonitorEnabled(true);
const splitview = new SplitviewComponent(container, { const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: {
switch (options.name) { testPanel: TestPanel,
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
splitview.layout(600, 400); splitview.layout(600, 400);
const panel1 = splitview.addPanel({ const panel1 = splitview.addPanel({
id: 'panel1', id: 'panel1',
component: 'default', component: 'testPanel',
}); });
const panel2 = splitview.addPanel({ const panel2 = splitview.addPanel({
id: 'panel2', id: 'panel2',
component: 'default', component: 'testPanel',
}); });
splitview.movePanel(0, 1); splitview.movePanel(0, 1);
@ -93,22 +66,18 @@ describe('componentSplitview', () => {
}); });
test('remove panel', () => { test('remove panel', () => {
const splitview = new SplitviewComponent(container, { const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: {
switch (options.name) { testPanel: TestPanel,
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
splitview.layout(600, 400); splitview.layout(600, 400);
splitview.addPanel({ id: 'panel1', component: 'default' }); splitview.addPanel({ id: 'panel1', component: 'testPanel' });
splitview.addPanel({ id: 'panel2', component: 'default' }); splitview.addPanel({ id: 'panel2', component: 'testPanel' });
splitview.addPanel({ id: 'panel3', component: 'default' }); splitview.addPanel({ id: 'panel3', component: 'testPanel' });
const panel1 = splitview.getPanel('panel1')!; const panel1 = splitview.getPanel('panel1')!;
const panel2 = splitview.getPanel('panel2')!; const panel2 = splitview.getPanel('panel2')!;
@ -133,15 +102,11 @@ describe('componentSplitview', () => {
}); });
test('horizontal dimensions', () => { test('horizontal dimensions', () => {
const splitview = new SplitviewComponent(container, { const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: {
switch (options.name) { testPanel: TestPanel,
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
splitview.layout(600, 400); splitview.layout(600, 400);
@ -151,15 +116,11 @@ describe('componentSplitview', () => {
}); });
test('vertical dimensions', () => { test('vertical dimensions', () => {
const splitview = new SplitviewComponent(container, { const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: {
switch (options.name) { testPanel: TestPanel,
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
splitview.layout(600, 400); splitview.layout(600, 400);
@ -169,22 +130,18 @@ describe('componentSplitview', () => {
}); });
test('api resize', () => { test('api resize', () => {
const splitview = new SplitviewComponent(container, { const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: {
switch (options.name) { testPanel: TestPanel,
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
splitview.layout(400, 600); splitview.layout(400, 600);
splitview.addPanel({ id: 'panel1', component: 'default' }); splitview.addPanel({ id: 'panel1', component: 'testPanel' });
splitview.addPanel({ id: 'panel2', component: 'default' }); splitview.addPanel({ id: 'panel2', component: 'testPanel' });
splitview.addPanel({ id: 'panel3', component: 'default' }); splitview.addPanel({ id: 'panel3', component: 'testPanel' });
const panel1 = splitview.getPanel('panel1')!; const panel1 = splitview.getPanel('panel1')!;
const panel2 = splitview.getPanel('panel2')!; const panel2 = splitview.getPanel('panel2')!;
@ -226,20 +183,16 @@ describe('componentSplitview', () => {
}); });
test('api', () => { test('api', () => {
const splitview = new SplitviewComponent(container, { const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: {
switch (options.name) { testPanel: TestPanel,
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
splitview.layout(600, 400); splitview.layout(600, 400);
splitview.addPanel({ id: 'panel1', component: 'default' }); splitview.addPanel({ id: 'panel1', component: 'testPanel' });
const panel1 = splitview.getPanel('panel1'); const panel1 = splitview.getPanel('panel1');
@ -250,7 +203,7 @@ describe('componentSplitview', () => {
// expect(panel1?.api.isFocused).toBeFalsy(); // expect(panel1?.api.isFocused).toBeFalsy();
expect(panel1!.api.isVisible).toBeTruthy(); expect(panel1!.api.isVisible).toBeTruthy();
splitview.addPanel({ id: 'panel2', component: 'default' }); splitview.addPanel({ id: 'panel2', component: 'testPanel' });
const panel2 = splitview.getPanel('panel2'); const panel2 = splitview.getPanel('panel2');
@ -272,22 +225,18 @@ describe('componentSplitview', () => {
test('vertical panels', () => { test('vertical panels', () => {
const disposables = new CompositeDisposable(); const disposables = new CompositeDisposable();
const splitview = new SplitviewComponent(container, { const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: {
switch (options.name) { testPanel: TestPanel,
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
splitview.layout(300, 200); splitview.layout(300, 200);
splitview.addPanel({ id: 'panel1', component: 'default' }); splitview.addPanel({ id: 'panel1', component: 'testPanel' });
splitview.addPanel({ id: 'panel2', component: 'default' }); splitview.addPanel({ id: 'panel2', component: 'testPanel' });
const panel1 = splitview.getPanel('panel1') as SplitviewPanel; const panel1 = splitview.getPanel('panel1') as SplitviewPanel;
const panel2 = splitview.getPanel('panel2') as SplitviewPanel; const panel2 = splitview.getPanel('panel2') as SplitviewPanel;
@ -328,22 +277,18 @@ describe('componentSplitview', () => {
test('horizontal panels', () => { test('horizontal panels', () => {
const disposables = new CompositeDisposable(); const disposables = new CompositeDisposable();
const splitview = new SplitviewComponent(container, { const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: {
switch (options.name) { testPanel: TestPanel,
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
splitview.layout(300, 200); splitview.layout(300, 200);
splitview.addPanel({ id: 'panel1', component: 'default' }); splitview.addPanel({ id: 'panel1', component: 'testPanel' });
splitview.addPanel({ id: 'panel2', component: 'default' }); splitview.addPanel({ id: 'panel2', component: 'testPanel' });
const panel1 = splitview.getPanel('panel1') as SplitviewPanel; const panel1 = splitview.getPanel('panel1') as SplitviewPanel;
const panel2 = splitview.getPanel('panel2') as SplitviewPanel; const panel2 = splitview.getPanel('panel2') as SplitviewPanel;
@ -382,63 +327,51 @@ describe('componentSplitview', () => {
}); });
test('serialization', () => { test('serialization', () => {
const splitview = new SplitviewComponent(container, { const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: {
switch (options.name) { testPanel: TestPanel,
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
splitview.layout(400, 6); splitview.layout(400, 6);
expect(
container.querySelectorAll('.dv-split-view-container').length
).toBe(1);
splitview.fromJSON({ splitview.fromJSON({
views: [ views: [
{ {
size: 1, size: 1,
data: { id: 'panel1', component: 'default' }, data: { id: 'panel1', component: 'testPanel' },
snap: false, snap: false,
}, },
{ {
size: 2, size: 2,
data: { id: 'panel2', component: 'default' }, data: { id: 'panel2', component: 'testPanel' },
snap: true, snap: true,
}, },
{ size: 3, data: { id: 'panel3', component: 'default' } }, { size: 3, data: { id: 'panel3', component: 'testPanel' } },
], ],
size: 6, size: 6,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
activeView: 'panel1', activeView: 'panel1',
}); });
expect(
container.querySelectorAll('.dv-split-view-container').length
).toBe(1);
expect(splitview.length).toBe(3); expect(splitview.length).toBe(3);
expect(JSON.parse(JSON.stringify(splitview.toJSON()))).toEqual({ expect(JSON.parse(JSON.stringify(splitview.toJSON()))).toEqual({
views: [ views: [
{ {
size: 1, size: 1,
data: { id: 'panel1', component: 'default' }, data: { id: 'panel1', component: 'testPanel' },
snap: false, snap: false,
}, },
{ {
size: 2, size: 2,
data: { id: 'panel2', component: 'default' }, data: { id: 'panel2', component: 'testPanel' },
snap: true, snap: true,
}, },
{ {
size: 3, size: 3,
data: { id: 'panel3', component: 'default' }, data: { id: 'panel3', component: 'testPanel' },
snap: false, snap: false,
}, },
], ],
@ -449,15 +382,11 @@ describe('componentSplitview', () => {
}); });
test('toJSON shouldnt fire any layout events', () => { test('toJSON shouldnt fire any layout events', () => {
const splitview = new SplitviewComponent(container, { const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: {
switch (options.name) { testPanel: TestPanel,
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
@ -465,11 +394,11 @@ describe('componentSplitview', () => {
splitview.addPanel({ splitview.addPanel({
id: 'panel1', id: 'panel1',
component: 'default', component: 'testPanel',
}); });
splitview.addPanel({ splitview.addPanel({
id: 'panel2', id: 'panel2',
component: 'default', component: 'testPanel',
}); });
const disposable = splitview.onDidLayoutChange(() => { const disposable = splitview.onDidLayoutChange(() => {
@ -482,16 +411,41 @@ describe('componentSplitview', () => {
disposable.dispose(); disposable.dispose();
}); });
test('panel is disposed of when component is disposed', () => { test('dispose of splitviewComponent', () => {
const splitview = new SplitviewComponent(container, { expect(container.childNodes.length).toBe(0);
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: {
switch (options.name) { testPanel: TestPanel,
case 'default': },
return new TestPanel(options.id, options.name); });
default:
throw new Error('unsupported'); splitview.layout(1000, 1000);
}
splitview.addPanel({
id: 'panel1',
component: 'testPanel',
});
splitview.addPanel({
id: 'panel2',
component: 'testPanel',
});
expect(container.childNodes.length).toBeGreaterThan(0);
splitview.dispose();
expect(container.childNodes.length).toBe(0);
});
test('panel is disposed of when component is disposed', () => {
const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL,
components: {
default: TestPanel,
}, },
}); });
@ -519,15 +473,11 @@ describe('componentSplitview', () => {
}); });
test('panel is disposed of when removed', () => { test('panel is disposed of when removed', () => {
const splitview = new SplitviewComponent(container, { const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: {
switch (options.name) { default: TestPanel,
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
@ -555,15 +505,11 @@ describe('componentSplitview', () => {
}); });
test('panel is disposed of when fromJSON is called', () => { test('panel is disposed of when fromJSON is called', () => {
const splitview = new SplitviewComponent(container, { const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: {
switch (options.name) { default: TestPanel,
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
@ -595,15 +541,11 @@ describe('componentSplitview', () => {
}); });
test('that fromJSON layouts are resized to the current dimensions', async () => { test('that fromJSON layouts are resized to the current dimensions', async () => {
const splitview = new SplitviewComponent(container, { const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: {
switch (options.name) { testPanel: TestPanel,
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
splitview.layout(400, 600); splitview.layout(400, 600);
@ -612,15 +554,15 @@ describe('componentSplitview', () => {
views: [ views: [
{ {
size: 1, size: 1,
data: { id: 'panel1', component: 'default' }, data: { id: 'panel1', component: 'testPanel' },
snap: false, snap: false,
}, },
{ {
size: 2, size: 2,
data: { id: 'panel2', component: 'default' }, data: { id: 'panel2', component: 'testPanel' },
snap: true, snap: true,
}, },
{ size: 3, data: { id: 'panel3', component: 'default' } }, { size: 3, data: { id: 'panel3', component: 'testPanel' } },
], ],
size: 6, size: 6,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
@ -631,17 +573,17 @@ describe('componentSplitview', () => {
views: [ views: [
{ {
size: 100, size: 100,
data: { id: 'panel1', component: 'default' }, data: { id: 'panel1', component: 'testPanel' },
snap: false, snap: false,
}, },
{ {
size: 200, size: 200,
data: { id: 'panel2', component: 'default' }, data: { id: 'panel2', component: 'testPanel' },
snap: true, snap: true,
}, },
{ {
size: 300, size: 300,
data: { id: 'panel3', component: 'default' }, data: { id: 'panel3', component: 'testPanel' },
snap: false, snap: false,
}, },
], ],
@ -652,15 +594,11 @@ describe('componentSplitview', () => {
}); });
test('that disableAutoResizing is false by default', () => { test('that disableAutoResizing is false by default', () => {
const splitview = new SplitviewComponent(container, { const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: {
switch (options.name) { testPanel: TestPanel,
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
@ -668,15 +606,11 @@ describe('componentSplitview', () => {
}); });
test('that disableAutoResizing can be enabled', () => { test('that disableAutoResizing can be enabled', () => {
const splitview = new SplitviewComponent(container, { const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.VERTICAL, orientation: Orientation.VERTICAL,
createComponent: (options) => { components: {
switch (options.name) { testPanel: TestPanel,
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
disableAutoResizing: true, disableAutoResizing: true,
}); });
@ -685,15 +619,11 @@ describe('componentSplitview', () => {
}); });
test('that setVisible toggles visiblity', () => { test('that setVisible toggles visiblity', () => {
const splitview = new SplitviewComponent(container, { const splitview = new SplitviewComponent({
parentElement: container,
orientation: Orientation.HORIZONTAL, orientation: Orientation.HORIZONTAL,
createComponent: (options) => { components: {
switch (options.name) { default: TestPanel,
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
}, },
}); });
@ -719,25 +649,4 @@ describe('componentSplitview', () => {
expect(panel1.api.isVisible).toBeTruthy(); expect(panel1.api.isVisible).toBeTruthy();
expect(panel2.api.isVisible).toBeTruthy(); expect(panel2.api.isVisible).toBeTruthy();
}); });
test('update className', () => {
const splitview = new SplitviewComponent(container, {
orientation: Orientation.HORIZONTAL,
createComponent: (options) => {
switch (options.name) {
case 'default':
return new TestPanel(options.id, options.name);
default:
throw new Error('unsupported');
}
},
className: 'test-a test-b',
});
expect(splitview.element.className).toBe('test-a test-b');
splitview.updateOptions({ className: 'test-b test-c' });
expect(splitview.element.className).toBe('test-b test-c');
});
}); });

View File

@ -1,16 +1,10 @@
import { import {
DockviewMaximizedGroupChanged,
FloatingGroupOptions,
IDockviewComponent, IDockviewComponent,
MovePanelEvent,
PopoutGroupChangePositionEvent,
PopoutGroupChangeSizeEvent,
SerializedDockview, SerializedDockview,
} from '../dockview/dockviewComponent'; } from '../dockview/dockviewComponent';
import { import {
AddGroupOptions, AddGroupOptions,
AddPanelOptions, AddPanelOptions,
DockviewComponentOptions,
DockviewDndOverlayEvent, DockviewDndOverlayEvent,
MovementOptions, MovementOptions,
} from '../dockview/options'; } from '../dockview/options';
@ -33,6 +27,7 @@ import {
AddSplitviewComponentOptions, AddSplitviewComponentOptions,
ISplitviewComponent, ISplitviewComponent,
SerializedSplitview, SerializedSplitview,
SplitviewComponentUpdateOptions,
} from '../splitview/splitviewComponent'; } from '../splitview/splitviewComponent';
import { IView, Orientation, Sizing } from '../splitview/splitview'; import { IView, Orientation, Sizing } from '../splitview/splitview';
import { ISplitviewPanel } from '../splitview/splitviewPanel'; import { ISplitviewPanel } from '../splitview/splitviewPanel';
@ -40,9 +35,9 @@ import {
DockviewGroupPanel, DockviewGroupPanel,
IDockviewGroupPanel, IDockviewGroupPanel,
} from '../dockview/dockviewGroupPanel'; } from '../dockview/dockviewGroupPanel';
import { Event } from '../events'; import { Emitter, Event } from '../events';
import { IDockviewPanel } from '../dockview/dockviewPanel'; import { IDockviewPanel } from '../dockview/dockviewPanel';
import { PaneviewDidDropEvent } from '../paneview/draggablePaneviewPanel'; import { PaneviewDropEvent } from '../paneview/draggablePaneviewPanel';
import { import {
GroupDragEvent, GroupDragEvent,
TabDragEvent, TabDragEvent,
@ -53,12 +48,6 @@ import {
DockviewWillDropEvent, DockviewWillDropEvent,
WillShowOverlayLocationEvent, WillShowOverlayLocationEvent,
} from '../dockview/dockviewGroupPanelModel'; } from '../dockview/dockviewGroupPanelModel';
import {
PaneviewComponentOptions,
PaneviewDndOverlayEvent,
} from '../paneview/options';
import { SplitviewComponentOptions } from '../splitview/options';
import { GridviewComponentOptions } from '../gridview/options';
export interface CommonApi<T = any> { export interface CommonApi<T = any> {
readonly height: number; readonly height: number;
@ -70,7 +59,6 @@ export interface CommonApi<T = any> {
fromJSON(data: T): void; fromJSON(data: T): void;
toJSON(): T; toJSON(): T;
clear(): void; clear(): void;
dispose(): void;
} }
export class SplitviewApi implements CommonApi<SerializedSplitview> { export class SplitviewApi implements CommonApi<SerializedSplitview> {
@ -153,6 +141,13 @@ export class SplitviewApi implements CommonApi<SerializedSplitview> {
constructor(private readonly component: ISplitviewComponent) {} constructor(private readonly component: ISplitviewComponent) {}
/**
* Update configuratable options.
*/
updateOptions(options: SplitviewComponentUpdateOptions): void {
this.component.updateOptions(options);
}
/** /**
* Removes an existing panel and optionally provide a `Sizing` method * Removes an existing panel and optionally provide a `Sizing` method
* for the subsequent resize. * for the subsequent resize.
@ -216,20 +211,6 @@ export class SplitviewApi implements CommonApi<SerializedSplitview> {
clear(): void { clear(): void {
this.component.clear(); this.component.clear();
} }
/**
* Update configuratable options.
*/
updateOptions(options: Partial<SplitviewComponentOptions>): void {
this.component.updateOptions(options);
}
/**
* Release resources and teardown component. Do not call when using framework versions of dockview.
*/
dispose(): void {
this.component.dispose();
}
} }
export class PaneviewApi implements CommonApi<SerializedPaneview> { export class PaneviewApi implements CommonApi<SerializedPaneview> {
@ -299,12 +280,19 @@ export class PaneviewApi implements CommonApi<SerializedPaneview> {
/** /**
* Invoked when a Drag'n'Drop event occurs that the component was unable to handle. Exposed for custom Drag'n'Drop functionality. * Invoked when a Drag'n'Drop event occurs that the component was unable to handle. Exposed for custom Drag'n'Drop functionality.
*/ */
get onDidDrop(): Event<PaneviewDidDropEvent> { get onDidDrop(): Event<PaneviewDropEvent> {
return this.component.onDidDrop; const emitter = new Emitter<PaneviewDropEvent>();
}
get onUnhandledDragOverEvent(): Event<PaneviewDndOverlayEvent> { const disposable = this.component.onDidDrop((e) => {
return this.component.onUnhandledDragOverEvent; emitter.fire({ ...e, api: this });
});
emitter.dispose = () => {
disposable.dispose();
emitter.dispose();
};
return emitter.event;
} }
constructor(private readonly component: IPaneviewComponent) {} constructor(private readonly component: IPaneviewComponent) {}
@ -373,20 +361,6 @@ export class PaneviewApi implements CommonApi<SerializedPaneview> {
clear(): void { clear(): void {
this.component.clear(); this.component.clear();
} }
/**
* Update configuratable options.
*/
updateOptions(options: Partial<PaneviewComponentOptions>): void {
this.component.updateOptions(options);
}
/**
* Release resources and teardown component. Do not call when using framework versions of dockview.
*/
dispose(): void {
this.component.dispose();
}
} }
export class GridviewApi implements CommonApi<SerializedGridviewComponent> { export class GridviewApi implements CommonApi<SerializedGridviewComponent> {
@ -554,17 +528,6 @@ export class GridviewApi implements CommonApi<SerializedGridviewComponent> {
clear(): void { clear(): void {
this.component.clear(); this.component.clear();
} }
updateOptions(options: Partial<GridviewComponentOptions>) {
this.component.updateOptions(options);
}
/**
* Release resources and teardown component. Do not call when using framework versions of dockview.
*/
dispose(): void {
this.component.dispose();
}
} }
export class DockviewApi implements CommonApi<SerializedDockview> { export class DockviewApi implements CommonApi<SerializedDockview> {
@ -673,10 +636,6 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.onDidRemovePanel; return this.component.onDidRemovePanel;
} }
get onDidMovePanel(): Event<MovePanelEvent> {
return this.component.onDidMovePanel;
}
/** /**
* Invoked after a layout is deserialzied using the `fromJSON` method. * Invoked after a layout is deserialzied using the `fromJSON` method.
*/ */
@ -741,14 +700,6 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.onUnhandledDragOverEvent; return this.component.onUnhandledDragOverEvent;
} }
get onDidPopoutGroupSizeChange(): Event<PopoutGroupChangeSizeEvent> {
return this.component.onDidPopoutGroupSizeChange;
}
get onDidPopoutGroupPositionChange(): Event<PopoutGroupChangePositionEvent> {
return this.component.onDidPopoutGroupPositionChange;
}
/** /**
* All panel objects. * All panel objects.
*/ */
@ -849,9 +800,9 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
*/ */
addFloatingGroup( addFloatingGroup(
item: IDockviewPanel | DockviewGroupPanel, item: IDockviewPanel | DockviewGroupPanel,
options?: FloatingGroupOptions coord?: { x: number; y: number }
): void { ): void {
return this.component.addFloatingGroup(item, options); return this.component.addFloatingGroup(item, coord);
} }
/** /**
@ -901,7 +852,7 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
this.component.exitMaximizedGroup(); this.component.exitMaximizedGroup();
} }
get onDidMaximizedGroupChange(): Event<DockviewMaximizedGroupChanged> { get onDidMaximizedGroupChange(): Event<void> {
return this.component.onDidMaximizedGroupChange; return this.component.onDidMaximizedGroupChange;
} }
@ -916,18 +867,7 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
onDidOpen?: (event: { id: string; window: Window }) => void; onDidOpen?: (event: { id: string; window: Window }) => void;
onWillClose?: (event: { id: string; window: Window }) => void; onWillClose?: (event: { id: string; window: Window }) => void;
} }
): Promise<boolean> { ): Promise<void> {
return this.component.addPopoutGroup(item, options); return this.component.addPopoutGroup(item, options);
} }
updateOptions(options: Partial<DockviewComponentOptions>) {
this.component.updateOptions(options);
}
/**
* Release resources and teardown component. Do not call when using framework versions of dockview.
*/
dispose(): void {
this.component.dispose();
}
} }

View File

@ -6,17 +6,9 @@ import {
DockviewGroupLocation, DockviewGroupLocation,
} from '../dockview/dockviewGroupPanelModel'; } from '../dockview/dockviewGroupPanelModel';
import { Emitter, Event } from '../events'; import { Emitter, Event } from '../events';
import { MutableDisposable } from '../lifecycle';
import { GridviewPanelApi, GridviewPanelApiImpl } from './gridviewPanelApi'; import { GridviewPanelApi, GridviewPanelApiImpl } from './gridviewPanelApi';
export interface DockviewGroupMoveParams {
group?: DockviewGroupPanel;
position?: Position;
/**
* The index to place the panel within a group, only applicable if the placement is within an existing group
*/
index?: number;
}
export interface DockviewGroupPanelApi extends GridviewPanelApi { export interface DockviewGroupPanelApi extends GridviewPanelApi {
readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent>; readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent>;
readonly onDidActivePanelChange: Event<DockviewGroupChangeEvent>; readonly onDidActivePanelChange: Event<DockviewGroupChangeEvent>;
@ -25,7 +17,7 @@ export interface DockviewGroupPanelApi extends GridviewPanelApi {
* If you require the Window object * If you require the Window object
*/ */
getWindow(): Window; getWindow(): Window;
moveTo(options: DockviewGroupMoveParams): void; moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void;
maximize(): void; maximize(): void;
isMaximized(): boolean; isMaximized(): boolean;
exitMaximized(): void; exitMaximized(): void;
@ -36,10 +28,12 @@ export interface DockviewGroupPanelFloatingChangeEvent {
readonly location: DockviewGroupLocation; readonly location: DockviewGroupLocation;
} }
const NOT_INITIALIZED_MESSAGE = // TODO find a better way to initialize and avoid needing null checks
'dockview: DockviewGroupPanelApiImpl not initialized'; const NOT_INITIALIZED_MESSAGE = 'DockviewGroupPanelApiImpl not initialized';
export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl { export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
private readonly _mutableDisposable = new MutableDisposable();
private _group: DockviewGroupPanel | undefined; private _group: DockviewGroupPanel | undefined;
readonly _onDidLocationChange = readonly _onDidLocationChange =
@ -47,7 +41,8 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent> = readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent> =
this._onDidLocationChange.event; this._onDidLocationChange.event;
readonly _onDidActivePanelChange = new Emitter<DockviewGroupChangeEvent>(); private readonly _onDidActivePanelChange =
new Emitter<DockviewGroupChangeEvent>();
readonly onDidActivePanelChange = this._onDidActivePanelChange.event; readonly onDidActivePanelChange = this._onDidActivePanelChange.event;
get location(): DockviewGroupLocation { get location(): DockviewGroupLocation {
@ -62,7 +57,8 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
this.addDisposables( this.addDisposables(
this._onDidLocationChange, this._onDidLocationChange,
this._onDidActivePanelChange this._onDidActivePanelChange,
this._mutableDisposable
); );
} }
@ -79,7 +75,7 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
: window; : window;
} }
moveTo(options: DockviewGroupMoveParams): void { moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void {
if (!this._group) { if (!this._group) {
throw new Error(NOT_INITIALIZED_MESSAGE); throw new Error(NOT_INITIALIZED_MESSAGE);
} }
@ -98,7 +94,6 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
position: options.group position: options.group
? options.position ?? 'center' ? options.position ?? 'center'
: 'center', : 'center',
index: options.index,
}, },
}); });
} }
@ -136,5 +131,19 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
initialize(group: DockviewGroupPanel): void { initialize(group: DockviewGroupPanel): void {
this._group = group; this._group = group;
/**
* TODO: Annoying initialization order caveat
*
* Due to the order on initialization we know that the model isn't defined until later in the same stack-frame of setup.
* By queuing a microtask we can ensure the setup is completed within the same stack-frame, but after everything else has
* finished ensuring the `model` is defined.
*/
queueMicrotask(() => {
this._mutableDisposable.value =
this._group!.model.onDidActivePanelChange((event) => {
this._onDidActivePanelChange.fire(event);
});
});
} }
} }

View File

@ -4,11 +4,9 @@ import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { CompositeDisposable, MutableDisposable } from '../lifecycle'; import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { DockviewPanel } from '../dockview/dockviewPanel'; import { DockviewPanel } from '../dockview/dockviewPanel';
import { DockviewComponent } from '../dockview/dockviewComponent'; import { DockviewComponent } from '../dockview/dockviewComponent';
import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer'; import { Position } from '../dnd/droptarget';
import { import { DockviewPanelRenderer } from '../overlayRenderContainer';
DockviewGroupMoveParams, import { DockviewGroupPanelFloatingChangeEvent } from './dockviewGroupPanelApi';
DockviewGroupPanelFloatingChangeEvent,
} from './dockviewGroupPanelApi';
import { DockviewGroupLocation } from '../dockview/dockviewGroupPanelModel'; import { DockviewGroupLocation } from '../dockview/dockviewGroupPanelModel';
export interface TitleEvent { export interface TitleEvent {
@ -27,8 +25,6 @@ export interface GroupChangedEvent {
// empty // empty
} }
export type DockviewPanelMoveParams = DockviewGroupMoveParams;
export interface DockviewPanelApi export interface DockviewPanelApi
extends Omit< extends Omit<
GridviewPanelApi, GridviewPanelApi,
@ -54,7 +50,11 @@ export interface DockviewPanelApi
close(): void; close(): void;
setTitle(title: string): void; setTitle(title: string): void;
setRenderer(renderer: DockviewPanelRenderer): void; setRenderer(renderer: DockviewPanelRenderer): void;
moveTo(options: DockviewPanelMoveParams): void; moveTo(options: {
group: DockviewGroupPanel;
position?: Position;
index?: number;
}): void;
maximize(): void; maximize(): void;
isMaximized(): boolean; isMaximized(): boolean;
exitMaximized(): void; exitMaximized(): void;
@ -69,7 +69,7 @@ export class DockviewPanelApiImpl
implements DockviewPanelApi implements DockviewPanelApi
{ {
private _group: DockviewGroupPanel; private _group: DockviewGroupPanel;
private readonly _tabComponent: string | undefined; private _tabComponent: string | undefined;
readonly _onDidTitleChange = new Emitter<TitleEvent>(); readonly _onDidTitleChange = new Emitter<TitleEvent>();
readonly onDidTitleChange = this._onDidTitleChange.event; readonly onDidTitleChange = this._onDidTitleChange.event;
@ -131,7 +131,7 @@ export class DockviewPanelApiImpl
} }
constructor( constructor(
private readonly panel: DockviewPanel, private panel: DockviewPanel,
group: DockviewGroupPanel, group: DockviewGroupPanel,
private readonly accessor: DockviewComponent, private readonly accessor: DockviewComponent,
component: string, component: string,
@ -160,14 +160,16 @@ export class DockviewPanelApiImpl
return this.group.api.getWindow(); return this.group.api.getWindow();
} }
moveTo(options: DockviewPanelMoveParams): void { moveTo(options: {
group: DockviewGroupPanel;
position?: Position;
index?: number;
}): void {
this.accessor.moveGroupOrPanel({ this.accessor.moveGroupOrPanel({
from: { groupId: this._group.id, panelId: this.panel.id }, from: { groupId: this._group.id, panelId: this.panel.id },
to: { to: {
group: options.group ?? this._group, group: options.group,
position: options.group position: options.position ?? 'center',
? options.position ?? 'center'
: 'center',
index: options.index, index: options.index,
}, },
}); });
@ -202,14 +204,13 @@ export class DockviewPanelApiImpl
this.groupEventsDisposable.value = new CompositeDisposable( this.groupEventsDisposable.value = new CompositeDisposable(
this.group.api.onDidVisibilityChange((event) => { this.group.api.onDidVisibilityChange((event) => {
const hasBecomeHidden = !event.isVisible && this.isVisible; if (!event.isVisible && this.isVisible) {
const hasBecomeVisible = event.isVisible && !this.isVisible; this._onDidVisibilityChange.fire(event);
} else if (
const isActivePanel = this.group.model.isPanelActive( event.isVisible &&
this.panel !this.isVisible &&
); this.group.model.isPanelActive(this.panel)
) {
if (hasBecomeHidden || (hasBecomeVisible && isActivePanel)) {
this._onDidVisibilityChange.fire(event); this._onDidVisibilityChange.fire(event);
} }
}), }),

View File

@ -1,46 +0,0 @@
import {
DockviewApi,
GridviewApi,
PaneviewApi,
SplitviewApi,
} from '../api/component.api';
import { DockviewComponent } from '../dockview/dockviewComponent';
import { DockviewComponentOptions } from '../dockview/options';
import { GridviewComponent } from '../gridview/gridviewComponent';
import { GridviewComponentOptions } from '../gridview/options';
import { PaneviewComponentOptions } from '../paneview/options';
import { PaneviewComponent } from '../paneview/paneviewComponent';
import { SplitviewComponentOptions } from '../splitview/options';
import { SplitviewComponent } from '../splitview/splitviewComponent';
export function createDockview(
element: HTMLElement,
options: DockviewComponentOptions
): DockviewApi {
const component = new DockviewComponent(element, options);
return component.api;
}
export function createSplitview(
element: HTMLElement,
options: SplitviewComponentOptions
): SplitviewApi {
const component = new SplitviewComponent(element, options);
return new SplitviewApi(component);
}
export function createGridview(
element: HTMLElement,
options: GridviewComponentOptions
): GridviewApi {
const component = new GridviewComponent(element, options);
return new GridviewApi(component);
}
export function createPaneview(
element: HTMLElement,
options: PaneviewComponentOptions
): PaneviewApi {
const component = new PaneviewComponent(element, options);
return new PaneviewApi(component);
}

View File

@ -1,3 +1,3 @@
export const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100; export const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100;
export const DEFAULT_FLOATING_GROUP_POSITION = { left: 100, top: 100, width: 300, height: 300 }; export const DEFAULT_FLOATING_GROUP_POSITION = { left: 100, top: 100 };

View File

@ -1,4 +1,4 @@
import { disableIframePointEvents } from '../dom'; import { getElementsByTagName } from '../dom';
import { addDisposableListener, Emitter } from '../events'; import { addDisposableListener, Emitter } from '../events';
import { import {
CompositeDisposable, CompositeDisposable,
@ -40,14 +40,23 @@ export abstract class DragHandler extends CompositeDisposable {
return; return;
} }
const iframes = disableIframePointEvents(); const iframes = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
this.pointerEventsDisposable.value = { this.pointerEventsDisposable.value = {
dispose: () => { dispose: () => {
iframes.release(); for (const iframe of iframes) {
iframe.style.pointerEvents = 'auto';
}
}, },
}; };
for (const iframe of iframes) {
iframe.style.pointerEvents = 'none';
}
this.el.classList.add('dv-dragged'); this.el.classList.add('dv-dragged');
setTimeout(() => this.el.classList.remove('dv-dragged'), 0); setTimeout(() => this.el.classList.remove('dv-dragged'), 0);
@ -67,17 +76,18 @@ export abstract class DragHandler extends CompositeDisposable {
* For example: in react-dnd if dataTransfer.types is not set then the dragStart event will be cancelled * For example: in react-dnd if dataTransfer.types is not set then the dragStart event will be cancelled
* through .preventDefault(). Since this is applied globally to all drag events this would break dockviews * through .preventDefault(). Since this is applied globally to all drag events this would break dockviews
* dnd logic. You can see the code at * dnd logic. You can see the code at
P * https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542 * https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542
*/ */
event.dataTransfer.setData('text/plain', ''); event.dataTransfer.setData(
'text/plain',
'__dockview_internal_drag_event__'
);
} }
} }
}), }),
addDisposableListener(this.el, 'dragend', () => { addDisposableListener(this.el, 'dragend', () => {
this.pointerEventsDisposable.dispose(); this.pointerEventsDisposable.dispose();
setTimeout(() => { this.dataDisposable.dispose();
this.dataDisposable.dispose(); // allow the data to be read by other handlers before disposing
}, 0);
}) })
); );
} }

View File

@ -1,6 +1,4 @@
class TransferObject { class TransferObject {}
// intentionally empty class
}
export class PanelTransfer extends TransferObject { export class PanelTransfer extends TransferObject {
constructor( constructor(

View File

@ -13,8 +13,8 @@ export class DragAndDropObserver extends CompositeDisposable {
private target: EventTarget | null = null; private target: EventTarget | null = null;
constructor( constructor(
private readonly element: HTMLElement, private element: HTMLElement,
private readonly callbacks: IDragAndDropObserverCallbacks private callbacks: IDragAndDropObserverCallbacks
) { ) {
super(); super();

View File

@ -1,23 +0,0 @@
.dv-drop-target-container {
position: absolute;
z-index: 9999;
top: 0px;
left: 0px;
height: 100%;
width: 100%;
pointer-events: none;
overflow: hidden;
--dv-transition-duration: 300ms;
.dv-drop-target-anchor {
position: relative;
border: var(--dv-drag-over-border);
transition: opacity var(--dv-transition-duration) ease-in,
top var(--dv-transition-duration) ease-out,
left var(--dv-transition-duration) ease-out,
width var(--dv-transition-duration) ease-out,
height var(--dv-transition-duration) ease-out;
background-color: var(--dv-drag-over-background-color);
opacity: 1;
}
}

View File

@ -1,102 +0,0 @@
import { CompositeDisposable, Disposable } from '../lifecycle';
import { DropTargetTargetModel } from './droptarget';
export class DropTargetAnchorContainer extends CompositeDisposable {
private _model:
| { root: HTMLElement; overlay: HTMLElement; changed: boolean }
| undefined;
private _outline: HTMLElement | undefined;
private _disabled = false;
get disabled(): boolean {
return this._disabled;
}
set disabled(value: boolean) {
if (this.disabled === value) {
return;
}
this._disabled = value;
if (value) {
this.model?.clear();
}
}
get model(): DropTargetTargetModel | undefined {
if (this.disabled) {
return undefined;
}
return {
clear: () => {
if (this._model) {
this._model.root.parentElement?.removeChild(
this._model.root
);
}
this._model = undefined;
},
exists: () => {
return !!this._model;
},
getElements: (event?: DragEvent, outline?: HTMLElement) => {
const changed = this._outline !== outline;
this._outline = outline;
if (this._model) {
this._model.changed = changed;
return this._model;
}
const container = this.createContainer();
const anchor = this.createAnchor();
this._model = { root: container, overlay: anchor, changed };
container.appendChild(anchor);
this.element.appendChild(container);
if (event?.target instanceof HTMLElement) {
const targetBox = event.target.getBoundingClientRect();
const box = this.element.getBoundingClientRect();
anchor.style.left = `${targetBox.left - box.left}px`;
anchor.style.top = `${targetBox.top - box.top}px`;
}
return this._model;
},
};
}
constructor(readonly element: HTMLElement, options: { disabled: boolean }) {
super();
this._disabled = options.disabled;
this.addDisposables(
Disposable.from(() => {
this.model?.clear();
})
);
}
private createContainer(): HTMLElement {
const el = document.createElement('div');
el.className = 'dv-drop-target-container';
return el;
}
private createAnchor(): HTMLElement {
const el = document.createElement('div');
el.className = 'dv-drop-target-anchor';
el.style.visibility = 'hidden';
return el;
}
}

View File

@ -1,8 +1,7 @@
.dv-drop-target { .drop-target {
position: relative; position: relative;
--dv-transition-duration: 70ms;
> .dv-drop-target-dropzone { > .drop-target-dropzone {
position: absolute; position: absolute;
left: 0px; left: 0px;
top: 0px; top: 0px;
@ -11,18 +10,15 @@
z-index: 1000; z-index: 1000;
pointer-events: none; pointer-events: none;
> .dv-drop-target-selection { > .drop-target-selection {
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
height: 100%; height: 100%;
width: 100%; width: 100%;
border: var(--dv-drag-over-border);
background-color: var(--dv-drag-over-background-color); background-color: var(--dv-drag-over-background-color);
transition: top var(--dv-transition-duration) ease-out, transition: top 70ms ease-out, left 70ms ease-out,
left var(--dv-transition-duration) ease-out, width 70ms ease-out, height 70ms ease-out,
width var(--dv-transition-duration) ease-out, opacity 0.15s ease-out;
height var(--dv-transition-duration) ease-out,
opacity var(--dv-transition-duration) ease-out;
will-change: transform; will-change: transform;
pointer-events: none; pointer-events: none;

View File

@ -93,26 +93,10 @@ const DEFAULT_SIZE: MeasuredValue = {
const SMALL_WIDTH_BOUNDARY = 100; const SMALL_WIDTH_BOUNDARY = 100;
const SMALL_HEIGHT_BOUNDARY = 100; const SMALL_HEIGHT_BOUNDARY = 100;
export interface DropTargetTargetModel {
getElements(
event?: DragEvent,
outline?: HTMLElement
): {
root: HTMLElement;
overlay: HTMLElement;
changed: boolean;
};
exists(): boolean;
clear(): void;
}
export interface DroptargetOptions { export interface DroptargetOptions {
canDisplayOverlay: CanDisplayOverlay; canDisplayOverlay: CanDisplayOverlay;
acceptedTargetZones: Position[]; acceptedTargetZones: Position[];
overlayModel?: DroptargetOverlayModel; overlayModel?: DroptargetOverlayModel;
getOverrideTarget?: () => DropTargetTargetModel | undefined;
className?: string;
getOverlayOutline?: () => HTMLElement | null;
} }
export class Droptarget extends CompositeDisposable { export class Droptarget extends CompositeDisposable {
@ -132,18 +116,6 @@ export class Droptarget extends CompositeDisposable {
private static USED_EVENT_ID = '__dockview_droptarget_event_is_used__'; private static USED_EVENT_ID = '__dockview_droptarget_event_is_used__';
private static ACTUAL_TARGET: Droptarget | undefined;
private _disabled: boolean;
get disabled(): boolean {
return this._disabled;
}
set disabled(value: boolean) {
this._disabled = value;
}
get state(): Position | undefined { get state(): Position | undefined {
return this._state; return this._state;
} }
@ -154,35 +126,21 @@ export class Droptarget extends CompositeDisposable {
) { ) {
super(); super();
this._disabled = false;
// use a set to take advantage of #<set>.has // use a set to take advantage of #<set>.has
this._acceptedTargetZonesSet = new Set( this._acceptedTargetZonesSet = new Set(
this.options.acceptedTargetZones this.options.acceptedTargetZones
); );
this.dnd = new DragAndDropObserver(this.element, { this.dnd = new DragAndDropObserver(this.element, {
onDragEnter: () => { onDragEnter: () => undefined,
this.options.getOverrideTarget?.()?.getElements();
},
onDragOver: (e) => { onDragOver: (e) => {
Droptarget.ACTUAL_TARGET = this;
const overrideTraget = this.options.getOverrideTarget?.();
if (this._acceptedTargetZonesSet.size === 0) { if (this._acceptedTargetZonesSet.size === 0) {
if (overrideTraget) {
return;
}
this.removeDropTarget(); this.removeDropTarget();
return; return;
} }
const target = const width = this.element.clientWidth;
this.options.getOverlayOutline?.() ?? this.element; const height = this.element.clientHeight;
const width = target.offsetWidth;
const height = target.offsetHeight;
if (width === 0 || height === 0) { if (width === 0 || height === 0) {
return; // avoid div!0 return; // avoid div!0
@ -191,8 +149,8 @@ export class Droptarget extends CompositeDisposable {
const rect = ( const rect = (
e.currentTarget as HTMLElement e.currentTarget as HTMLElement
).getBoundingClientRect(); ).getBoundingClientRect();
const x = (e.clientX ?? 0) - rect.left; const x = e.clientX - rect.left;
const y = (e.clientY ?? 0) - rect.top; const y = e.clientY - rect.top;
const quadrant = this.calculateQuadrant( const quadrant = this.calculateQuadrant(
this._acceptedTargetZonesSet, this._acceptedTargetZonesSet,
@ -214,9 +172,6 @@ export class Droptarget extends CompositeDisposable {
} }
if (!this.options.canDisplayOverlay(e, quadrant)) { if (!this.options.canDisplayOverlay(e, quadrant)) {
if (overrideTraget) {
return;
}
this.removeDropTarget(); this.removeDropTarget();
return; return;
} }
@ -239,26 +194,16 @@ export class Droptarget extends CompositeDisposable {
this.markAsUsed(e); this.markAsUsed(e);
if (overrideTraget) { if (!this.targetElement) {
//
} else if (!this.targetElement) {
this.targetElement = document.createElement('div'); this.targetElement = document.createElement('div');
this.targetElement.className = 'dv-drop-target-dropzone'; this.targetElement.className = 'drop-target-dropzone';
this.overlayElement = document.createElement('div'); this.overlayElement = document.createElement('div');
this.overlayElement.className = 'dv-drop-target-selection'; this.overlayElement.className = 'drop-target-selection';
this._state = 'center'; this._state = 'center';
this.targetElement.appendChild(this.overlayElement); this.targetElement.appendChild(this.overlayElement);
target.classList.add('dv-drop-target'); this.element.classList.add('drop-target');
target.append(this.targetElement); this.element.append(this.targetElement);
// this.overlayElement.style.opacity = '0';
// requestAnimationFrame(() => {
// if (this.overlayElement) {
// this.overlayElement.style.opacity = '';
// }
// });
} }
this.toggleClasses(quadrant, width, height); this.toggleClasses(quadrant, width, height);
@ -266,32 +211,10 @@ export class Droptarget extends CompositeDisposable {
this._state = quadrant; this._state = quadrant;
}, },
onDragLeave: () => { onDragLeave: () => {
const target = this.options.getOverrideTarget?.();
if (target) {
return;
}
this.removeDropTarget(); this.removeDropTarget();
}, },
onDragEnd: (e) => { onDragEnd: () => {
const target = this.options.getOverrideTarget?.();
if (target && Droptarget.ACTUAL_TARGET === this) {
if (this._state) {
// only stop the propagation of the event if we are dealing with it
// which is only when the target has state
e.stopPropagation();
this._onDrop.fire({
position: this._state,
nativeEvent: e,
});
}
}
this.removeDropTarget(); this.removeDropTarget();
target?.clear();
}, },
onDrop: (e) => { onDrop: (e) => {
e.preventDefault(); e.preventDefault();
@ -300,8 +223,6 @@ export class Droptarget extends CompositeDisposable {
this.removeDropTarget(); this.removeDropTarget();
this.options.getOverrideTarget?.()?.clear();
if (state) { if (state) {
// only stop the propagation of the event if we are dealing with it // only stop the propagation of the event if we are dealing with it
// which is only when the target has state // which is only when the target has state
@ -347,9 +268,7 @@ export class Droptarget extends CompositeDisposable {
width: number, width: number,
height: number height: number
): void { ): void {
const target = this.options.getOverrideTarget?.(); if (!this.overlayElement) {
if (!target && !this.overlayElement) {
return; return;
} }
@ -381,103 +300,6 @@ export class Droptarget extends CompositeDisposable {
} }
} }
if (target) {
const outlineEl =
this.options.getOverlayOutline?.() ?? this.element;
const elBox = outlineEl.getBoundingClientRect();
const ta = target.getElements(undefined, outlineEl);
const el = ta.root;
const overlay = ta.overlay;
const bigbox = el.getBoundingClientRect();
const rootTop = elBox.top - bigbox.top;
const rootLeft = elBox.left - bigbox.left;
const box = {
top: rootTop,
left: rootLeft,
width: width,
height: height,
};
if (rightClass) {
box.left = rootLeft + width * (1 - size);
box.width = width * size;
} else if (leftClass) {
box.width = width * size;
} else if (topClass) {
box.height = height * size;
} else if (bottomClass) {
box.top = rootTop + height * (1 - size);
box.height = height * size;
}
if (isSmallX && isLeft) {
box.width = 4;
}
if (isSmallX && isRight) {
box.left = rootLeft + width - 4;
box.width = 4;
}
const topPx = `${Math.round(box.top)}px`;
const leftPx = `${Math.round(box.left)}px`;
const widthPx = `${Math.round(box.width)}px`;
const heightPx = `${Math.round(box.height)}px`;
if (
overlay.style.top === topPx &&
overlay.style.left === leftPx &&
overlay.style.width === widthPx &&
overlay.style.height === heightPx
) {
return;
}
overlay.style.top = topPx;
overlay.style.left = leftPx;
overlay.style.width = widthPx;
overlay.style.height = heightPx;
overlay.style.visibility = 'visible';
overlay.className = `dv-drop-target-anchor${
this.options.className ? ` ${this.options.className}` : ''
}`;
toggleClass(overlay, 'dv-drop-target-left', isLeft);
toggleClass(overlay, 'dv-drop-target-right', isRight);
toggleClass(overlay, 'dv-drop-target-top', isTop);
toggleClass(overlay, 'dv-drop-target-bottom', isBottom);
toggleClass(
overlay,
'dv-drop-target-center',
quadrant === 'center'
);
if (ta.changed) {
toggleClass(
overlay,
'dv-drop-target-anchor-container-changed',
true
);
setTimeout(() => {
toggleClass(
overlay,
'dv-drop-target-anchor-container-changed',
false
);
}, 10);
}
return;
}
if (!this.overlayElement) {
return;
}
const box = { top: '0px', left: '0px', width: '100%', height: '100%' }; const box = { top: '0px', left: '0px', width: '100%', height: '100%' };
/** /**
@ -574,12 +396,10 @@ export class Droptarget extends CompositeDisposable {
private removeDropTarget(): void { private removeDropTarget(): void {
if (this.targetElement) { if (this.targetElement) {
this._state = undefined; this._state = undefined;
this.targetElement.parentElement?.classList.remove( this.element.removeChild(this.targetElement);
'dv-drop-target'
);
this.targetElement.remove();
this.targetElement = undefined; this.targetElement = undefined;
this.overlayElement = undefined; this.overlayElement = undefined;
this.element.classList.remove('drop-target');
} }
} }
} }

View File

@ -2,17 +2,13 @@ import { addClasses, removeClasses } from '../dom';
export function addGhostImage( export function addGhostImage(
dataTransfer: DataTransfer, dataTransfer: DataTransfer,
ghostElement: HTMLElement, ghostElement: HTMLElement
options?: { x?: number; y?: number }
): void { ): void {
// class dockview provides to force ghost image to be drawn on a different layer and prevent weird rendering issues // class dockview provides to force ghost image to be drawn on a different layer and prevent weird rendering issues
addClasses(ghostElement, 'dv-dragged'); addClasses(ghostElement, 'dv-dragged');
// move the element off-screen initially otherwise it may in some cases be rendered at (0,0) momentarily
ghostElement.style.top = '-9999px';
document.body.appendChild(ghostElement); document.body.appendChild(ghostElement);
dataTransfer.setDragImage(ghostElement, options?.x ?? 0, options?.y ?? 0); dataTransfer.setDragImage(ghostElement, 0, 0);
setTimeout(() => { setTimeout(() => {
removeClasses(ghostElement, 'dv-dragged'); removeClasses(ghostElement, 'dv-dragged');

View File

@ -21,7 +21,7 @@ export class GroupDragHandler extends DragHandler {
this.addDisposables( this.addDisposables(
addDisposableListener( addDisposableListener(
element, element,
'pointerdown', 'mousedown',
(e) => { (e) => {
if (e.shiftKey) { if (e.shiftKey) {
/** /**
@ -72,11 +72,9 @@ export class GroupDragHandler extends DragHandler {
ghostElement.style.lineHeight = '20px'; ghostElement.style.lineHeight = '20px';
ghostElement.style.borderRadius = '12px'; ghostElement.style.borderRadius = '12px';
ghostElement.style.position = 'absolute'; ghostElement.style.position = 'absolute';
ghostElement.style.pointerEvents = 'none';
ghostElement.style.top = '-9999px';
ghostElement.textContent = `Multiple Panels (${this.group.size})`; ghostElement.textContent = `Multiple Panels (${this.group.size})`;
addGhostImage(dataTransfer, ghostElement, { y: -10, x: 30 }); addGhostImage(dataTransfer, ghostElement);
} }
return { return {

View File

@ -26,18 +26,16 @@
} }
.dv-resize-container { .dv-resize-container {
--dv-overlay-z-index: var(--dv-overlay-z-index, 999);
position: absolute; position: absolute;
z-index: calc(var(--dv-overlay-z-index) - 2); z-index: 997;
&.dv-bring-to-front {
z-index: 998;
}
border: 1px solid var(--dv-tab-divider-color); border: 1px solid var(--dv-tab-divider-color);
box-shadow: var(--dv-floating-box-shadow); box-shadow: var(--dv-floating-box-shadow);
&.dv-hidden {
display: none;
}
&.dv-resize-container-dragging { &.dv-resize-container-dragging {
opacity: 0.5; opacity: 0.5;
} }
@ -47,7 +45,7 @@
width: calc(100% - 8px); width: calc(100% - 8px);
left: 4px; left: 4px;
top: -2px; top: -2px;
z-index: var(--dv-overlay-z-index); z-index: 999;
position: absolute; position: absolute;
cursor: ns-resize; cursor: ns-resize;
} }
@ -57,7 +55,7 @@
width: calc(100% - 8px); width: calc(100% - 8px);
left: 4px; left: 4px;
bottom: -2px; bottom: -2px;
z-index: var(--dv-overlay-z-index); z-index: 999;
position: absolute; position: absolute;
cursor: ns-resize; cursor: ns-resize;
} }
@ -67,7 +65,7 @@
width: 4px; width: 4px;
left: -2px; left: -2px;
top: 4px; top: 4px;
z-index: var(--dv-overlay-z-index); z-index: 999;
position: absolute; position: absolute;
cursor: ew-resize; cursor: ew-resize;
} }
@ -77,7 +75,7 @@
width: 4px; width: 4px;
right: -2px; right: -2px;
top: 4px; top: 4px;
z-index: var(--dv-overlay-z-index); z-index: 999;
position: absolute; position: absolute;
cursor: ew-resize; cursor: ew-resize;
} }
@ -87,7 +85,7 @@
width: 4px; width: 4px;
top: -2px; top: -2px;
left: -2px; left: -2px;
z-index: var(--dv-overlay-z-index); z-index: 999;
position: absolute; position: absolute;
cursor: nw-resize; cursor: nw-resize;
} }
@ -97,7 +95,7 @@
width: 4px; width: 4px;
right: -2px; right: -2px;
top: -2px; top: -2px;
z-index: var(--dv-overlay-z-index); z-index: 999;
position: absolute; position: absolute;
cursor: ne-resize; cursor: ne-resize;
} }
@ -107,7 +105,7 @@
width: 4px; width: 4px;
left: -2px; left: -2px;
bottom: -2px; bottom: -2px;
z-index: var(--dv-overlay-z-index); z-index: 999;
position: absolute; position: absolute;
cursor: sw-resize; cursor: sw-resize;
} }
@ -117,7 +115,7 @@
width: 4px; width: 4px;
right: -2px; right: -2px;
bottom: -2px; bottom: -2px;
z-index: var(--dv-overlay-z-index); z-index: 999;
position: absolute; position: absolute;
cursor: se-resize; cursor: se-resize;
} }

View File

@ -1,5 +1,5 @@
import { import {
disableIframePointEvents, getElementsByTagName,
quasiDefaultPrevented, quasiDefaultPrevented,
toggleClass, toggleClass,
} from '../dom'; } from '../dom';
@ -7,44 +7,29 @@ import {
Emitter, Emitter,
Event, Event,
addDisposableListener, addDisposableListener,
addDisposableWindowListener,
} from '../events'; } from '../events';
import { CompositeDisposable, MutableDisposable } from '../lifecycle'; import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { clamp } from '../math'; import { clamp } from '../math';
import { AnchoredBox } from '../types'; import { Box } from '../types';
class AriaLevelTracker { const bringElementToFront = (() => {
private _orderedList: HTMLElement[] = []; let previous: HTMLElement | null = null;
push(element: HTMLElement): void { function pushToTop(element: HTMLElement) {
this._orderedList = [ if (previous !== element && previous !== null) {
...this._orderedList.filter((item) => item !== element), toggleClass(previous, 'dv-bring-to-front', false);
element,
];
this.update();
}
destroy(element: HTMLElement): void {
this._orderedList = this._orderedList.filter(
(item) => item !== element
);
this.update();
}
private update(): void {
for (let i = 0; i < this._orderedList.length; i++) {
this._orderedList[i].setAttribute('aria-level', `${i}`);
this._orderedList[
i
].style.zIndex = `calc(var(--dv-overlay-z-index, 999) + ${i * 2})`;
} }
}
}
const arialLevelTracker = new AriaLevelTracker(); toggleClass(element, 'dv-bring-to-front', true);
previous = element;
}
return pushToTop;
})();
export class Overlay extends CompositeDisposable { export class Overlay extends CompositeDisposable {
private readonly _element: HTMLElement = document.createElement('div'); private _element: HTMLElement = document.createElement('div');
private readonly _onDidChange = new Emitter<void>(); private readonly _onDidChange = new Emitter<void>();
readonly onDidChange: Event<void> = this._onDidChange.event; readonly onDidChange: Event<void> = this._onDidChange.event;
@ -52,13 +37,8 @@ export class Overlay extends CompositeDisposable {
private readonly _onDidChangeEnd = new Emitter<void>(); private readonly _onDidChangeEnd = new Emitter<void>();
readonly onDidChangeEnd: Event<void> = this._onDidChangeEnd.event; readonly onDidChangeEnd: Event<void> = this._onDidChangeEnd.event;
private static readonly MINIMUM_HEIGHT = 20; private static MINIMUM_HEIGHT = 20;
private static readonly MINIMUM_WIDTH = 20; private static MINIMUM_WIDTH = 20;
private verticalAlignment: 'top' | 'bottom' | undefined;
private horiziontalAlignment: 'left' | 'right' | undefined;
private _isVisible: boolean;
set minimumInViewportWidth(value: number | undefined) { set minimumInViewportWidth(value: number | undefined) {
this.options.minimumInViewportWidth = value; this.options.minimumInViewportWidth = value;
@ -68,16 +48,8 @@ export class Overlay extends CompositeDisposable {
this.options.minimumInViewportHeight = value; this.options.minimumInViewportHeight = value;
} }
get element(): HTMLElement {
return this._element;
}
get isVisible(): boolean {
return this._isVisible;
}
constructor( constructor(
private readonly options: AnchoredBox & { private readonly options: Box & {
container: HTMLElement; container: HTMLElement;
content: HTMLElement; content: HTMLElement;
minimumInViewportWidth?: number; minimumInViewportWidth?: number;
@ -89,7 +61,6 @@ export class Overlay extends CompositeDisposable {
this.addDisposables(this._onDidChange, this._onDidChangeEnd); this.addDisposables(this._onDidChange, this._onDidChangeEnd);
this._element.className = 'dv-resize-container'; this._element.className = 'dv-resize-container';
this._isVisible = true;
this.setupResize('top'); this.setupResize('top');
this.setupResize('bottom'); this.setupResize('bottom');
@ -107,55 +78,23 @@ export class Overlay extends CompositeDisposable {
this.setBounds({ this.setBounds({
height: this.options.height, height: this.options.height,
width: this.options.width, width: this.options.width,
...('top' in this.options && { top: this.options.top }), top: this.options.top,
...('bottom' in this.options && { bottom: this.options.bottom }), left: this.options.left,
...('left' in this.options && { left: this.options.left }),
...('right' in this.options && { right: this.options.right }),
}); });
arialLevelTracker.push(this._element);
} }
setVisible(isVisible: boolean): void { setBounds(bounds: Partial<Box> = {}): void {
if (isVisible === this.isVisible) {
return;
}
this._isVisible = isVisible;
toggleClass(this.element, 'dv-hidden', !this.isVisible);
}
bringToFront(): void {
arialLevelTracker.push(this._element);
}
setBounds(bounds: Partial<AnchoredBox> = {}): void {
if (typeof bounds.height === 'number') { if (typeof bounds.height === 'number') {
this._element.style.height = `${bounds.height}px`; this._element.style.height = `${bounds.height}px`;
} }
if (typeof bounds.width === 'number') { if (typeof bounds.width === 'number') {
this._element.style.width = `${bounds.width}px`; this._element.style.width = `${bounds.width}px`;
} }
if ('top' in bounds && typeof bounds.top === 'number') { if (typeof bounds.top === 'number') {
this._element.style.top = `${bounds.top}px`; this._element.style.top = `${bounds.top}px`;
this._element.style.bottom = 'auto';
this.verticalAlignment = 'top';
} }
if ('bottom' in bounds && typeof bounds.bottom === 'number') { if (typeof bounds.left === 'number') {
this._element.style.bottom = `${bounds.bottom}px`;
this._element.style.top = 'auto';
this.verticalAlignment = 'bottom';
}
if ('left' in bounds && typeof bounds.left === 'number') {
this._element.style.left = `${bounds.left}px`; this._element.style.left = `${bounds.left}px`;
this._element.style.right = 'auto';
this.horiziontalAlignment = 'left';
}
if ('right' in bounds && typeof bounds.right === 'number') {
this._element.style.right = `${bounds.right}px`;
this._element.style.left = 'auto';
this.horiziontalAlignment = 'right';
} }
const containerRect = this.options.container.getBoundingClientRect(); const containerRect = this.options.container.getBoundingClientRect();
@ -167,77 +106,39 @@ export class Overlay extends CompositeDisposable {
const xOffset = Math.max(0, this.getMinimumWidth(overlayRect.width)); const xOffset = Math.max(0, this.getMinimumWidth(overlayRect.width));
// a minimum height of minimumViewportHeight must be inside the viewport // a minimum height of minimumViewportHeight must be inside the viewport
const yOffset = Math.max(0, this.getMinimumHeight(overlayRect.height)); const yOffset =
typeof this.options.minimumInViewportHeight === 'number'
? Math.max(0, this.getMinimumHeight(overlayRect.height))
: 0;
if (this.verticalAlignment === 'top') { const left = clamp(
const top = clamp( overlayRect.left - containerRect.left,
overlayRect.top - containerRect.top, -xOffset,
-yOffset, Math.max(0, containerRect.width - overlayRect.width + xOffset)
Math.max(0, containerRect.height - overlayRect.height + yOffset) );
);
this._element.style.top = `${top}px`;
this._element.style.bottom = 'auto';
}
if (this.verticalAlignment === 'bottom') { const top = clamp(
const bottom = clamp( overlayRect.top - containerRect.top,
containerRect.bottom - overlayRect.bottom, -yOffset,
-yOffset, Math.max(0, containerRect.height - overlayRect.height + yOffset)
Math.max(0, containerRect.height - overlayRect.height + yOffset) );
);
this._element.style.bottom = `${bottom}px`;
this._element.style.top = 'auto';
}
if (this.horiziontalAlignment === 'left') { this._element.style.left = `${left}px`;
const left = clamp( this._element.style.top = `${top}px`;
overlayRect.left - containerRect.left,
-xOffset,
Math.max(0, containerRect.width - overlayRect.width + xOffset)
);
this._element.style.left = `${left}px`;
this._element.style.right = 'auto';
}
if (this.horiziontalAlignment === 'right') {
const right = clamp(
containerRect.right - overlayRect.right,
-xOffset,
Math.max(0, containerRect.width - overlayRect.width + xOffset)
);
this._element.style.right = `${right}px`;
this._element.style.left = 'auto';
}
this._onDidChange.fire(); this._onDidChange.fire();
} }
toJSON(): AnchoredBox { toJSON(): Box {
const container = this.options.container.getBoundingClientRect(); const container = this.options.container.getBoundingClientRect();
const element = this._element.getBoundingClientRect(); const element = this._element.getBoundingClientRect();
const result: any = {}; return {
top: element.top - container.top,
if (this.verticalAlignment === 'top') { left: element.left - container.left,
result.top = parseFloat(this._element.style.top); width: element.width,
} else if (this.verticalAlignment === 'bottom') { height: element.height,
result.bottom = parseFloat(this._element.style.bottom); };
} else {
result.top = element.top - container.top;
}
if (this.horiziontalAlignment === 'left') {
result.left = parseFloat(this._element.style.left);
} else if (this.horiziontalAlignment === 'right') {
result.right = parseFloat(this._element.style.right);
} else {
result.left = element.left - container.left;
}
result.width = element.width;
result.height = element.height;
return result;
} }
setupDrag( setupDrag(
@ -249,15 +150,24 @@ export class Overlay extends CompositeDisposable {
const track = () => { const track = () => {
let offset: { x: number; y: number } | null = null; let offset: { x: number; y: number } | null = null;
const iframes = disableIframePointEvents(); const iframes = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
for (const iframe of iframes) {
iframe.style.pointerEvents = 'none';
}
move.value = new CompositeDisposable( move.value = new CompositeDisposable(
{ {
dispose: () => { dispose: () => {
iframes.release(); for (const iframe of iframes) {
iframe.style.pointerEvents = 'auto';
}
}, },
}, },
addDisposableListener(window, 'pointermove', (e) => { addDisposableWindowListener(window, 'mousemove', (e) => {
const containerRect = const containerRect =
this.options.container.getBoundingClientRect(); this.options.container.getBoundingClientRect();
const x = e.clientX - containerRect.left; const x = e.clientX - containerRect.left;
@ -283,28 +193,9 @@ export class Overlay extends CompositeDisposable {
); );
const yOffset = Math.max( const yOffset = Math.max(
0, 0,
this.getMinimumHeight(overlayRect.height) this.options.minimumInViewportHeight
); ? this.getMinimumHeight(overlayRect.height)
: 0
const top = clamp(
y - offset.y,
-yOffset,
Math.max(
0,
containerRect.height - overlayRect.height + yOffset
)
);
const bottom = clamp(
offset.y -
y +
containerRect.height -
overlayRect.height,
-yOffset,
Math.max(
0,
containerRect.height - overlayRect.height + yOffset
)
); );
const left = clamp( const left = clamp(
@ -316,34 +207,18 @@ export class Overlay extends CompositeDisposable {
) )
); );
const right = clamp( const top = clamp(
offset.x - x + containerRect.width - overlayRect.width, y - offset.y,
-xOffset, -yOffset,
Math.max( Math.max(
0, 0,
containerRect.width - overlayRect.width + xOffset containerRect.height - overlayRect.height + yOffset
) )
); );
const bounds: any = {}; this.setBounds({ top, left });
// Anchor to top or to bottom depending on which one is closer
if (top <= bottom) {
bounds.top = top;
} else {
bounds.bottom = bottom;
}
// Anchor to left or to right depending on which one is closer
if (left <= right) {
bounds.left = left;
} else {
bounds.right = right;
}
this.setBounds(bounds);
}), }),
addDisposableListener(window, 'pointerup', () => { addDisposableWindowListener(window, 'mouseup', () => {
toggleClass( toggleClass(
this._element, this._element,
'dv-resize-container-dragging', 'dv-resize-container-dragging',
@ -358,7 +233,7 @@ export class Overlay extends CompositeDisposable {
this.addDisposables( this.addDisposables(
move, move,
addDisposableListener(dragTarget, 'pointerdown', (event) => { addDisposableListener(dragTarget, 'mousedown', (event) => {
if (event.defaultPrevented) { if (event.defaultPrevented) {
event.preventDefault(); event.preventDefault();
return; return;
@ -374,7 +249,7 @@ export class Overlay extends CompositeDisposable {
}), }),
addDisposableListener( addDisposableListener(
this.options.content, this.options.content,
'pointerdown', 'mousedown',
(event) => { (event) => {
if (event.defaultPrevented) { if (event.defaultPrevented) {
return; return;
@ -393,14 +268,16 @@ export class Overlay extends CompositeDisposable {
), ),
addDisposableListener( addDisposableListener(
this.options.content, this.options.content,
'pointerdown', 'mousedown',
() => { () => {
arialLevelTracker.push(this._element); bringElementToFront(this._element);
}, },
true true
) )
); );
bringElementToFront(this._element);
if (options.inDragMode) { if (options.inDragMode) {
track(); track();
} }
@ -425,7 +302,7 @@ export class Overlay extends CompositeDisposable {
this.addDisposables( this.addDisposables(
move, move,
addDisposableListener(resizeHandleElement, 'pointerdown', (e) => { addDisposableListener(resizeHandleElement, 'mousedown', (e) => {
e.preventDefault(); e.preventDefault();
let startPosition: { let startPosition: {
@ -435,10 +312,17 @@ export class Overlay extends CompositeDisposable {
originalWidth: number; originalWidth: number;
} | null = null; } | null = null;
const iframes = disableIframePointEvents(); const iframes = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
for (const iframe of iframes) {
iframe.style.pointerEvents = 'none';
}
move.value = new CompositeDisposable( move.value = new CompositeDisposable(
addDisposableListener(window, 'pointermove', (e) => { addDisposableWindowListener(window, 'mousemove', (e) => {
const containerRect = const containerRect =
this.options.container.getBoundingClientRect(); this.options.container.getBoundingClientRect();
const overlayRect = const overlayRect =
@ -458,10 +342,8 @@ export class Overlay extends CompositeDisposable {
} }
let top: number | undefined = undefined; let top: number | undefined = undefined;
let bottom: number | undefined = undefined;
let height: number | undefined = undefined; let height: number | undefined = undefined;
let left: number | undefined = undefined; let left: number | undefined = undefined;
let right: number | undefined = undefined;
let width: number | undefined = undefined; let width: number | undefined = undefined;
const moveTop = () => { const moveTop = () => {
@ -481,13 +363,10 @@ export class Overlay extends CompositeDisposable {
Overlay.MINIMUM_HEIGHT Overlay.MINIMUM_HEIGHT
) )
); );
height = height =
startPosition!.originalY + startPosition!.originalY +
startPosition!.originalHeight - startPosition!.originalHeight -
top; top;
bottom = containerRect.height - top - height;
}; };
const moveBottom = () => { const moveBottom = () => {
@ -505,8 +384,6 @@ export class Overlay extends CompositeDisposable {
: Overlay.MINIMUM_HEIGHT, : Overlay.MINIMUM_HEIGHT,
Number.MAX_VALUE Number.MAX_VALUE
); );
bottom = containerRect.height - top - height;
}; };
const moveLeft = () => { const moveLeft = () => {
@ -529,8 +406,6 @@ export class Overlay extends CompositeDisposable {
startPosition!.originalX + startPosition!.originalX +
startPosition!.originalWidth - startPosition!.originalWidth -
left; left;
right = containerRect.width - left - width;
}; };
const moveRight = () => { const moveRight = () => {
@ -548,8 +423,6 @@ export class Overlay extends CompositeDisposable {
: Overlay.MINIMUM_WIDTH, : Overlay.MINIMUM_WIDTH,
Number.MAX_VALUE Number.MAX_VALUE
); );
right = containerRect.width - left - width;
}; };
switch (direction) { switch (direction) {
@ -583,33 +456,16 @@ export class Overlay extends CompositeDisposable {
break; break;
} }
const bounds: any = {}; this.setBounds({ height, width, top, left });
// Anchor to top or to bottom depending on which one is closer
if (top! <= bottom!) {
bounds.top = top;
} else {
bounds.bottom = bottom;
}
// Anchor to left or to right depending on which one is closer
if (left! <= right!) {
bounds.left = left;
} else {
bounds.right = right;
}
bounds.height = height;
bounds.width = width;
this.setBounds(bounds);
}), }),
{ {
dispose: () => { dispose: () => {
iframes.release(); for (const iframe of iframes) {
iframe.style.pointerEvents = 'auto';
}
}, },
}, },
addDisposableListener(window, 'pointerup', () => { addDisposableWindowListener(window, 'mouseup', () => {
move.dispose(); move.dispose();
this._onDidChangeEnd.fire(); this._onDidChangeEnd.fire();
}) })
@ -629,11 +485,10 @@ export class Overlay extends CompositeDisposable {
if (typeof this.options.minimumInViewportHeight === 'number') { if (typeof this.options.minimumInViewportHeight === 'number') {
return height - this.options.minimumInViewportHeight; return height - this.options.minimumInViewportHeight;
} }
return 0; return height;
} }
override dispose(): void { override dispose(): void {
arialLevelTracker.destroy(this._element);
this._element.remove(); this._element.remove();
super.dispose(); super.dispose();
} }

View File

@ -28,9 +28,9 @@ export class ContentContainer
extends CompositeDisposable extends CompositeDisposable
implements IContentContainer implements IContentContainer
{ {
private readonly _element: HTMLElement; private _element: HTMLElement;
private panel: IDockviewPanel | undefined; private panel: IDockviewPanel | undefined;
private readonly disposable = new MutableDisposable(); private disposable = new MutableDisposable();
private readonly _onDidFocus = new Emitter<void>(); private readonly _onDidFocus = new Emitter<void>();
readonly onDidFocus: Event<void> = this._onDidFocus.event; readonly onDidFocus: Event<void> = this._onDidFocus.event;
@ -50,20 +50,12 @@ export class ContentContainer
) { ) {
super(); super();
this._element = document.createElement('div'); this._element = document.createElement('div');
this._element.className = 'dv-content-container'; this._element.className = 'content-container';
this._element.tabIndex = -1; this._element.tabIndex = -1;
this.addDisposables(this._onDidFocus, this._onDidBlur); this.addDisposables(this._onDidFocus, this._onDidBlur);
const target = group.dropTargetContainer;
this.dropTarget = new Droptarget(this.element, { this.dropTarget = new Droptarget(this.element, {
getOverlayOutline: () => {
return accessor.options.theme?.dndPanelOverlay === 'group'
? this.element.parentElement
: null;
},
className: 'dv-drop-target-content',
acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'],
canDisplayOverlay: (event, position) => { canDisplayOverlay: (event, position) => {
if ( if (
@ -84,12 +76,26 @@ export class ContentContainer
} }
if (data && data.viewId === this.accessor.id) { if (data && data.viewId === this.accessor.id) {
return true; if (data.groupId === this.group.id) {
if (position === 'center') {
// don't allow to drop on self for center position
return false;
}
if (data.panelId === null) {
// don't allow group move to drop anywhere on self
return false;
}
}
const groupHasOnePanelAndIsActiveDragElement =
this.group.panels.length === 1 &&
data.groupId === this.group.id;
return !groupHasOnePanelAndIsActiveDragElement;
} }
return this.group.canDisplayOverlay(event, position, 'content'); return this.group.canDisplayOverlay(event, position, 'content');
}, },
getOverrideTarget: target ? () => target.model : undefined,
}); });
this.addDisposables(this.dropTarget); this.addDisposables(this.dropTarget);
@ -148,10 +154,6 @@ export class ContentContainer
referenceContainer: this, referenceContainer: this,
}); });
break; break;
default:
throw new Error(
`dockview: invalid renderer type '${panel.api.renderer}'`
);
} }
if (doRender) { if (doRender) {

View File

@ -1,87 +0,0 @@
import { shiftAbsoluteElementIntoView } from '../../dom';
import { addDisposableListener } from '../../events';
import {
CompositeDisposable,
Disposable,
MutableDisposable,
} from '../../lifecycle';
export class PopupService extends CompositeDisposable {
private readonly _element: HTMLElement;
private _active: HTMLElement | null = null;
private readonly _activeDisposable = new MutableDisposable();
constructor(private readonly root: HTMLElement) {
super();
this._element = document.createElement('div');
this._element.className = 'dv-popover-anchor';
this._element.style.position = 'relative';
this.root.prepend(this._element);
this.addDisposables(
Disposable.from(() => {
this.close();
}),
this._activeDisposable
);
}
openPopover(
element: HTMLElement,
position: { x: number; y: number; zIndex?: string }
): void {
this.close();
const wrapper = document.createElement('div');
wrapper.style.position = 'absolute';
wrapper.style.zIndex = position.zIndex ?? 'var(--dv-overlay-z-index)';
wrapper.appendChild(element);
const anchorBox = this._element.getBoundingClientRect();
const offsetX = anchorBox.left;
const offsetY = anchorBox.top;
wrapper.style.top = `${position.y - offsetY}px`;
wrapper.style.left = `${position.x - offsetX}px`;
this._element.appendChild(wrapper);
this._active = wrapper;
this._activeDisposable.value = new CompositeDisposable(
addDisposableListener(window, 'pointerdown', (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
let el: HTMLElement | null = target;
while (el && el !== wrapper) {
el = el?.parentElement ?? null;
}
if (el) {
return; // clicked within popover
}
this.close();
})
);
requestAnimationFrame(() => {
shiftAbsoluteElementIntoView(wrapper, this.root);
});
}
close(): void {
if (this._active) {
this._active.remove();
this._activeDisposable.dispose();
this._active = null;
}
}
}

View File

@ -6,7 +6,7 @@
); /* forces tab to be drawn on a separate layer (see https://github.com/microsoft/vscode/issues/18733) */ ); /* forces tab to be drawn on a separate layer (see https://github.com/microsoft/vscode/issues/18733) */
} }
.dv-tab { .tab {
flex-shrink: 0; flex-shrink: 0;
&:focus-within, &:focus-within,
@ -33,7 +33,7 @@
} }
} }
&.dv-active-tab { &.active-tab {
.dv-default-tab { .dv-default-tab {
.dv-default-tab-action { .dv-default-tab-action {
visibility: visible; visibility: visible;
@ -41,7 +41,7 @@
} }
} }
&.dv-inactive-tab { &.inactive-tab {
.dv-default-tab { .dv-default-tab {
.dv-default-tab-action { .dv-default-tab-action {
visibility: hidden; visibility: hidden;
@ -58,13 +58,15 @@
position: relative; position: relative;
height: 100%; height: 100%;
display: flex; display: flex;
min-width: 80px;
align-items: center; align-items: center;
padding: 0px 8px;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: elipsis;
.dv-default-tab-content { .dv-default-tab-content {
padding: 0px 8px;
flex-grow: 1; flex-grow: 1;
margin-right: 4px;
} }
.dv-default-tab-action { .dv-default-tab-action {

View File

@ -1,13 +1,16 @@
import { CompositeDisposable } from '../../../lifecycle'; import { CompositeDisposable } from '../../../lifecycle';
import { ITabRenderer, GroupPanelPartInitParameters } from '../../types'; import { ITabRenderer, GroupPanelPartInitParameters } from '../../types';
import { addDisposableListener } from '../../../events'; import { addDisposableListener } from '../../../events';
import { PanelUpdateEvent } from '../../../panel/types';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { createCloseButton } from '../../../svg'; import { createCloseButton } from '../../../svg';
export class DefaultTab extends CompositeDisposable implements ITabRenderer { export class DefaultTab extends CompositeDisposable implements ITabRenderer {
private readonly _element: HTMLElement; private _element: HTMLElement;
private readonly _content: HTMLElement; private _content: HTMLElement;
private readonly action: HTMLElement; private action: HTMLElement;
private _title: string | undefined; //
private params: GroupPanelPartInitParameters = {} as any;
get element(): HTMLElement { get element(): HTMLElement {
return this._element; return this._element;
@ -18,7 +21,7 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer {
this._element = document.createElement('div'); this._element = document.createElement('div');
this._element.className = 'dv-default-tab'; this._element.className = 'dv-default-tab';
//
this._content = document.createElement('div'); this._content = document.createElement('div');
this._content.className = 'dv-default-tab-content'; this._content.className = 'dv-default-tab-content';
@ -26,39 +29,53 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer {
this.action.className = 'dv-default-tab-action'; this.action.className = 'dv-default-tab-action';
this.action.appendChild(createCloseButton()); this.action.appendChild(createCloseButton());
//
this._element.appendChild(this._content); this._element.appendChild(this._content);
this._element.appendChild(this.action); this._element.appendChild(this.action);
//
this.render();
}
init(params: GroupPanelPartInitParameters): void {
this._title = params.title;
this.addDisposables( this.addDisposables(
params.api.onDidTitleChange((event) => { addDisposableListener(this.action, 'mousedown', (ev) => {
this._title = event.title;
this.render();
}),
addDisposableListener(this.action, 'pointerdown', (ev) => {
ev.preventDefault(); ev.preventDefault();
}),
addDisposableListener(this.action, 'click', (ev) => {
if (ev.defaultPrevented) {
return;
}
ev.preventDefault();
params.api.close();
}) })
); );
this.render(); this.render();
} }
public update(event: PanelUpdateEvent): void {
this.params = { ...this.params, ...event.params };
this.render();
}
focus(): void {
//noop
}
public init(params: GroupPanelPartInitParameters): void {
this.params = params;
this._content.textContent = params.title;
addDisposableListener(this.action, 'click', (ev) => {
ev.preventDefault(); //
this.params.api.close();
});
}
onGroupChange(_group: DockviewGroupPanel): void {
this.render();
}
onPanelVisibleChange(_isPanelVisible: boolean): void {
this.render();
}
public layout(_width: number, _height: number): void {
// noop
}
private render(): void { private render(): void {
if (this._content.textContent !== this._title) { if (this._content.textContent !== this.params.title) {
this._content.textContent = this._title ?? ''; this._content.textContent = this.params.title;
} }
} }
} }

View File

@ -12,11 +12,11 @@ import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { import {
DroptargetEvent, DroptargetEvent,
Droptarget, Droptarget,
Position,
WillShowOverlayEvent, WillShowOverlayEvent,
} from '../../../dnd/droptarget'; } from '../../../dnd/droptarget';
import { DragHandler } from '../../../dnd/abstractDragHandler'; import { DragHandler } from '../../../dnd/abstractDragHandler';
import { IDockviewPanel } from '../../dockviewPanel'; import { IDockviewPanel } from '../../dockviewPanel';
import { addGhostImage } from '../../../dnd/ghost';
class TabDragHandler extends DragHandler { class TabDragHandler extends DragHandler {
private readonly panelTransfer = private readonly panelTransfer =
@ -50,8 +50,8 @@ export class Tab extends CompositeDisposable {
private readonly dropTarget: Droptarget; private readonly dropTarget: Droptarget;
private content: ITabRenderer | undefined = undefined; private content: ITabRenderer | undefined = undefined;
private readonly _onPointDown = new Emitter<MouseEvent>(); private readonly _onChanged = new Emitter<MouseEvent>();
readonly onPointerDown: Event<MouseEvent> = this._onPointDown.event; readonly onChanged: Event<MouseEvent> = this._onChanged.event;
private readonly _onDropped = new Emitter<DroptargetEvent>(); private readonly _onDropped = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDropped.event; readonly onDrop: Event<DroptargetEvent> = this._onDropped.event;
@ -73,11 +73,11 @@ export class Tab extends CompositeDisposable {
super(); super();
this._element = document.createElement('div'); this._element = document.createElement('div');
this._element.className = 'dv-tab'; this._element.className = 'tab';
this._element.tabIndex = 0; this._element.tabIndex = 0;
this._element.draggable = true; this._element.draggable = true;
toggleClass(this.element, 'dv-inactive-tab', true); toggleClass(this.element, 'inactive-tab', true);
const dragHandler = new TabDragHandler( const dragHandler = new TabDragHandler(
this._element, this._element,
@ -87,8 +87,7 @@ export class Tab extends CompositeDisposable {
); );
this.dropTarget = new Droptarget(this._element, { this.dropTarget = new Droptarget(this._element, {
acceptedTargetZones: ['left', 'right'], acceptedTargetZones: ['center'],
overlayModel: { activationSize: { value: 50, type: 'percentage' } },
canDisplayOverlay: (event, position) => { canDisplayOverlay: (event, position) => {
if (this.group.locked) { if (this.group.locked) {
return false; return false;
@ -97,7 +96,15 @@ export class Tab extends CompositeDisposable {
const data = getPanelData(); const data = getPanelData();
if (data && this.accessor.id === data.viewId) { if (data && this.accessor.id === data.viewId) {
return true; if (
data.panelId === null &&
data.groupId === this.group.id
) {
// don't allow group move to drop on self
return false;
}
return this.panel.id !== data.panelId;
} }
return this.group.model.canDisplayOverlay( return this.group.model.canDisplayOverlay(
@ -106,38 +113,24 @@ export class Tab extends CompositeDisposable {
'tab' 'tab'
); );
}, },
getOverrideTarget: () => group.model.dropTargetContainer?.model,
}); });
this.onWillShowOverlay = this.dropTarget.onWillShowOverlay; this.onWillShowOverlay = this.dropTarget.onWillShowOverlay;
this.addDisposables( this.addDisposables(
this._onPointDown, this._onChanged,
this._onDropped, this._onDropped,
this._onDragStart, this._onDragStart,
dragHandler.onDragStart((event) => { dragHandler.onDragStart((event) => {
if (event.dataTransfer) {
const style = getComputedStyle(this.element);
const newNode = this.element.cloneNode(true) as HTMLElement;
Array.from(style).forEach((key) =>
newNode.style.setProperty(
key,
style.getPropertyValue(key),
style.getPropertyPriority(key)
)
);
newNode.style.position = 'absolute';
addGhostImage(event.dataTransfer, newNode, {
y: -10,
x: 30,
});
}
this._onDragStart.fire(event); this._onDragStart.fire(event);
}), }),
dragHandler, dragHandler,
addDisposableListener(this._element, 'pointerdown', (event) => { addDisposableListener(this._element, 'mousedown', (event) => {
this._onPointDown.fire(event); if (event.defaultPrevented) {
return;
}
this._onChanged.fire(event);
}), }),
this.dropTarget.onDrop((event) => { this.dropTarget.onDrop((event) => {
this._onDropped.fire(event); this._onDropped.fire(event);
@ -147,8 +140,8 @@ export class Tab extends CompositeDisposable {
} }
public setActive(isActive: boolean): void { public setActive(isActive: boolean): void {
toggleClass(this.element, 'dv-active-tab', isActive); toggleClass(this.element, 'active-tab', isActive);
toggleClass(this.element, 'dv-inactive-tab', !isActive); toggleClass(this.element, 'inactive-tab', !isActive);
} }
public setContent(part: ITabRenderer): void { public setContent(part: ITabRenderer): void {

View File

@ -1,19 +0,0 @@
.dv-tabs-overflow-dropdown-default {
height: 100%;
color: var(--dv-activegroup-hiddenpanel-tab-color);
margin: var(--dv-tab-margin);
display: flex;
align-items: center;
flex-shrink: 0;
padding: 0.25rem 0.5rem;
cursor: pointer;
> span {
padding-left: 0.25rem;
}
> svg {
transform: rotate(90deg);
}
}

View File

@ -1,25 +0,0 @@
import { createChevronRightButton } from '../../../svg';
export type DropdownElement = {
element: HTMLElement;
update: (params: { tabs: number }) => void;
dispose?: () => void;
};
export function createDropdownElementHandle(): DropdownElement {
const el = document.createElement('div');
el.className = 'dv-tabs-overflow-dropdown-default';
const text = document.createElement('span');
text.textContent = ``;
const icon = createChevronRightButton();
el.appendChild(icon);
el.appendChild(text);
return {
element: el,
update: (params: { tabs: number }) => {
text.textContent = `${params.tabs}`;
},
};
}

View File

@ -1,79 +0,0 @@
.dv-tabs-container {
display: flex;
height: 100%;
overflow: auto;
scrollbar-width: thin; // firefox
&.dv-horizontal {
.dv-tab {
&:not(:first-child)::before {
content: ' ';
position: absolute;
top: 0;
left: 0;
z-index: 5;
pointer-events: none;
background-color: var(--dv-tab-divider-color);
width: 1px;
height: 100%;
}
}
}
&::-webkit-scrollbar {
height: 3px;
}
/* Track */
&::-webkit-scrollbar-track {
background: transparent;
}
/* Handle */
&::-webkit-scrollbar-thumb {
background: var(--dv-tabs-container-scrollbar-color);
}
}
.dv-scrollable {
> .dv-tabs-container {
overflow: hidden;
}
}
.dv-tab {
-webkit-user-drag: element;
outline: none;
padding: 0.25rem 0.5rem;
cursor: pointer;
position: relative;
box-sizing: border-box;
font-size: var(--dv-tab-font-size);
margin: var(--dv-tab-margin);
}
.dv-tabs-overflow-container {
flex-direction: column;
height: unset;
border: 1px solid var(--dv-tab-divider-color);
background-color: var(--dv-group-view-background-color);
.dv-tab {
&:not(:last-child) {
border-bottom: 1px solid var(--dv-tab-divider-color);
}
}
.dv-active-tab {
background-color: var(
--dv-activegroup-visiblepanel-tab-background-color
);
color: var(--dv-activegroup-visiblepanel-tab-color);
}
.dv-inactive-tab {
background-color: var(
--dv-activegroup-hiddenpanel-tab-background-color
);
color: var(--dv-activegroup-hiddenpanel-tab-color);
}
}

View File

@ -1,301 +0,0 @@
import { getPanelData } from '../../../dnd/dataTransfer';
import {
isChildEntirelyVisibleWithinParent,
OverflowObserver,
} from '../../../dom';
import { addDisposableListener, Emitter, Event } from '../../../events';
import {
CompositeDisposable,
Disposable,
IValueDisposable,
MutableDisposable,
} from '../../../lifecycle';
import { Scrollbar } from '../../../scrollbar';
import { DockviewComponent } from '../../dockviewComponent';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel';
import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel';
import { Tab } from '../tab/tab';
import { TabDragEvent, TabDropIndexEvent } from './tabsContainer';
export class Tabs extends CompositeDisposable {
private readonly _element: HTMLElement;
private readonly _tabsList: HTMLElement;
private readonly _observerDisposable = new MutableDisposable();
private _tabs: IValueDisposable<Tab>[] = [];
private selectedIndex = -1;
private _showTabsOverflowControl = false;
private readonly _onTabDragStart = new Emitter<TabDragEvent>();
readonly onTabDragStart: Event<TabDragEvent> = this._onTabDragStart.event;
private readonly _onDrop = new Emitter<TabDropIndexEvent>();
readonly onDrop: Event<TabDropIndexEvent> = this._onDrop.event;
private readonly _onWillShowOverlay =
new Emitter<WillShowOverlayLocationEvent>();
readonly onWillShowOverlay: Event<WillShowOverlayLocationEvent> =
this._onWillShowOverlay.event;
private readonly _onOverflowTabsChange = new Emitter<{
tabs: string[];
reset: boolean;
}>();
readonly onOverflowTabsChange = this._onOverflowTabsChange.event;
get showTabsOverflowControl(): boolean {
return this._showTabsOverflowControl;
}
set showTabsOverflowControl(value: boolean) {
if (this._showTabsOverflowControl == value) {
return;
}
this._showTabsOverflowControl = value;
if (value) {
const observer = new OverflowObserver(this._tabsList);
this._observerDisposable.value = new CompositeDisposable(
observer,
observer.onDidChange((event) => {
const hasOverflow = event.hasScrollX || event.hasScrollY;
this.toggleDropdown({ reset: !hasOverflow });
}),
addDisposableListener(this._tabsList, 'scroll', () => {
this.toggleDropdown({ reset: false });
})
);
}
}
get element(): HTMLElement {
return this._element;
}
get panels(): string[] {
return this._tabs.map((_) => _.value.panel.id);
}
get size(): number {
return this._tabs.length;
}
get tabs(): Tab[] {
return this._tabs.map((_) => _.value);
}
constructor(
private readonly group: DockviewGroupPanel,
private readonly accessor: DockviewComponent,
options: {
showTabsOverflowControl: boolean;
}
) {
super();
this._tabsList = document.createElement('div');
this._tabsList.className = 'dv-tabs-container dv-horizontal';
this.showTabsOverflowControl = options.showTabsOverflowControl;
if (accessor.options.scrollbars === 'native') {
this._element = this._tabsList;
} else {
const scrollbar = new Scrollbar(this._tabsList);
this._element = scrollbar.element;
this.addDisposables(scrollbar);
}
this.addDisposables(
this._onOverflowTabsChange,
this._observerDisposable,
this._onWillShowOverlay,
this._onDrop,
this._onTabDragStart,
addDisposableListener(this.element, 'pointerdown', (event) => {
if (event.defaultPrevented) {
return;
}
const isLeftClick = event.button === 0;
if (isLeftClick) {
this.accessor.doSetGroupActive(this.group);
}
}),
Disposable.from(() => {
for (const { value, disposable } of this._tabs) {
disposable.dispose();
value.dispose();
}
this._tabs = [];
})
);
}
indexOf(id: string): number {
return this._tabs.findIndex((tab) => tab.value.panel.id === id);
}
isActive(tab: Tab): boolean {
return (
this.selectedIndex > -1 &&
this._tabs[this.selectedIndex].value === tab
);
}
setActivePanel(panel: IDockviewPanel): void {
let runningWidth = 0;
for (const tab of this._tabs) {
const isActivePanel = panel.id === tab.value.panel.id;
tab.value.setActive(isActivePanel);
if (isActivePanel) {
const element = tab.value.element;
const parentElement = element.parentElement!;
if (
runningWidth < parentElement.scrollLeft ||
runningWidth + element.clientWidth >
parentElement.scrollLeft + parentElement.clientWidth
) {
parentElement.scrollLeft = runningWidth;
}
}
runningWidth += tab.value.element.clientWidth;
}
}
openPanel(panel: IDockviewPanel, index: number = this._tabs.length): void {
if (this._tabs.find((tab) => tab.value.panel.id === panel.id)) {
return;
}
const tab = new Tab(panel, this.accessor, this.group);
tab.setContent(panel.view.tab);
const disposable = new CompositeDisposable(
tab.onDragStart((event) => {
this._onTabDragStart.fire({ nativeEvent: event, panel });
}),
tab.onPointerDown((event) => {
if (event.defaultPrevented) {
return;
}
const isFloatingGroupsEnabled =
!this.accessor.options.disableFloatingGroups;
const isFloatingWithOnePanel =
this.group.api.location.type === 'floating' &&
this.size === 1;
if (
isFloatingGroupsEnabled &&
!isFloatingWithOnePanel &&
event.shiftKey
) {
event.preventDefault();
const panel = this.accessor.getGroupPanel(tab.panel.id);
const { top, left } = tab.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;
}
switch (event.button) {
case 0: // left click or touch
if (this.group.activePanel !== panel) {
this.group.model.openPanel(panel);
}
break;
}
}),
tab.onDrop((event) => {
this._onDrop.fire({
event: event.nativeEvent,
index: this._tabs.findIndex((x) => x.value === tab),
});
}),
tab.onWillShowOverlay((event) => {
this._onWillShowOverlay.fire(
new WillShowOverlayLocationEvent(event, {
kind: 'tab',
panel: this.group.activePanel,
api: this.accessor.api,
group: this.group,
getData: getPanelData,
})
);
})
);
const value: IValueDisposable<Tab> = { value: tab, disposable };
this.addTab(value, index);
}
delete(id: string): void {
const index = this.indexOf(id);
const tabToRemove = this._tabs.splice(index, 1)[0];
const { value, disposable } = tabToRemove;
disposable.dispose();
value.dispose();
value.element.remove();
}
private addTab(
tab: IValueDisposable<Tab>,
index: number = this._tabs.length
): void {
if (index < 0 || index > this._tabs.length) {
throw new Error('invalid location');
}
this._tabsList.insertBefore(
tab.value.element,
this._tabsList.children[index]
);
this._tabs = [
...this._tabs.slice(0, index),
tab,
...this._tabs.slice(index),
];
if (this.selectedIndex < 0) {
this.selectedIndex = index;
}
}
private toggleDropdown(options: { reset: boolean }): void {
const tabs = options.reset
? []
: this._tabs
.filter(
(tab) =>
!isChildEntirelyVisibleWithinParent(
tab.value.element,
this._tabsList
)
)
.map((x) => x.value.panel.id);
this._onOverflowTabsChange.fire({ tabs, reset: options.reset });
}
}

View File

@ -1,4 +1,4 @@
.dv-tabs-and-actions-container { .tabs-and-actions-container {
display: flex; display: flex;
background-color: var(--dv-tabs-and-actions-container-background-color); background-color: var(--dv-tabs-and-actions-container-background-color);
flex-shrink: 0; flex-shrink: 0;
@ -6,32 +6,70 @@
height: var(--dv-tabs-and-actions-container-height); height: var(--dv-tabs-and-actions-container-height);
font-size: var(--dv-tabs-and-actions-container-font-size); font-size: var(--dv-tabs-and-actions-container-font-size);
&.dv-single-tab.dv-full-width-single-tab { &.hidden {
.dv-scrollable { display: none;
flex-grow: 1;
}
.dv-tabs-container {
flex-grow: 1;
.dv-tab {
flex-grow: 1;
padding: 0px;
}
}
.dv-void-container {
flex-grow: 0;
}
} }
.dv-void-container { &.dv-single-tab.dv-full-width-single-tab {
.tabs-container {
flex-grow: 1;
.tab {
flex-grow: 1;
}
}
.void-container {
flex-grow: 0;
}
}
.void-container {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
cursor: grab; cursor: grab;
} }
.dv-right-actions-container { .tabs-container {
display: flex; display: flex;
overflow-x: overlay;
overflow-y: hidden;
scrollbar-width: thin; // firefox
&::-webkit-scrollbar {
height: 3px;
}
/* Track */
&::-webkit-scrollbar-track {
background: transparent;
}
/* Handle */
&::-webkit-scrollbar-thumb {
background: var(--dv-tabs-container-scrollbar-color);
}
.tab {
-webkit-user-drag: element;
outline: none;
min-width: 75px;
cursor: pointer;
position: relative;
box-sizing: border-box;
&:not(:first-child)::before {
content: ' ';
position: absolute;
top: 0;
left: 0;
z-index: 5;
pointer-events: none;
background-color: var(--dv-tab-divider-color);
width: 1px;
height: 100%;
}
}
} }
} }

View File

@ -1,23 +1,21 @@
import { import {
IDisposable, IDisposable,
CompositeDisposable, CompositeDisposable,
Disposable, IValueDisposable,
MutableDisposable,
} from '../../../lifecycle'; } from '../../../lifecycle';
import { addDisposableListener, Emitter, Event } from '../../../events'; import { addDisposableListener, Emitter, Event } from '../../../events';
import { Tab } from '../tab/tab'; import { Tab } from '../tab/tab';
import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { VoidContainer } from './voidContainer'; import { VoidContainer } from './voidContainer';
import { findRelativeZIndexParent, toggleClass } from '../../../dom'; import { toggleClass } from '../../../dom';
import { IDockviewPanel } from '../../dockviewPanel'; import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel';
import { DockviewComponent } from '../../dockviewComponent'; import { DockviewComponent } from '../../dockviewComponent';
import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel'; import { WillShowOverlayEvent } from '../../../dnd/droptarget';
import { getPanelData } from '../../../dnd/dataTransfer';
import { Tabs } from './tabs';
import { import {
createDropdownElementHandle, DockviewGroupDropLocation,
DropdownElement, WillShowOverlayLocationEvent,
} from './tabOverflowControl'; } from '../../dockviewGroupPanelModel';
import { getPanelData } from '../../../dnd/dataTransfer';
export interface TabDropIndexEvent { export interface TabDropIndexEvent {
readonly event: DragEvent; readonly event: DragEvent;
@ -62,28 +60,25 @@ export class TabsContainer
implements ITabsContainer implements ITabsContainer
{ {
private readonly _element: HTMLElement; private readonly _element: HTMLElement;
private readonly tabs: Tabs; private readonly tabContainer: HTMLElement;
private readonly rightActionsContainer: HTMLElement; private readonly rightActionsContainer: HTMLElement;
private readonly leftActionsContainer: HTMLElement; private readonly leftActionsContainer: HTMLElement;
private readonly preActionsContainer: HTMLElement; private readonly preActionsContainer: HTMLElement;
private readonly voidContainer: VoidContainer; private readonly voidContainer: VoidContainer;
private tabs: IValueDisposable<Tab>[] = [];
private selectedIndex = -1;
private rightActions: HTMLElement | undefined; private rightActions: HTMLElement | undefined;
private leftActions: HTMLElement | undefined; private leftActions: HTMLElement | undefined;
private preActions: HTMLElement | undefined; private preActions: HTMLElement | undefined;
private _hidden = false; private _hidden = false;
private dropdownPart: DropdownElement | null = null;
private _overflowTabs: string[] = [];
private readonly _dropdownDisposable = new MutableDisposable();
private readonly _onDrop = new Emitter<TabDropIndexEvent>(); private readonly _onDrop = new Emitter<TabDropIndexEvent>();
readonly onDrop: Event<TabDropIndexEvent> = this._onDrop.event; readonly onDrop: Event<TabDropIndexEvent> = this._onDrop.event;
get onTabDragStart(): Event<TabDragEvent> { private readonly _onTabDragStart = new Emitter<TabDragEvent>();
return this.tabs.onTabDragStart; readonly onTabDragStart: Event<TabDragEvent> = this._onTabDragStart.event;
}
private readonly _onGroupDragStart = new Emitter<GroupDragEvent>(); private readonly _onGroupDragStart = new Emitter<GroupDragEvent>();
readonly onGroupDragStart: Event<GroupDragEvent> = readonly onGroupDragStart: Event<GroupDragEvent> =
@ -95,11 +90,11 @@ export class TabsContainer
this._onWillShowOverlay.event; this._onWillShowOverlay.event;
get panels(): string[] { get panels(): string[] {
return this.tabs.panels; return this.tabs.map((_) => _.value.panel.id);
} }
get size(): number { get size(): number {
return this.tabs.size; return this.tabs.length;
} }
get hidden(): boolean { get hidden(): boolean {
@ -111,118 +106,6 @@ export class TabsContainer
this.element.style.display = value ? 'none' : ''; this.element.style.display = value ? 'none' : '';
} }
get element(): HTMLElement {
return this._element;
}
constructor(
private readonly accessor: DockviewComponent,
private readonly group: DockviewGroupPanel
) {
super();
this._element = document.createElement('div');
this._element.className = 'dv-tabs-and-actions-container';
toggleClass(
this._element,
'dv-full-width-single-tab',
this.accessor.options.singleTabMode === 'fullwidth'
);
this.rightActionsContainer = document.createElement('div');
this.rightActionsContainer.className = 'dv-right-actions-container';
this.leftActionsContainer = document.createElement('div');
this.leftActionsContainer.className = 'dv-left-actions-container';
this.preActionsContainer = document.createElement('div');
this.preActionsContainer.className = 'dv-pre-actions-container';
this.tabs = new Tabs(group, accessor, {
showTabsOverflowControl: !accessor.options.disableTabsOverflowList,
});
this.voidContainer = new VoidContainer(this.accessor, this.group);
this._element.appendChild(this.preActionsContainer);
this._element.appendChild(this.tabs.element);
this._element.appendChild(this.leftActionsContainer);
this._element.appendChild(this.voidContainer.element);
this._element.appendChild(this.rightActionsContainer);
this.addDisposables(
this.tabs.onDrop((e) => this._onDrop.fire(e)),
this.tabs.onWillShowOverlay((e) => this._onWillShowOverlay.fire(e)),
accessor.onDidOptionsChange(() => {
this.tabs.showTabsOverflowControl =
!accessor.options.disableTabsOverflowList;
}),
this.tabs.onOverflowTabsChange((event) => {
this.toggleDropdown(event);
}),
this.tabs,
this._onWillShowOverlay,
this._onDrop,
this._onGroupDragStart,
this.voidContainer,
this.voidContainer.onDragStart((event) => {
this._onGroupDragStart.fire({
nativeEvent: event,
group: this.group,
});
}),
this.voidContainer.onDrop((event) => {
this._onDrop.fire({
event: event.nativeEvent,
index: this.tabs.size,
});
}),
this.voidContainer.onWillShowOverlay((event) => {
this._onWillShowOverlay.fire(
new WillShowOverlayLocationEvent(event, {
kind: 'header_space',
panel: this.group.activePanel,
api: this.accessor.api,
group: this.group,
getData: getPanelData,
})
);
}),
addDisposableListener(
this.voidContainer.element,
'pointerdown',
(event) => {
if (event.defaultPrevented) {
return;
}
const isFloatingGroupsEnabled =
!this.accessor.options.disableFloatingGroups;
if (
isFloatingGroupsEnabled &&
event.shiftKey &&
this.group.api.location.type !== 'floating'
) {
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,
});
}
}
)
);
}
show(): void { show(): void {
if (!this.hidden) { if (!this.hidden) {
this.element.style.display = ''; this.element.style.display = '';
@ -275,129 +158,289 @@ export class TabsContainer
} }
} }
isActive(tab: Tab): boolean { get element(): HTMLElement {
return this.tabs.isActive(tab); return this._element;
} }
indexOf(id: string): number { public isActive(tab: Tab): boolean {
return this.tabs.indexOf(id); return (
this.selectedIndex > -1 &&
this.tabs[this.selectedIndex].value === tab
);
} }
setActive(_isGroupActive: boolean) { public indexOf(id: string): number {
// noop return this.tabs.findIndex((tab) => tab.value.panel.id === id);
} }
delete(id: string): void { constructor(
this.tabs.delete(id); private readonly accessor: DockviewComponent,
this.updateClassnames(); private readonly group: DockviewGroupPanel
} ) {
super();
setActivePanel(panel: IDockviewPanel): void { this._element = document.createElement('div');
this.tabs.setActivePanel(panel); this._element.className = 'tabs-and-actions-container';
}
openPanel(panel: IDockviewPanel, index: number = this.tabs.size): void { toggleClass(
this.tabs.openPanel(panel, index); this._element,
this.updateClassnames(); 'dv-full-width-single-tab',
} this.accessor.options.singleTabMode === 'fullwidth'
);
closePanel(panel: IDockviewPanel): void { this.rightActionsContainer = document.createElement('div');
this.delete(panel.id); this.rightActionsContainer.className = 'right-actions-container';
}
private updateClassnames(): void { this.leftActionsContainer = document.createElement('div');
toggleClass(this._element, 'dv-single-tab', this.size === 1); this.leftActionsContainer.className = 'left-actions-container';
}
private toggleDropdown(options: { tabs: string[]; reset: boolean }): void { this.preActionsContainer = document.createElement('div');
const tabs = options.reset ? [] : options.tabs; this.preActionsContainer.className = 'pre-actions-container';
this._overflowTabs = tabs;
if (this._overflowTabs.length > 0 && this.dropdownPart) { this.tabContainer = document.createElement('div');
this.dropdownPart.update({ tabs: tabs.length }); this.tabContainer.className = 'tabs-container';
return;
}
if (this._overflowTabs.length === 0) { this.voidContainer = new VoidContainer(this.accessor, this.group);
this._dropdownDisposable.dispose();
return;
}
const root = document.createElement('div'); this._element.appendChild(this.preActionsContainer);
root.className = 'dv-tabs-overflow-dropdown-root'; this._element.appendChild(this.tabContainer);
this._element.appendChild(this.leftActionsContainer);
this._element.appendChild(this.voidContainer.element);
this._element.appendChild(this.rightActionsContainer);
const part = createDropdownElementHandle(); this.addDisposables(
part.update({ tabs: tabs.length }); this.accessor.onDidAddPanel((e) => {
if (e.api.group === this.group) {
this.dropdownPart = part; toggleClass(
this._element,
root.appendChild(part.element); 'dv-single-tab',
this.rightActionsContainer.prepend(root); this.size === 1
);
this._dropdownDisposable.value = new CompositeDisposable( }
Disposable.from(() => { }),
root.remove(); this.accessor.onDidRemovePanel((e) => {
this.dropdownPart?.dispose?.(); if (e.api.group === this.group) {
this.dropdownPart = null; toggleClass(
this._element,
'dv-single-tab',
this.size === 1
);
}
}),
this._onWillShowOverlay,
this._onDrop,
this._onTabDragStart,
this._onGroupDragStart,
this.voidContainer,
this.voidContainer.onDragStart((event) => {
this._onGroupDragStart.fire({
nativeEvent: event,
group: this.group,
});
}),
this.voidContainer.onDrop((event) => {
this._onDrop.fire({
event: event.nativeEvent,
index: this.tabs.length,
});
}),
this.voidContainer.onWillShowOverlay((event) => {
this._onWillShowOverlay.fire(
new WillShowOverlayLocationEvent(event, {
kind: 'header_space',
panel: this.group.activePanel,
api: this.accessor.api,
group: this.group,
getData: getPanelData,
})
);
}), }),
addDisposableListener( addDisposableListener(
root, this.voidContainer.element,
'pointerdown', 'mousedown',
(event) => { (event) => {
event.preventDefault(); const isFloatingGroupsEnabled =
}, !this.accessor.options.disableFloatingGroups;
{ capture: true }
if (
isFloatingGroupsEnabled &&
event.shiftKey &&
this.group.api.location.type !== 'floating'
) {
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(root, 'click', (event) => { addDisposableListener(this.tabContainer, 'mousedown', (event) => {
const el = document.createElement('div'); if (event.defaultPrevented) {
el.style.overflow = 'auto'; return;
el.className = 'dv-tabs-overflow-container';
for (const tab of this.tabs.tabs.filter((tab) =>
this._overflowTabs.includes(tab.panel.id)
)) {
const panelObject = this.group.panels.find(
(panel) => panel === tab.panel
)!;
const tabComponent =
panelObject.view.createTabRenderer('headerOverflow');
const child = tabComponent.element;
const wrapper = document.createElement('div');
toggleClass(wrapper, 'dv-tab', true);
toggleClass(
wrapper,
'dv-active-tab',
panelObject.api.isActive
);
toggleClass(
wrapper,
'dv-inactive-tab',
!panelObject.api.isActive
);
wrapper.addEventListener('pointerdown', () => {
this.accessor.popupService.close();
tab.element.scrollIntoView();
tab.panel.api.setActive();
});
wrapper.appendChild(child);
el.appendChild(wrapper);
} }
const relativeParent = findRelativeZIndexParent(root); const isLeftClick = event.button === 0;
this.accessor.popupService.openPopover(el, { if (isLeftClick) {
x: event.clientX, this.accessor.doSetGroupActive(this.group);
y: event.clientY, }
zIndex: relativeParent?.style.zIndex
? `calc(${relativeParent.style.zIndex} * 2)`
: undefined,
});
}) })
); );
} }
public setActive(_isGroupActive: boolean) {
// noop
}
private addTab(
tab: IValueDisposable<Tab>,
index: number = this.tabs.length
): void {
if (index < 0 || index > this.tabs.length) {
throw new Error('invalid location');
}
this.tabContainer.insertBefore(
tab.value.element,
this.tabContainer.children[index]
);
this.tabs = [
...this.tabs.slice(0, index),
tab,
...this.tabs.slice(index),
];
if (this.selectedIndex < 0) {
this.selectedIndex = index;
}
}
public delete(id: string): void {
const index = this.tabs.findIndex((tab) => tab.value.panel.id === id);
const tabToRemove = this.tabs.splice(index, 1)[0];
const { value, disposable } = tabToRemove;
disposable.dispose();
value.dispose();
value.element.remove();
}
public setActivePanel(panel: IDockviewPanel): void {
this.tabs.forEach((tab) => {
const isActivePanel = panel.id === tab.value.panel.id;
tab.value.setActive(isActivePanel);
});
}
public openPanel(
panel: IDockviewPanel,
index: number = this.tabs.length
): void {
if (this.tabs.find((tab) => tab.value.panel.id === panel.id)) {
return;
}
const tab = new Tab(panel, this.accessor, this.group);
if (!panel.view?.tab) {
throw new Error('invalid header component');
}
tab.setContent(panel.view.tab);
const disposable = new CompositeDisposable(
tab.onDragStart((event) => {
this._onTabDragStart.fire({ nativeEvent: event, panel });
}),
tab.onChanged((event) => {
const isFloatingGroupsEnabled =
!this.accessor.options.disableFloatingGroups;
const isFloatingWithOnePanel =
this.group.api.location.type === 'floating' &&
this.size === 1;
if (
isFloatingGroupsEnabled &&
!isFloatingWithOnePanel &&
event.shiftKey
) {
event.preventDefault();
const panel = this.accessor.getGroupPanel(tab.panel.id);
const { top, left } = tab.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 isLeftClick = event.button === 0;
if (!isLeftClick || event.defaultPrevented) {
return;
}
if (this.group.activePanel !== panel) {
this.group.model.openPanel(panel);
}
}),
tab.onDrop((event) => {
this._onDrop.fire({
event: event.nativeEvent,
index: this.tabs.findIndex((x) => x.value === tab),
});
}),
tab.onWillShowOverlay((event) => {
this._onWillShowOverlay.fire(
new WillShowOverlayLocationEvent(event, {
kind: 'tab',
panel: this.group.activePanel,
api: this.accessor.api,
group: this.group,
getData: getPanelData,
})
);
})
);
const value: IValueDisposable<Tab> = { value: tab, disposable };
this.addTab(value, index);
}
public closePanel(panel: IDockviewPanel): void {
this.delete(panel.id);
}
public dispose(): void {
super.dispose();
for (const { value, disposable } of this.tabs) {
disposable.dispose();
value.dispose();
}
this.tabs = [];
}
} }

View File

@ -1,3 +1,4 @@
import { last } from '../../../array';
import { getPanelData } from '../../../dnd/dataTransfer'; import { getPanelData } from '../../../dnd/dataTransfer';
import { import {
Droptarget, Droptarget,
@ -9,7 +10,6 @@ import { DockviewComponent } from '../../dockviewComponent';
import { addDisposableListener, Emitter, Event } from '../../../events'; import { addDisposableListener, Emitter, Event } from '../../../events';
import { CompositeDisposable } from '../../../lifecycle'; import { CompositeDisposable } from '../../../lifecycle';
import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { DockviewGroupPanelModel } from '../../dockviewGroupPanelModel';
export class VoidContainer extends CompositeDisposable { export class VoidContainer extends CompositeDisposable {
private readonly _element: HTMLElement; private readonly _element: HTMLElement;
@ -35,13 +35,14 @@ export class VoidContainer extends CompositeDisposable {
this._element = document.createElement('div'); this._element = document.createElement('div');
this._element.className = 'dv-void-container'; this._element.className = 'void-container';
this._element.tabIndex = 0;
this._element.draggable = true; this._element.draggable = true;
this.addDisposables( this.addDisposables(
this._onDrop, this._onDrop,
this._onDragStart, this._onDragStart,
addDisposableListener(this._element, 'pointerdown', () => { addDisposableListener(this._element, 'click', () => {
this.accessor.doSetGroupActive(this.group); this.accessor.doSetGroupActive(this.group);
}) })
); );
@ -54,7 +55,16 @@ export class VoidContainer extends CompositeDisposable {
const data = getPanelData(); const data = getPanelData();
if (data && this.accessor.id === data.viewId) { if (data && this.accessor.id === data.viewId) {
return true; if (
data.panelId === null &&
data.groupId === this.group.id
) {
// don't allow group move to drop on self
return false;
}
// don't show the overlay if the tab being dragged is the last panel of this group
return last(this.group.panels)?.id !== data.panelId;
} }
return group.model.canDisplayOverlay( return group.model.canDisplayOverlay(
@ -63,7 +73,6 @@ export class VoidContainer extends CompositeDisposable {
'header_space' 'header_space'
); );
}, },
getOverrideTarget: () => group.model.dropTargetContainer?.model,
}); });
this.onWillShowOverlay = this.dropTraget.onWillShowOverlay; this.onWillShowOverlay = this.dropTraget.onWillShowOverlay;

View File

@ -1,4 +1,42 @@
.dv-watermark { .watermark {
display: flex; display: flex;
height: 100%; width: 100%;
&.has-actions {
.watermark-title {
.actions-container {
display: none;
}
}
}
.watermark-title {
height: 35px;
width: 100%;
display: flex;
}
.watermark-content {
flex-grow: 1;
}
.actions-container {
display: flex;
align-items: center;
padding: 0px 8px;
.close-action {
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
cursor: pointer;
color: var(--dv-activegroup-hiddenpanel-tab-color);
&:hover {
border-radius: 2px;
background-color: var(--dv-icon-hover-background-color);
}
}
}
} }

View File

@ -2,13 +2,21 @@ import {
IWatermarkRenderer, IWatermarkRenderer,
WatermarkRendererInitParameters, WatermarkRendererInitParameters,
} from '../../types'; } from '../../types';
import { addDisposableListener } from '../../../events';
import { toggleClass } from '../../../dom';
import { CompositeDisposable } from '../../../lifecycle'; import { CompositeDisposable } from '../../../lifecycle';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { PanelUpdateEvent } from '../../../panel/types';
import { createCloseButton } from '../../../svg';
import { DockviewApi } from '../../../api/component.api';
export class Watermark export class Watermark
extends CompositeDisposable extends CompositeDisposable
implements IWatermarkRenderer implements IWatermarkRenderer
{ {
private readonly _element: HTMLElement; private _element: HTMLElement;
private _group: DockviewGroupPanel | undefined;
private _api: DockviewApi | undefined;
get element(): HTMLElement { get element(): HTMLElement {
return this._element; return this._element;
@ -17,10 +25,70 @@ export class Watermark
constructor() { constructor() {
super(); super();
this._element = document.createElement('div'); this._element = document.createElement('div');
this._element.className = 'dv-watermark'; this._element.className = 'watermark';
const title = document.createElement('div');
title.className = 'watermark-title';
const emptySpace = document.createElement('span');
emptySpace.style.flexGrow = '1';
const content = document.createElement('div');
content.className = 'watermark-content';
this._element.appendChild(title);
this._element.appendChild(content);
const actionsContainer = document.createElement('div');
actionsContainer.className = 'actions-container';
const closeAnchor = document.createElement('div');
closeAnchor.className = 'close-action';
closeAnchor.appendChild(createCloseButton());
actionsContainer.appendChild(closeAnchor);
title.appendChild(emptySpace);
title.appendChild(actionsContainer);
this.addDisposables(
addDisposableListener(closeAnchor, 'click', (ev) => {
ev.preventDefault();
if (this._group) {
this._api?.removeGroup(this._group);
}
})
);
}
update(_event: PanelUpdateEvent): void {
// noop
}
focus(): void {
// noop
}
layout(_width: number, _height: number): void {
// noop
} }
init(_params: WatermarkRendererInitParameters): void { init(_params: WatermarkRendererInitParameters): void {
// noop this._api = _params.containerApi;
this.render();
}
updateParentGroup(group: DockviewGroupPanel, _visible: boolean): void {
this._group = group;
this.render();
}
dispose(): void {
super.dispose();
}
private render(): void {
const isOneGroup = !!(this._api && this._api.size <= 1);
toggleClass(this.element, 'has-actions', isOneGroup);
} }
} }

View File

@ -57,10 +57,6 @@ export class DefaultDockviewDeserialzier implements IPanelDeserializer {
view, view,
{ {
renderer: panelData.renderer, renderer: panelData.renderer,
minimumWidth: panelData.minimumWidth,
minimumHeight: panelData.minimumHeight,
maximumWidth: panelData.maximumWidth,
maximumHeight: panelData.maximumHeight,
} }
); );

View File

@ -14,42 +14,64 @@
.dv-overlay-render-container { .dv-overlay-render-container {
position: relative; position: relative;
} }
}
.dv-groupview { .split-view-container {
&.dv-active-group { &.horizontal {
> .dv-tabs-and-actions-container { > .view-container > .view {
.dv-tabs-container > .dv-tab { &:not(:last-child) {
&.dv-active-tab { border-right: var(--dv-group-gap-size) solid transparent;
background-color: var(
--dv-activegroup-visiblepanel-tab-background-color
);
color: var(--dv-activegroup-visiblepanel-tab-color);
} }
&.dv-inactive-tab {
background-color: var( &:not(:first-child) {
--dv-activegroup-hiddenpanel-tab-background-color border-left: var(--dv-group-gap-size) solid transparent;
); }
color: var(--dv-activegroup-hiddenpanel-tab-color); }
}
&.vertical {
> .view-container > .view {
&:not(:last-child) {
border-bottom: var(--dv-group-gap-size) solid transparent;
}
&:not(:first-child) {
border-top: var(--dv-group-gap-size) solid transparent;
} }
} }
} }
} }
&.dv-inactive-group { }
> .dv-tabs-and-actions-container {
.dv-tabs-container > .dv-tab { .groupview {
&.dv-active-tab { &.active-group {
background-color: var( > .tabs-and-actions-container > .tabs-container > .tab {
--dv-inactivegroup-visiblepanel-tab-background-color &.active-tab {
); background-color: var(
color: var(--dv-inactivegroup-visiblepanel-tab-color); --dv-activegroup-visiblepanel-tab-background-color
} );
&.dv-inactive-tab { color: var(--dv-activegroup-visiblepanel-tab-color);
background-color: var( }
--dv-inactivegroup-hiddenpanel-tab-background-color &.inactive-tab {
); background-color: var(
color: var(--dv-inactivegroup-hiddenpanel-tab-color); --dv-activegroup-hiddenpanel-tab-background-color
} );
color: var(--dv-activegroup-hiddenpanel-tab-color);
}
}
}
&.inactive-group {
> .tabs-and-actions-container > .tabs-container > .tab {
&.active-tab {
background-color: var(
--dv-inactivegroup-visiblepanel-tab-background-color
);
color: var(--dv-inactivegroup-visiblepanel-tab-color);
}
&.inactive-tab {
background-color: var(
--dv-inactivegroup-hiddenpanel-tab-background-color
);
color: var(--dv-inactivegroup-hiddenpanel-tab-color);
} }
} }
} }
@ -59,7 +81,7 @@
* when a tab is dragged we lose the above stylings because they are conditional on parent elements * when a tab is dragged we lose the above stylings because they are conditional on parent elements
* therefore we also set some stylings for the dragging event * therefore we also set some stylings for the dragging event
**/ **/
.dv-tab { .tab {
&.dv-tab-dragging { &.dv-tab-dragging {
background-color: var( background-color: var(
--dv-activegroup-visiblepanel-tab-background-color --dv-activegroup-visiblepanel-tab-background-color

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,17 @@
import { Overlay } from '../overlay/overlay'; import { Overlay } from '../dnd/overlay';
import { CompositeDisposable } from '../lifecycle'; import { CompositeDisposable } from '../lifecycle';
import { AnchoredBox } from '../types';
import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel'; import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel';
export interface IDockviewFloatingGroupPanel { export interface IDockviewFloatingGroupPanel {
readonly group: IDockviewGroupPanel; readonly group: IDockviewGroupPanel;
position(bounds: Partial<AnchoredBox>): void; position(
bounds: Partial<{
top: number;
left: number;
height: number;
width: number;
}>
): void;
} }
export class DockviewFloatingGroupPanel export class DockviewFloatingGroupPanel
@ -14,10 +20,18 @@ export class DockviewFloatingGroupPanel
{ {
constructor(readonly group: DockviewGroupPanel, readonly overlay: Overlay) { constructor(readonly group: DockviewGroupPanel, readonly overlay: Overlay) {
super(); super();
this.addDisposables(overlay); this.addDisposables(overlay);
} }
position(bounds: Partial<AnchoredBox>): void { position(
bounds: Partial<{
top: number;
left: number;
height: number;
width: number;
}>
): void {
this.overlay.setBounds(bounds); this.overlay.setBounds(bounds);
} }
} }

View File

@ -1,4 +1,4 @@
.dv-groupview { .groupview {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
@ -9,7 +9,13 @@
outline: none; outline: none;
} }
> .dv-content-container { &.empty {
> .tabs-and-actions-container {
display: none;
}
}
> .content-container {
flex-grow: 1; flex-grow: 1;
min-height: 0; min-height: 0;
outline: none; outline: none;

View File

@ -34,38 +34,6 @@ export class DockviewGroupPanel
{ {
private readonly _model: DockviewGroupPanelModel; private readonly _model: DockviewGroupPanelModel;
override get minimumWidth(): number {
const activePanelMinimumWidth = this.activePanel?.minimumWidth;
if (typeof activePanelMinimumWidth === 'number') {
return activePanelMinimumWidth;
}
return super.__minimumWidth();
}
override get minimumHeight(): number {
const activePanelMinimumHeight = this.activePanel?.minimumHeight;
if (typeof activePanelMinimumHeight === 'number') {
return activePanelMinimumHeight;
}
return super.__minimumHeight();
}
override get maximumWidth(): number {
const activePanelMaximumWidth = this.activePanel?.maximumWidth;
if (typeof activePanelMaximumWidth === 'number') {
return activePanelMaximumWidth;
}
return super.__maximumWidth();
}
override get maximumHeight(): number {
const activePanelMaximumHeight = this.activePanel?.maximumHeight;
if (typeof activePanelMaximumHeight === 'number') {
return activePanelMaximumHeight;
}
return super.__maximumHeight();
}
get panels(): IDockviewPanel[] { get panels(): IDockviewPanel[] {
return this._model.panels; return this._model.panels;
} }
@ -103,14 +71,8 @@ export class DockviewGroupPanel
id, id,
'groupview_default', 'groupview_default',
{ {
minimumHeight: minimumHeight: MINIMUM_DOCKVIEW_GROUP_PANEL_HEIGHT,
options.constraints?.minimumHeight ?? minimumWidth: MINIMUM_DOCKVIEW_GROUP_PANEL_WIDTH,
MINIMUM_DOCKVIEW_GROUP_PANEL_HEIGHT,
minimumWidth:
options.constraints?.maximumHeight ??
MINIMUM_DOCKVIEW_GROUP_PANEL_WIDTH,
maximumHeight: options.constraints?.maximumHeight,
maximumWidth: options.constraints?.maximumWidth,
}, },
new DockviewGroupPanelApiImpl(id, accessor) new DockviewGroupPanelApiImpl(id, accessor)
); );
@ -124,12 +86,6 @@ export class DockviewGroupPanel
options, options,
this this
); );
this.addDisposables(
this.model.onDidActivePanelChange((event) => {
this.api._onDidActivePanelChange.fire(event);
})
);
} }
override focus(): void { override focus(): void {

View File

@ -36,10 +36,8 @@ import {
DockviewUnhandledDragOverEvent, DockviewUnhandledDragOverEvent,
IHeaderActionsRenderer, IHeaderActionsRenderer,
} from './options'; } from './options';
import { OverlayRenderContainer } from '../overlay/overlayRenderContainer'; import { OverlayRenderContainer } from '../overlayRenderContainer';
import { TitleEvent } from '../api/dockviewPanelApi'; import { TitleEvent } from '../api/dockviewPanelApi';
import { Contraints } from '../gridview/gridviewPanel';
import { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer';
interface GroupMoveEvent { interface GroupMoveEvent {
groupId: string; groupId: string;
@ -52,9 +50,6 @@ interface CoreGroupOptions {
locked?: DockviewGroupPanelLocked; locked?: DockviewGroupPanelLocked;
hideHeader?: boolean; hideHeader?: boolean;
skipSetActive?: boolean; skipSetActive?: boolean;
constraints?: Partial<Contraints>;
initialWidth?: number;
initialHeight?: number;
} }
export interface GroupOptions extends CoreGroupOptions { export interface GroupOptions extends CoreGroupOptions {
@ -197,7 +192,7 @@ export interface IDockviewGroupPanelModel extends IPanel {
export type DockviewGroupLocation = export type DockviewGroupLocation =
| { type: 'grid' } | { type: 'grid' }
| { type: 'floating' } | { type: 'floating' }
| { type: 'popout'; getWindow: () => Window; popoutUrl?: string }; | { type: 'popout'; getWindow: () => Window };
export class WillShowOverlayLocationEvent implements IDockviewEvent { export class WillShowOverlayLocationEvent implements IDockviewEvent {
get kind(): DockviewGroupDropLocation { get kind(): DockviewGroupDropLocation {
@ -265,9 +260,6 @@ export class DockviewGroupPanelModel
private _location: DockviewGroupLocation = { type: 'grid' }; private _location: DockviewGroupLocation = { type: 'grid' };
private mostRecentlyUsed: IDockviewPanel[] = []; private mostRecentlyUsed: IDockviewPanel[] = [];
private _overwriteRenderContainer: OverlayRenderContainer | null = null;
private _overwriteDropTargetContainer: DropTargetAnchorContainer | null =
null;
private readonly _onDidChange = new Emitter<IViewSize | undefined>(); private readonly _onDidChange = new Emitter<IViewSize | undefined>();
readonly onDidChange: Event<IViewSize | undefined> = readonly onDidChange: Event<IViewSize | undefined> =
@ -276,7 +268,7 @@ export class DockviewGroupPanelModel
private _width = 0; private _width = 0;
private _height = 0; private _height = 0;
private readonly _panels: IDockviewPanel[] = []; private _panels: IDockviewPanel[] = [];
private readonly _panelDisposables = new Map<string, IDisposable>(); private readonly _panelDisposables = new Map<string, IDisposable>();
private readonly _onMove = new Emitter<GroupMoveEvent>(); private readonly _onMove = new Emitter<GroupMoveEvent>();
@ -330,7 +322,7 @@ export class DockviewGroupPanelModel
private readonly _api: DockviewApi; private readonly _api: DockviewApi;
get element(): HTMLElement { get element(): HTMLElement {
throw new Error('dockview: not supported'); throw new Error('not supported');
} }
get activePanel(): IDockviewPanel | undefined { get activePanel(): IDockviewPanel | undefined {
@ -346,7 +338,7 @@ export class DockviewGroupPanelModel
toggleClass( toggleClass(
this.container, this.container,
'dv-locked-groupview', 'locked-groupview',
value === 'no-drop-target' || value value === 'no-drop-target' || value
); );
} }
@ -433,14 +425,14 @@ export class DockviewGroupPanelModel
constructor( constructor(
private readonly container: HTMLElement, private readonly container: HTMLElement,
private readonly accessor: DockviewComponent, private accessor: DockviewComponent,
public id: string, public id: string,
private readonly options: GroupOptions, private readonly options: GroupOptions,
private readonly groupPanel: DockviewGroupPanel private readonly groupPanel: DockviewGroupPanel
) { ) {
super(); super();
toggleClass(this.container, 'dv-groupview', true); toggleClass(this.container, 'groupview', true);
this._api = new DockviewApi(this.accessor); this._api = new DockviewApi(this.accessor);
@ -509,9 +501,7 @@ export class DockviewGroupPanelModel
this._onDidAddPanel, this._onDidAddPanel,
this._onDidRemovePanel, this._onDidRemovePanel,
this._onDidActivePanelChange, this._onDidActivePanelChange,
this._onUnhandledDragOverEvent, this._onUnhandledDragOverEvent
this._onDidPanelTitleChange,
this._onDidPanelParametersChange
); );
} }
@ -519,6 +509,8 @@ export class DockviewGroupPanelModel
this.contentContainer.element.focus(); this.contentContainer.element.focus();
} }
private _overwriteRenderContainer: OverlayRenderContainer | null = null;
set renderContainer(value: OverlayRenderContainer | null) { set renderContainer(value: OverlayRenderContainer | null) {
this.panels.forEach((panel) => { this.panels.forEach((panel) => {
this.renderContainer.detatch(panel); this.renderContainer.detatch(panel);
@ -538,17 +530,6 @@ export class DockviewGroupPanelModel
); );
} }
set dropTargetContainer(value: DropTargetAnchorContainer | null) {
this._overwriteDropTargetContainer = value;
}
get dropTargetContainer(): DropTargetAnchorContainer | null {
return (
this._overwriteDropTargetContainer ??
this.accessor.rootDropTargetContainer
);
}
initialize(): void { initialize(): void {
if (this.options.panels) { if (this.options.panels) {
this.options.panels.forEach((panel) => { this.options.panels.forEach((panel) => {
@ -803,15 +784,7 @@ export class DockviewGroupPanelModel
} }
private doClose(panel: IDockviewPanel): void { private doClose(panel: IDockviewPanel): void {
const isLast = this.accessor.removePanel(panel);
this.panels.length === 1 && this.accessor.groups.length === 1;
this.accessor.removePanel(
panel,
isLast && this.accessor.options.noPanelsOverlay === 'emptyGroup'
? { removeEmptyGroup: false }
: undefined
);
} }
public isPanelActive(panel: IDockviewPanel): boolean { public isPanelActive(panel: IDockviewPanel): boolean {
@ -829,8 +802,8 @@ export class DockviewGroupPanelModel
this._isGroupActive = isGroupActive; this._isGroupActive = isGroupActive;
toggleClass(this.container, 'dv-active-group', isGroupActive); toggleClass(this.container, 'active-group', isGroupActive);
toggleClass(this.container, 'dv-inactive-group', !isGroupActive); toggleClass(this.container, 'inactive-group', !isGroupActive);
this.tabsContainer.setActive(this.isActive); this.tabsContainer.setActive(this.isActive);
@ -979,6 +952,8 @@ export class DockviewGroupPanelModel
} }
private updateContainer(): void { private updateContainer(): void {
toggleClass(this.container, 'empty', this.isEmpty);
this.panels.forEach((panel) => panel.runEvents()); this.panels.forEach((panel) => panel.runEvents());
if (this.isEmpty && !this.watermark) { if (this.isEmpty && !this.watermark) {
@ -989,18 +964,22 @@ export class DockviewGroupPanelModel
}); });
this.watermark = watermark; this.watermark = watermark;
addDisposableListener(this.watermark.element, 'pointerdown', () => { addDisposableListener(this.watermark.element, 'click', () => {
if (!this.isActive) { if (!this.isActive) {
this.accessor.doSetGroupActive(this.groupPanel); this.accessor.doSetGroupActive(this.groupPanel);
} }
}); });
this.tabsContainer.hide();
this.contentContainer.element.appendChild(this.watermark.element); this.contentContainer.element.appendChild(this.watermark.element);
this.watermark.updateParentGroup(this.groupPanel, true);
} }
if (!this.isEmpty && this.watermark) { if (!this.isEmpty && this.watermark) {
this.watermark.element.remove(); this.watermark.element.remove();
this.watermark.dispose?.(); this.watermark.dispose?.();
this.watermark = undefined; this.watermark = undefined;
this.tabsContainer.show();
} }
} }
@ -1014,7 +993,7 @@ export class DockviewGroupPanelModel
target, target,
position, position,
getPanelData, getPanelData,
this.accessor.getPanel(this.id) this.accessor.getPanel(this.id)!
); );
this._onUnhandledDragOverEvent.fire(firedEvent); this._onUnhandledDragOverEvent.fire(firedEvent);
@ -1063,29 +1042,6 @@ export class DockviewGroupPanelModel
const data = getPanelData(); const data = getPanelData();
if (data && data.viewId === this.accessor.id) { if (data && data.viewId === this.accessor.id) {
if (type === 'content') {
if (data.groupId === this.id) {
// don't allow to drop on self for center position
if (position === 'center') {
return;
}
if (data.panelId === null) {
// don't allow group move to drop anywhere on self
return;
}
}
}
if (type === 'header') {
if (data.groupId === this.id) {
if (data.panelId === null) {
return;
}
}
}
if (data.panelId === null) { if (data.panelId === null) {
// this is a group move dnd event // this is a group move dnd event
const { groupId } = data; const { groupId } = data;

View File

@ -9,9 +9,8 @@ import { CompositeDisposable, IDisposable } from '../lifecycle';
import { IPanel, PanelUpdateEvent, Parameters } from '../panel/types'; import { IPanel, PanelUpdateEvent, Parameters } from '../panel/types';
import { IDockviewPanelModel } from './dockviewPanelModel'; import { IDockviewPanelModel } from './dockviewPanelModel';
import { DockviewComponent } from './dockviewComponent'; import { DockviewComponent } from './dockviewComponent';
import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer'; import { DockviewPanelRenderer } from '../overlayRenderContainer';
import { WillFocusEvent } from '../api/panelApi'; import { WillFocusEvent } from '../api/panelApi';
import { Contraints } from '../gridview/gridviewPanel';
export interface IDockviewPanel extends IDisposable, IPanel { export interface IDockviewPanel extends IDisposable, IPanel {
readonly view: IDockviewPanelModel; readonly view: IDockviewPanelModel;
@ -19,10 +18,6 @@ export interface IDockviewPanel extends IDisposable, IPanel {
readonly api: DockviewPanelApi; readonly api: DockviewPanelApi;
readonly title: string | undefined; readonly title: string | undefined;
readonly params: Parameters | undefined; readonly params: Parameters | undefined;
readonly minimumWidth?: number;
readonly minimumHeight?: number;
readonly maximumWidth?: number;
readonly maximumHeight?: number;
updateParentGroup( updateParentGroup(
group: DockviewGroupPanel, group: DockviewGroupPanel,
options?: { skipSetActive?: boolean } options?: { skipSetActive?: boolean }
@ -45,11 +40,6 @@ export class DockviewPanel
private _title: string | undefined; private _title: string | undefined;
private _renderer: DockviewPanelRenderer | undefined; private _renderer: DockviewPanelRenderer | undefined;
private readonly _minimumWidth: number | undefined;
private readonly _minimumHeight: number | undefined;
private readonly _maximumWidth: number | undefined;
private readonly _maximumHeight: number | undefined;
get params(): Parameters | undefined { get params(): Parameters | undefined {
return this._params; return this._params;
} }
@ -66,22 +56,6 @@ export class DockviewPanel
return this._renderer ?? this.accessor.renderer; return this._renderer ?? this.accessor.renderer;
} }
get minimumWidth(): number | undefined {
return this._minimumWidth;
}
get minimumHeight(): number | undefined {
return this._minimumHeight;
}
get maximumWidth(): number | undefined {
return this._maximumWidth;
}
get maximumHeight(): number | undefined {
return this._maximumHeight;
}
constructor( constructor(
public readonly id: string, public readonly id: string,
component: string, component: string,
@ -90,15 +64,11 @@ export class DockviewPanel
private readonly containerApi: DockviewApi, private readonly containerApi: DockviewApi,
group: DockviewGroupPanel, group: DockviewGroupPanel,
readonly view: IDockviewPanelModel, readonly view: IDockviewPanelModel,
options: { renderer?: DockviewPanelRenderer } & Partial<Contraints> options: { renderer?: DockviewPanelRenderer }
) { ) {
super(); super();
this._renderer = options.renderer; this._renderer = options.renderer;
this._group = group; this._group = group;
this._minimumWidth = options.minimumWidth;
this._minimumHeight = options.minimumHeight;
this._maximumWidth = options.maximumWidth;
this._maximumHeight = options.maximumHeight;
this.api = new DockviewPanelApiImpl( this.api = new DockviewPanelApiImpl(
this, this,
@ -117,7 +87,7 @@ export class DockviewPanel
// you are actually just resizing the panels parent which is the group // you are actually just resizing the panels parent which is the group
this.group.api.setSize(event); this.group.api.setSize(event);
}), }),
this.api.onDidRendererChange(() => { this.api.onDidRendererChange((event) => {
this.group.model.rerender(this); this.group.model.rerender(this);
}) })
); );
@ -159,10 +129,6 @@ export class DockviewPanel
: undefined, : undefined,
title: this.title, title: this.title,
renderer: this._renderer, renderer: this._renderer,
minimumHeight: this._minimumHeight,
maximumHeight: this._maximumHeight,
minimumWidth: this._minimumWidth,
maximumWidth: this._maximumWidth,
}; };
} }
@ -171,6 +137,13 @@ export class DockviewPanel
if (didTitleChange) { if (didTitleChange) {
this._title = title; this._title = title;
this.view.update({
params: {
params: this._params,
title: this.title,
},
});
this.api._onDidTitleChange.fire({ title }); this.api._onDidTitleChange.fire({ title });
} }
} }
@ -205,7 +178,10 @@ export class DockviewPanel
// update the view with the updated props // update the view with the updated props
this.view.update({ this.view.update({
params: this._params, params: {
params: this._params,
title: this.title,
},
}); });
} }

View File

@ -4,29 +4,26 @@ import {
IContentRenderer, IContentRenderer,
ITabRenderer, ITabRenderer,
} from './types'; } from './types';
import { DockviewGroupPanel } from './dockviewGroupPanel';
import { IDisposable } from '../lifecycle'; import { IDisposable } from '../lifecycle';
import { IDockviewComponent } from './dockviewComponent'; import { IDockviewComponent } from './dockviewComponent';
import { PanelUpdateEvent } from '../panel/types'; import { PanelUpdateEvent } from '../panel/types';
import { TabLocation } from './framework';
export interface IDockviewPanelModel extends IDisposable { export interface IDockviewPanelModel extends IDisposable {
readonly contentComponent: string; readonly contentComponent: string;
readonly tabComponent?: string; readonly tabComponent?: string;
readonly content: IContentRenderer; readonly content: IContentRenderer;
readonly tab: ITabRenderer; readonly tab?: ITabRenderer;
update(event: PanelUpdateEvent): void; update(event: PanelUpdateEvent): void;
layout(width: number, height: number): void; layout(width: number, height: number): void;
init(params: GroupPanelPartInitParameters): void; init(params: GroupPanelPartInitParameters): void;
createTabRenderer(tabLocation: TabLocation): ITabRenderer; updateParentGroup(group: DockviewGroupPanel, isPanelVisible: boolean): void;
} }
export class DockviewPanelModel implements IDockviewPanelModel { export class DockviewPanelModel implements IDockviewPanelModel {
private readonly _content: IContentRenderer; private readonly _content: IContentRenderer;
private readonly _tab: ITabRenderer; private readonly _tab: ITabRenderer;
private _params: GroupPanelPartInitParameters | undefined;
private _updateEvent: PanelUpdateEvent | undefined;
get content(): IContentRenderer { get content(): IContentRenderer {
return this._content; return this._content;
} }
@ -45,23 +42,16 @@ export class DockviewPanelModel implements IDockviewPanelModel {
this._tab = this.createTabComponent(this.id, tabComponent); this._tab = this.createTabComponent(this.id, tabComponent);
} }
createTabRenderer(tabLocation: TabLocation): ITabRenderer { init(params: GroupPanelPartInitParameters): void {
const cmp = this.createTabComponent(this.id, this.tabComponent); this.content.init(params);
if (this._params) { this.tab.init(params);
cmp.init({ ...this._params, tabLocation });
}
if (this._updateEvent) {
cmp.update?.(this._updateEvent);
}
return cmp;
} }
init(params: GroupPanelPartInitParameters): void { updateParentGroup(
this._params = params; _group: DockviewGroupPanel,
_isPanelVisible: boolean
this.content.init(params); ): void {
this.tab.init({ ...params, tabLocation: 'header' }); // noop
} }
layout(width: number, height: number): void { layout(width: number, height: number): void {
@ -69,8 +59,6 @@ export class DockviewPanelModel implements IDockviewPanelModel {
} }
update(event: PanelUpdateEvent): void { update(event: PanelUpdateEvent): void {
this._updateEvent = event;
this.content.update?.(event); this.content.update?.(event);
this.tab.update?.(event); this.tab.update?.(event);
} }

View File

@ -11,11 +11,9 @@ export interface IGroupPanelBaseProps<T extends { [index: string]: any } = any>
containerApi: DockviewApi; containerApi: DockviewApi;
} }
export type TabLocation = 'header' | 'headerOverflow';
export type IDockviewPanelHeaderProps< export type IDockviewPanelHeaderProps<
T extends { [index: string]: any } = any T extends { [index: string]: any } = any
> = IGroupPanelBaseProps<T> & { tabLocation: TabLocation }; > = IGroupPanelBaseProps<T>;
export type IDockviewPanelProps<T extends { [index: string]: any } = any> = export type IDockviewPanelProps<T extends { [index: string]: any } = any> =
IGroupPanelBaseProps<T>; IGroupPanelBaseProps<T>;

View File

@ -12,12 +12,8 @@ import {
GroupOptions, GroupOptions,
} from './dockviewGroupPanelModel'; } from './dockviewGroupPanelModel';
import { IDockviewPanel } from './dockviewPanel'; import { IDockviewPanel } from './dockviewPanel';
import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer'; import { DockviewPanelRenderer } from '../overlayRenderContainer';
import { IGroupHeaderProps } from './framework'; import { IGroupHeaderProps } from './framework';
import { FloatingGroupOptions } from './dockviewComponent';
import { Contraints } from '../gridview/gridviewPanel';
import { AcceptableEvent, IAcceptableEvent } from '../events';
import { DockviewTheme } from './theme';
export interface IHeaderActionsRenderer extends IDisposable { export interface IHeaderActionsRenderer extends IDisposable {
readonly element: HTMLElement; readonly element: HTMLElement;
@ -36,10 +32,6 @@ export interface ViewFactoryData {
} }
export interface DockviewOptions { export interface DockviewOptions {
/**
* Disable the auto-resizing which is controlled through a `ResizeObserver`.
* Call `.layout(width, height)` to manually resize the container.
*/
disableAutoResizing?: boolean; disableAutoResizing?: boolean;
hideBorders?: boolean; hideBorders?: boolean;
singleTabMode?: 'fullwidth' | 'default'; singleTabMode?: 'fullwidth' | 'default';
@ -53,55 +45,43 @@ export interface DockviewOptions {
popoutUrl?: string; popoutUrl?: string;
defaultRenderer?: DockviewPanelRenderer; defaultRenderer?: DockviewPanelRenderer;
debug?: boolean; debug?: boolean;
// #start dnd
dndEdges?: false | DroptargetOverlayModel;
/**
* @deprecated use `dndEdges` instead. To be removed in a future version.
* */
rootOverlayModel?: DroptargetOverlayModel; rootOverlayModel?: DroptargetOverlayModel;
disableDnd?: boolean;
// #end dnd
locked?: boolean; locked?: boolean;
className?: string; disableDnd?: boolean;
/**
* Define the behaviour of the dock when there are no panels to display. Defaults to `watermark`.
*/
noPanelsOverlay?: 'emptyGroup' | 'watermark';
theme?: DockviewTheme;
disableTabsOverflowList?: boolean;
/**
* Select `native` to use built-in scrollbar behaviours and `custom` to use an internal implementation
* that allows for improved scrollbar overlay UX.
*
* This is only applied to the tab header section. Defaults to `custom`.
*/
scrollbars?: 'native' | 'custom';
} }
export interface DockviewDndOverlayEvent extends IAcceptableEvent { export interface DockviewDndOverlayEvent {
nativeEvent: DragEvent; nativeEvent: DragEvent;
target: DockviewGroupDropLocation; target: DockviewGroupDropLocation;
position: Position; position: Position;
group?: DockviewGroupPanel; group?: DockviewGroupPanel;
getData: () => PanelTransfer | undefined; getData: () => PanelTransfer | undefined;
//
isAccepted: boolean;
accept(): void;
} }
export class DockviewUnhandledDragOverEvent export class DockviewUnhandledDragOverEvent implements DockviewDndOverlayEvent {
extends AcceptableEvent private _isAccepted = false;
implements DockviewDndOverlayEvent
{ get isAccepted(): boolean {
return this._isAccepted;
}
constructor( constructor(
readonly nativeEvent: DragEvent, readonly nativeEvent: DragEvent,
readonly target: DockviewGroupDropLocation, readonly target: DockviewGroupDropLocation,
readonly position: Position, readonly position: Position,
readonly getData: () => PanelTransfer | undefined, readonly getData: () => PanelTransfer | undefined,
readonly group?: DockviewGroupPanel readonly group?: DockviewGroupPanel
) { ) {}
super();
accept(): void {
this._isAccepted = true;
} }
} }
export const PROPERTY_KEYS_DOCKVIEW: (keyof DockviewOptions)[] = (() => { export const PROPERTY_KEYS: (keyof DockviewOptions)[] = (() => {
/** /**
* by readong the keys from an empty value object TypeScript will error * by readong the keys from an empty value object TypeScript will error
* when we add or remove new properties to `DockviewOptions` * when we add or remove new properties to `DockviewOptions`
@ -118,29 +98,13 @@ export const PROPERTY_KEYS_DOCKVIEW: (keyof DockviewOptions)[] = (() => {
rootOverlayModel: undefined, rootOverlayModel: undefined,
locked: undefined, locked: undefined,
disableDnd: undefined, disableDnd: undefined,
className: undefined,
noPanelsOverlay: undefined,
dndEdges: undefined,
theme: undefined,
disableTabsOverflowList: undefined,
scrollbars: undefined,
}; };
return Object.keys(properties) as (keyof DockviewOptions)[]; return Object.keys(properties) as (keyof DockviewOptions)[];
})(); })();
export interface CreateComponentOptions {
/**
* The unqiue identifer of the component
*/
id: string;
/**
* The component name, this should determine what is rendered.
*/
name: string;
}
export interface DockviewFrameworkOptions { export interface DockviewFrameworkOptions {
parentElement: HTMLElement;
defaultTabComponent?: string; defaultTabComponent?: string;
createRightHeaderActionComponent?: ( createRightHeaderActionComponent?: (
group: DockviewGroupPanel group: DockviewGroupPanel
@ -151,10 +115,14 @@ export interface DockviewFrameworkOptions {
createPrefixHeaderActionComponent?: ( createPrefixHeaderActionComponent?: (
group: DockviewGroupPanel group: DockviewGroupPanel
) => IHeaderActionsRenderer; ) => IHeaderActionsRenderer;
createTabComponent?: ( createTabComponent?: (options: {
options: CreateComponentOptions id: string;
) => ITabRenderer | undefined; name: string;
createComponent: (options: CreateComponentOptions) => IContentRenderer; }) => ITabRenderer | undefined;
createComponent: (options: {
id: string;
name: string;
}) => IContentRenderer;
createWatermarkComponent?: () => IWatermarkRenderer; createWatermarkComponent?: () => IWatermarkRenderer;
} }
@ -172,19 +140,11 @@ export interface PanelOptions<P extends object = Parameters> {
type RelativePanel = { type RelativePanel = {
direction?: Direction; direction?: Direction;
referencePanel: string | IDockviewPanel; referencePanel: string | IDockviewPanel;
/**
* The index to place the panel within a group, only applicable if the placement is within an existing group
*/
index?: number;
}; };
type RelativeGroup = { type RelativeGroup = {
direction?: Direction; direction?: Direction;
referenceGroup: string | DockviewGroupPanel; referenceGroup: string | DockviewGroupPanel;
/**
* The index to place the panel within a group, only applicable if the placement is within an existing group
*/
index?: number;
}; };
type AbsolutePosition = { type AbsolutePosition = {
@ -215,12 +175,19 @@ export function isPanelOptionsWithGroup(
} }
type AddPanelFloatingGroupUnion = { type AddPanelFloatingGroupUnion = {
floating: Partial<FloatingGroupOptions> | true; floating:
| {
height?: number;
width?: number;
x?: number;
y?: number;
}
| true;
position: never; position: never;
}; };
type AddPanelPositionUnion = { type AddPanelPositionUnion = {
floating: false; floating: false | never;
position: AddPanelPositionOptions; position: AddPanelPositionOptions;
}; };
@ -258,10 +225,7 @@ export type AddPanelOptions<P extends object = Parameters> = {
* Defaults to `false` which forces newly added panels to become active. * Defaults to `false` which forces newly added panels to become active.
*/ */
inactive?: boolean; inactive?: boolean;
initialWidth?: number; } & Partial<AddPanelOptionsUnion>;
initialHeight?: number;
} & Partial<AddPanelOptionsUnion> &
Partial<Contraints>;
type AddGroupOptionsWithPanel = { type AddGroupOptionsWithPanel = {
referencePanel: string | IDockviewPanel; referencePanel: string | IDockviewPanel;

View File

@ -1,54 +0,0 @@
import { CompositeDisposable } from '../lifecycle';
import { DockviewComponent } from './dockviewComponent';
export class StrictEventsSequencing extends CompositeDisposable {
constructor(private readonly accessor: DockviewComponent) {
super();
this.init();
}
private init(): void {
const panels = new Set<string>();
const groups = new Set<string>();
this.addDisposables(
this.accessor.onDidAddPanel((panel) => {
if (panels.has(panel.api.id)) {
throw new Error(
`dockview: Invalid event sequence. [onDidAddPanel] called for panel ${panel.api.id} but panel already exists`
);
} else {
panels.add(panel.api.id);
}
}),
this.accessor.onDidRemovePanel((panel) => {
if (!panels.has(panel.api.id)) {
throw new Error(
`dockview: Invalid event sequence. [onDidRemovePanel] called for panel ${panel.api.id} but panel does not exists`
);
} else {
panels.delete(panel.api.id);
}
}),
this.accessor.onDidAddGroup((group) => {
if (groups.has(group.api.id)) {
throw new Error(
`dockview: Invalid event sequence. [onDidAddGroup] called for group ${group.api.id} but group already exists`
);
} else {
groups.add(group.api.id);
}
}),
this.accessor.onDidRemoveGroup((group) => {
if (!groups.has(group.api.id)) {
throw new Error(
`dockview: Invalid event sequence. [onDidRemoveGroup] called for group ${group.api.id} but group does not exists`
);
} else {
groups.delete(group.api.id);
}
})
);
}
}

View File

@ -1,70 +0,0 @@
export interface DockviewTheme {
/**
* The name of the theme
*/
name: string;
/**
* The class name to apply to the theme containing the CSS variables settings.
*/
className: string;
/**
* The gap between the groups
*/
gap?: number;
/**
* The mouting position of the overlay shown when dragging a panel. `absolute`
* will mount the overlay to root of the dockview component whereas `relative` will mount the overlay to the group container.
*/
dndOverlayMounting?: 'absolute' | 'relative';
/**
* When dragging a panel, the overlay can either encompass the panel contents or the entire group including the tab header space.
*/
dndPanelOverlay?: 'content' | 'group';
}
export const themeDark: DockviewTheme = {
name: 'dark',
className: 'dockview-theme-dark',
};
export const themeLight: DockviewTheme = {
name: 'light',
className: 'dockview-theme-light',
};
export const themeVisualStudio: DockviewTheme = {
name: 'visualStudio',
className: 'dockview-theme-vs',
};
export const themeAbyss: DockviewTheme = {
name: 'abyss',
className: 'dockview-theme-abyss',
};
export const themeDracula: DockviewTheme = {
name: 'dracula',
className: 'dockview-theme-dracula',
};
export const themeReplit: DockviewTheme = {
name: 'replit',
className: 'dockview-theme-replit',
gap: 10,
};
export const themeAbyssSpaced: DockviewTheme = {
name: 'abyssSpaced',
className: 'dockview-theme-abyss-spaced',
gap: 10,
dndOverlayMounting: 'absolute',
dndPanelOverlay: 'group',
};
export const themeLightSpaced: DockviewTheme = {
name: 'lightSpaced',
className: 'dockview-theme-light-spaced',
gap: 10,
dndOverlayMounting: 'absolute',
dndPanelOverlay: 'group',
};

View File

@ -1,10 +1,11 @@
import { IDockviewComponent } from './dockviewComponent';
import { DockviewPanelApi } from '../api/dockviewPanelApi'; import { DockviewPanelApi } from '../api/dockviewPanelApi';
import { PanelInitParameters, IPanel } from '../panel/types'; import { PanelInitParameters, IPanel } from '../panel/types';
import { DockviewApi } from '../api/component.api'; import { DockviewApi } from '../api/component.api';
import { Event } from '../events';
import { Optional } from '../types'; import { Optional } from '../types';
import { IDockviewGroupPanel } from './dockviewGroupPanel'; import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel';
import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer'; import { DockviewPanelRenderer } from '../overlayRenderContainer';
import { TabLocation } from './framework';
export interface HeaderPartInitParameters { export interface HeaderPartInitParameters {
title: string; title: string;
@ -22,39 +23,46 @@ export interface WatermarkRendererInitParameters {
group?: IDockviewGroupPanel; group?: IDockviewGroupPanel;
} }
type RendererMethodOptionalList =
| 'dispose'
| 'update'
| 'layout'
| 'toJSON'
| 'focus';
export interface IWatermarkRenderer export interface IWatermarkRenderer
extends Optional<Omit<IPanel, 'id' | 'init'>, RendererMethodOptionalList> { extends Optional<
Omit<IPanel, 'id' | 'init'>,
'dispose' | 'update' | 'layout' | 'toJSON' | 'focus'
> {
readonly element: HTMLElement; readonly element: HTMLElement;
init: (params: WatermarkRendererInitParameters) => void; init: (params: WatermarkRendererInitParameters) => void;
} updateParentGroup(group: DockviewGroupPanel, visible: boolean): void;
export interface TabPartInitParameters extends GroupPanelPartInitParameters {
tabLocation: TabLocation;
} }
export interface ITabRenderer export interface ITabRenderer
extends Optional<Omit<IPanel, 'id'>, RendererMethodOptionalList> { extends Optional<
Omit<IPanel, 'id'>,
'dispose' | 'update' | 'layout' | 'toJSON' | 'focus'
> {
readonly element: HTMLElement; readonly element: HTMLElement;
init(parameters: TabPartInitParameters): void; init(parameters: GroupPanelPartInitParameters): void;
} }
export interface IContentRenderer export interface IContentRenderer
extends Optional<Omit<IPanel, 'id'>, RendererMethodOptionalList> { extends Optional<
Omit<IPanel, 'id'>,
'dispose' | 'update' | 'layout' | 'toJSON' | 'focus'
> {
readonly element: HTMLElement; readonly element: HTMLElement;
init(parameters: GroupPanelPartInitParameters): void; init(parameters: GroupPanelPartInitParameters): void;
} }
// watermark component // watermark component
export interface WatermarkPartInitParameters {
accessor: IDockviewComponent;
}
// constructors // constructors
export interface WatermarkConstructor {
new (): IWatermarkRenderer;
}
export interface IGroupPanelInitParameters export interface IGroupPanelInitParameters
extends PanelInitParameters, extends PanelInitParameters,
HeaderPartInitParameters { HeaderPartInitParameters {
@ -68,8 +76,4 @@ export interface GroupviewPanelState {
title?: string; title?: string;
renderer?: DockviewPanelRenderer; renderer?: DockviewPanelRenderer;
params?: { [key: string]: any }; params?: { [key: string]: any };
minimumWidth?: number;
minimumHeight?: number;
maximumWidth?: number;
maximumHeight?: number;
} }

View File

@ -1,90 +0,0 @@
// import { SerializedGridObject } from '../gridview/gridview';
// import { Orientation } from '../splitview/splitview';
// import { SerializedDockview } from './dockviewComponent';
// import { GroupPanelViewState } from './dockviewGroupPanelModel';
// function typeValidate3(data: GroupPanelViewState, path: string): void {
// if (typeof data.id !== 'string') {
// throw new Error(`${path}.id must be a string`);
// }
// if (
// typeof data.activeView !== 'string' ||
// typeof data.activeView !== 'undefined'
// ) {
// throw new Error(`${path}.activeView must be a string of undefined`);
// }
// }
// function typeValidate2(
// data: SerializedGridObject<GroupPanelViewState>,
// path: string
// ): void {
// if (typeof data.size !== 'number' && typeof data.size !== 'undefined') {
// throw new Error(`${path}.size must be a number or undefined`);
// }
// if (
// typeof data.visible !== 'boolean' &&
// typeof data.visible !== 'undefined'
// ) {
// throw new Error(`${path}.visible must be a boolean or undefined`);
// }
// if (data.type === 'leaf') {
// if (
// typeof data.data !== 'object' ||
// data.data === null ||
// Array.isArray(data.data)
// ) {
// throw new Error('object must be a non-null object');
// }
// typeValidate3(data.data, `${path}.data`);
// } else if (data.type === 'branch') {
// if (!Array.isArray(data.data)) {
// throw new Error(`${path}.data must be an array`);
// }
// } else {
// throw new Error(`${path}.type must be onew of {'branch', 'leaf'}`);
// }
// }
// function typeValidate(data: SerializedDockview): void {
// if (typeof data !== 'object' || data === null) {
// throw new Error('object must be a non-null object');
// }
// const { grid, panels, activeGroup, floatingGroups } = data;
// if (typeof grid !== 'object' || grid === null) {
// throw new Error("'.grid' must be a non-null object");
// }
// if (typeof grid.height !== 'number') {
// throw new Error("'.grid.height' must be a number");
// }
// if (typeof grid.width !== 'number') {
// throw new Error("'.grid.width' must be a number");
// }
// if (typeof grid.root !== 'object' || grid.root === null) {
// throw new Error("'.grid.root' must be a non-null object");
// }
// if (grid.root.type !== 'branch') {
// throw new Error(".grid.root.type must be of type 'branch'");
// }
// if (
// grid.orientation !== Orientation.HORIZONTAL &&
// grid.orientation !== Orientation.VERTICAL
// ) {
// throw new Error(
// `'.grid.width' must be one of {${Orientation.HORIZONTAL}, ${Orientation.VERTICAL}}`
// );
// }
// typeValidate2(grid.root, '.grid.root');
// }

View File

@ -2,39 +2,10 @@ import {
Event as DockviewEvent, Event as DockviewEvent,
Emitter, Emitter,
addDisposableListener, addDisposableListener,
addDisposableWindowListener,
} from './events'; } from './events';
import { IDisposable, CompositeDisposable } from './lifecycle'; import { IDisposable, CompositeDisposable } from './lifecycle';
export interface OverflowEvent {
hasScrollX: boolean;
hasScrollY: boolean;
}
export class OverflowObserver extends CompositeDisposable {
private readonly _onDidChange = new Emitter<OverflowEvent>();
readonly onDidChange = this._onDidChange.event;
private _value: OverflowEvent | null = null;
constructor(el: HTMLElement) {
super();
this.addDisposables(
this._onDidChange,
watchElementResize(el, (entry) => {
const hasScrollX =
entry.target.scrollWidth > entry.target.clientWidth;
const hasScrollY =
entry.target.scrollHeight > entry.target.clientHeight;
this._value = { hasScrollX, hasScrollY };
this._onDidChange.fire(this._value);
})
);
}
}
export function watchElementResize( export function watchElementResize(
element: HTMLElement, element: HTMLElement,
cb: (entry: ResizeObserverEntry) => void cb: (entry: ResizeObserverEntry) => void
@ -111,11 +82,8 @@ export function isAncestor(
return false; return false;
} }
export function getElementsByTagName( export function getElementsByTagName(tag: string): HTMLElement[] {
tag: string, return Array.prototype.slice.call(document.getElementsByTagName(tag), 0);
document: ParentNode
): HTMLElement[] {
return Array.prototype.slice.call(document.querySelectorAll(tag), 0);
} }
export interface IFocusTracker extends IDisposable { export interface IFocusTracker extends IDisposable {
@ -124,7 +92,7 @@ export interface IFocusTracker extends IDisposable {
refreshState?(): void; refreshState?(): void;
} }
export function trackFocus(element: HTMLElement): IFocusTracker { export function trackFocus(element: HTMLElement | Window): IFocusTracker {
return new FocusTracker(element); return new FocusTracker(element);
} }
@ -138,9 +106,9 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker {
private readonly _onDidBlur = new Emitter<void>(); private readonly _onDidBlur = new Emitter<void>();
public readonly onDidBlur: DockviewEvent<void> = this._onDidBlur.event; public readonly onDidBlur: DockviewEvent<void> = this._onDidBlur.event;
private readonly _refreshStateHandler: () => void; private _refreshStateHandler: () => void;
constructor(element: HTMLElement) { constructor(element: HTMLElement | Window) {
super(); super();
this.addDisposables(this._onDidFocus, this._onDidBlur); this.addDisposables(this._onDidFocus, this._onDidBlur);
@ -183,12 +151,21 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker {
} }
}; };
this.addDisposables( if (element instanceof HTMLElement) {
addDisposableListener(element, 'focus', onFocus, true) this.addDisposables(
); addDisposableListener(element, 'focus', onFocus, true)
this.addDisposables( );
addDisposableListener(element, 'blur', onBlur, true) this.addDisposables(
); addDisposableListener(element, 'blur', onBlur, true)
);
} else {
this.addDisposables(
addDisposableWindowListener(element, 'focus', onFocus, true)
);
this.addDisposables(
addDisposableWindowListener(element, 'blur', onBlur, true)
);
}
} }
refreshState(): void { refreshState(): void {
@ -276,225 +253,3 @@ export function isInDocument(element: Element): boolean {
return false; return false;
} }
export function addTestId(element: HTMLElement, id: string): void {
element.setAttribute('data-testid', id);
}
/**
* Should be more efficient than element.querySelectorAll("*") since there
* is no need to store every element in-memory using this approach
*/
function allTagsNamesInclusiveOfShadowDoms(tagNames: string[]) {
const iframes: HTMLElement[] = [];
function findIframesInNode(node: Element) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (tagNames.includes(node.tagName)) {
iframes.push(node as HTMLElement);
}
if (node.shadowRoot) {
findIframesInNode(<any>node.shadowRoot);
}
for (const child of node.children) {
findIframesInNode(child);
}
}
}
findIframesInNode(document.documentElement);
return iframes;
}
export function disableIframePointEvents(rootNode: ParentNode = document) {
const iframes = allTagsNamesInclusiveOfShadowDoms(['IFRAME', 'WEBVIEW']);
const original = new WeakMap<HTMLElement, string>(); // don't hold onto HTMLElement references longer than required
for (const iframe of iframes) {
original.set(iframe, iframe.style.pointerEvents);
iframe.style.pointerEvents = 'none';
}
return {
release: () => {
for (const iframe of iframes) {
iframe.style.pointerEvents = original.get(iframe) ?? 'auto';
}
iframes.splice(0, iframes.length); // don't hold onto HTMLElement references longer than required
},
};
}
export function getDockviewTheme(element: HTMLElement): string | undefined {
function toClassList(element: HTMLElement) {
const list: string[] = [];
for (let i = 0; i < element.classList.length; i++) {
list.push(element.classList.item(i)!);
}
return list;
}
let theme: string | undefined = undefined;
let parent: HTMLElement | null = element;
while (parent !== null) {
theme = toClassList(parent).find((cls) =>
cls.startsWith('dockview-theme-')
);
if (typeof theme === 'string') {
break;
}
parent = parent.parentElement;
}
return theme;
}
export class Classnames {
private _classNames: string[] = [];
constructor(private readonly element: HTMLElement) {}
setClassNames(classNames: string) {
for (const className of this._classNames) {
toggleClass(this.element, className, false);
}
this._classNames = classNames
.split(' ')
.filter((v) => v.trim().length > 0);
for (const className of this._classNames) {
toggleClass(this.element, className, true);
}
}
}
const DEBOUCE_DELAY = 100;
export function isChildEntirelyVisibleWithinParent(
child: HTMLElement,
parent: HTMLElement
): boolean {
//
const childPosition = getDomNodePagePosition(child);
const parentPosition = getDomNodePagePosition(parent);
if (childPosition.left < parentPosition.left) {
return false;
}
if (
childPosition.left + childPosition.width >
parentPosition.left + parentPosition.width
) {
return false;
}
return true;
}
export function onDidWindowMoveEnd(window: Window): Emitter<void> {
const emitter = new Emitter<void>();
let previousScreenX = window.screenX;
let previousScreenY = window.screenY;
let timeout: any;
const checkMovement = () => {
if (window.closed) {
return;
}
const currentScreenX = window.screenX;
const currentScreenY = window.screenY;
if (
currentScreenX !== previousScreenX ||
currentScreenY !== previousScreenY
) {
clearTimeout(timeout);
timeout = setTimeout(() => {
emitter.fire();
}, DEBOUCE_DELAY);
previousScreenX = currentScreenX;
previousScreenY = currentScreenY;
}
requestAnimationFrame(checkMovement);
};
checkMovement();
return emitter;
}
export function onDidWindowResizeEnd(element: Window, cb: () => void) {
let resizeTimeout: any;
const disposable = new CompositeDisposable(
addDisposableListener(element, 'resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
cb();
}, DEBOUCE_DELAY);
})
);
return disposable;
}
export function shiftAbsoluteElementIntoView(
element: HTMLElement,
root: HTMLElement,
options: { buffer: number } = { buffer: 10 }
) {
const buffer = options.buffer;
const rect = element.getBoundingClientRect();
const rootRect = root.getBoundingClientRect();
let translateX = 0;
let translateY = 0;
const left = rect.left - rootRect.left;
const top = rect.top - rootRect.top;
const bottom = rect.bottom - rootRect.bottom;
const right = rect.right - rootRect.right;
// Check horizontal overflow
if (left < buffer) {
translateX = buffer - left;
} else if (right > buffer) {
translateX = -buffer - right;
}
// Check vertical overflow
if (top < buffer) {
translateY = buffer - top;
} else if (bottom > buffer) {
translateY = -bottom - buffer;
}
// Apply the translation if needed
if (translateX !== 0 || translateY !== 0) {
element.style.transform = `translate(${translateX}px, ${translateY}px)`;
}
}
export function findRelativeZIndexParent(el: HTMLElement): HTMLElement | null {
let tmp: HTMLElement | null = el;
while (tmp && (tmp.style.zIndex === 'auto' || tmp.style.zIndex === '')) {
tmp = tmp.parentElement;
}
return tmp;
}

View File

@ -41,23 +41,6 @@ export class DockviewEvent implements IDockviewEvent {
} }
} }
export interface IAcceptableEvent {
readonly isAccepted: boolean;
accept(): void;
}
export class AcceptableEvent implements IAcceptableEvent {
private _isAccepted = false;
get isAccepted(): boolean {
return this._isAccepted;
}
accept(): void {
this._isAccepted = true;
}
}
class LeakageMonitor { class LeakageMonitor {
readonly events = new Map<Event<any>, Stacktrace>(); readonly events = new Map<Event<any>, Stacktrace>();
@ -86,7 +69,7 @@ class Stacktrace {
private constructor(readonly value: string) {} private constructor(readonly value: string) {}
print(): void { print(): void {
console.warn('dockview: stacktrace', this.value); console.warn(this.value);
} }
} }
@ -141,7 +124,7 @@ export class Emitter<T> implements IDisposable {
this._listeners.splice(index, 1); this._listeners.splice(index, 1);
} else if (Emitter.ENABLE_TRACKING) { } else if (Emitter.ENABLE_TRACKING) {
// console.warn( // console.warn(
// `dockview: listener already disposed`, // `Listener already disposed`,
// Stacktrace.create().print() // Stacktrace.create().print()
// ); // );
} }
@ -175,10 +158,7 @@ export class Emitter<T> implements IDisposable {
queueMicrotask(() => { queueMicrotask(() => {
// don't check until stack of execution is completed to allow for out-of-order disposals within the same execution block // don't check until stack of execution is completed to allow for out-of-order disposals within the same execution block
for (const listener of this._listeners) { for (const listener of this._listeners) {
console.warn( console.warn(listener.stacktrace?.print());
'dockview: stacktrace',
listener.stacktrace?.print()
);
} }
}); });
} }
@ -193,38 +173,32 @@ export class Emitter<T> implements IDisposable {
} }
} }
export function addDisposableListener<K extends keyof WindowEventMap>( export function addDisposableWindowListener<K extends keyof WindowEventMap>(
element: Window, element: Window,
type: K, type: K,
listener: (this: Window, ev: WindowEventMap[K]) => any, listener: (this: Window, ev: WindowEventMap[K]) => any,
options?: boolean | AddEventListenerOptions options?: boolean | AddEventListenerOptions
): IDisposable; ): IDisposable {
element.addEventListener(type, listener, options);
return {
dispose: () => {
element.removeEventListener(type, listener, options);
},
};
}
export function addDisposableListener<K extends keyof HTMLElementEventMap>( export function addDisposableListener<K extends keyof HTMLElementEventMap>(
element: HTMLElement, element: HTMLElement,
type: K, type: K,
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,
options?: boolean | AddEventListenerOptions options?: boolean | AddEventListenerOptions
): IDisposable;
export function addDisposableListener<
K extends keyof HTMLElementEventMap | keyof WindowEventMap
>(
element: HTMLElement | Window,
type: K,
listener: (
this: K extends keyof HTMLElementEventMap ? HTMLElement : Window,
ev: K extends keyof HTMLElementEventMap
? HTMLElementEventMap[K]
: K extends keyof WindowEventMap
? WindowEventMap[K]
: never
) => any,
options?: boolean | AddEventListenerOptions
): IDisposable { ): IDisposable {
element.addEventListener(type, <any>listener, options); element.addEventListener(type, listener, options);
return { return {
dispose: () => { dispose: () => {
element.removeEventListener(type, <any>listener, options); element.removeEventListener(type, listener, options);
}, },
}; };
} }

View File

@ -1,19 +1,15 @@
import { Emitter, Event, AsapEvent } from '../events'; import { Emitter, Event, AsapEvent } from '../events';
import { getGridLocation, Gridview, IGridView } from './gridview'; import { getGridLocation, Gridview, IGridView } from './gridview';
import { Position } from '../dnd/droptarget'; import { Position } from '../dnd/droptarget';
import { Disposable, IDisposable, IValueDisposable } from '../lifecycle'; import { Disposable, IValueDisposable } from '../lifecycle';
import { sequentialNumberGenerator } from '../math'; import { sequentialNumberGenerator } from '../math';
import { ISplitviewStyles, Orientation, Sizing } from '../splitview/splitview'; import { ISplitviewStyles, Orientation, Sizing } from '../splitview/splitview';
import { IPanel } from '../panel/types'; import { IPanel } from '../panel/types';
import { MovementOptions2 } from '../dockview/options'; import { MovementOptions2 } from '../dockview/options';
import { Resizable } from '../resizable'; import { Resizable } from '../resizable';
import { Classnames } from '../dom';
const nextLayoutId = sequentialNumberGenerator(); const nextLayoutId = sequentialNumberGenerator();
/**
* A direction in which a panel can be moved or placed relative to another panel.
*/
export type Direction = 'left' | 'right' | 'above' | 'below' | 'within'; export type Direction = 'left' | 'right' | 'above' | 'below' | 'within';
export function toTarget(direction: Direction): Position { export function toTarget(direction: Direction): Position {
@ -32,19 +28,13 @@ export function toTarget(direction: Direction): Position {
} }
} }
export interface MaximizedChanged<T extends IGridPanelView> {
panel: T;
isMaximized: boolean;
}
export interface BaseGridOptions { export interface BaseGridOptions {
readonly proportionalLayout: boolean; readonly proportionalLayout: boolean;
readonly orientation: Orientation; readonly orientation: Orientation;
readonly styles?: ISplitviewStyles; readonly styles?: ISplitviewStyles;
readonly parentElement: HTMLElement;
readonly disableAutoResizing?: boolean; readonly disableAutoResizing?: boolean;
readonly locked?: boolean; readonly locked?: boolean;
readonly margin?: number;
readonly className?: string;
} }
export interface IGridPanelView extends IGridView, IPanel { export interface IGridPanelView extends IGridView, IPanel {
@ -52,7 +42,7 @@ export interface IGridPanelView extends IGridView, IPanel {
readonly isActive: boolean; readonly isActive: boolean;
} }
export interface IBaseGrid<T extends IGridPanelView> extends IDisposable { export interface IBaseGrid<T extends IGridPanelView> {
readonly element: HTMLElement; readonly element: HTMLElement;
readonly id: string; readonly id: string;
readonly width: number; readonly width: number;
@ -64,8 +54,6 @@ export interface IBaseGrid<T extends IGridPanelView> extends IDisposable {
readonly activeGroup: T | undefined; readonly activeGroup: T | undefined;
readonly size: number; readonly size: number;
readonly groups: T[]; readonly groups: T[];
readonly onDidMaximizedChange: Event<MaximizedChanged<T>>;
readonly onDidLayoutChange: Event<void>;
getPanel(id: string): T | undefined; getPanel(id: string): T | undefined;
toJSON(): object; toJSON(): object;
fromJSON(data: any): void; fromJSON(data: any): void;
@ -77,6 +65,8 @@ export interface IBaseGrid<T extends IGridPanelView> extends IDisposable {
isMaximizedGroup(panel: T): boolean; isMaximizedGroup(panel: T): boolean;
exitMaximizedGroup(): void; exitMaximizedGroup(): void;
hasMaximizedGroup(): boolean; hasMaximizedGroup(): boolean;
readonly onDidMaximizedGroupChange: Event<void>;
readonly onDidLayoutChange: Event<void>;
} }
export abstract class BaseGrid<T extends IGridPanelView> export abstract class BaseGrid<T extends IGridPanelView>
@ -95,10 +85,6 @@ export abstract class BaseGrid<T extends IGridPanelView>
private readonly _onDidAdd = new Emitter<T>(); private readonly _onDidAdd = new Emitter<T>();
readonly onDidAdd: Event<T> = this._onDidAdd.event; readonly onDidAdd: Event<T> = this._onDidAdd.event;
private readonly _onDidMaximizedChange = new Emitter<MaximizedChanged<T>>();
readonly onDidMaximizedChange: Event<MaximizedChanged<T>> =
this._onDidMaximizedChange.event;
private readonly _onDidActiveChange = new Emitter<T | undefined>(); private readonly _onDidActiveChange = new Emitter<T | undefined>();
readonly onDidActiveChange: Event<T | undefined> = readonly onDidActiveChange: Event<T | undefined> =
this._onDidActiveChange.event; this._onDidActiveChange.event;
@ -107,12 +93,6 @@ export abstract class BaseGrid<T extends IGridPanelView>
readonly onDidLayoutChange: Event<void> = readonly onDidLayoutChange: Event<void> =
this._bufferOnDidLayoutChange.onEvent; this._bufferOnDidLayoutChange.onEvent;
private readonly _onDidViewVisibilityChangeMicroTaskQueue = new AsapEvent();
readonly onDidViewVisibilityChangeMicroTaskQueue =
this._onDidViewVisibilityChangeMicroTaskQueue.onEvent;
private readonly _classNames: Classnames;
get id(): string { get id(): string {
return this._id; return this._id;
} }
@ -158,23 +138,17 @@ export abstract class BaseGrid<T extends IGridPanelView>
this.gridview.locked = value; this.gridview.locked = value;
} }
constructor(container: HTMLElement, options: BaseGridOptions) { constructor(options: BaseGridOptions) {
super(document.createElement('div'), options.disableAutoResizing); super(document.createElement('div'), options.disableAutoResizing);
this.element.style.height = '100%'; this.element.style.height = '100%';
this.element.style.width = '100%'; this.element.style.width = '100%';
this._classNames = new Classnames(this.element); options.parentElement.appendChild(this.element);
this._classNames.setClassNames(options.className ?? '');
// the container is owned by the third-party, do not modify/delete it
container.appendChild(this.element);
this.gridview = new Gridview( this.gridview = new Gridview(
!!options.proportionalLayout, !!options.proportionalLayout,
options.styles, options.styles,
options.orientation, options.orientation
options.locked,
options.margin
); );
this.gridview.locked = !!options.locked; this.gridview.locked = !!options.locked;
@ -184,18 +158,6 @@ export abstract class BaseGrid<T extends IGridPanelView>
this.layout(0, 0, true); // set some elements height/widths this.layout(0, 0, true); // set some elements height/widths
this.addDisposables( this.addDisposables(
this.gridview.onDidMaximizedNodeChange((event) => {
this._onDidMaximizedChange.fire({
panel: event.view as T,
isMaximized: event.isMaximized,
});
}),
this.gridview.onDidViewVisibilityChange(() =>
this._onDidViewVisibilityChangeMicroTaskQueue.fire()
),
this.onDidViewVisibilityChangeMicroTaskQueue(() => {
this.layout(this.width, this.height, true);
}),
Disposable.from(() => { Disposable.from(() => {
this.element.parentElement?.removeChild(this.element); this.element.parentElement?.removeChild(this.element);
}), }),
@ -209,8 +171,6 @@ export abstract class BaseGrid<T extends IGridPanelView>
)(() => { )(() => {
this._bufferOnDidLayoutChange.fire(); this._bufferOnDidLayoutChange.fire();
}), }),
this._onDidMaximizedChange,
this._onDidViewVisibilityChangeMicroTaskQueue,
this._bufferOnDidLayoutChange this._bufferOnDidLayoutChange
); );
} }
@ -230,30 +190,6 @@ export abstract class BaseGrid<T extends IGridPanelView>
return this.gridview.isViewVisible(getGridLocation(panel.element)); return this.gridview.isViewVisible(getGridLocation(panel.element));
} }
updateOptions(options: Partial<BaseGridOptions>) {
if (typeof options.proportionalLayout === 'boolean') {
// this.gridview.proportionalLayout = options.proportionalLayout; // not supported
}
if (options.orientation) {
this.gridview.orientation = options.orientation;
}
if ('styles' in options) {
// this.gridview.styles = options.styles; // not supported
}
if ('disableResizing' in options) {
this.disableResizing = options.disableAutoResizing ?? false;
}
if ('locked' in options) {
this.locked = options.locked ?? false;
}
if ('margin' in options) {
this.gridview.margin = options.margin ?? 0;
}
if ('className' in options) {
this._classNames.setClassNames(options.className ?? '');
}
}
maximizeGroup(panel: T): void { maximizeGroup(panel: T): void {
this.gridview.maximizeView(panel); this.gridview.maximizeView(panel);
this.doSetGroupActive(panel); this.doSetGroupActive(panel);
@ -271,6 +207,10 @@ export abstract class BaseGrid<T extends IGridPanelView>
return this.gridview.hasMaximizedView(); return this.gridview.hasMaximizedView();
} }
get onDidMaximizedGroupChange(): Event<void> {
return this.gridview.onDidMaximizedNodeChange;
}
protected doAddGroup( protected doAddGroup(
group: T, group: T,
location: number[] = [0], location: number[] = [0],
@ -370,7 +310,7 @@ export abstract class BaseGrid<T extends IGridPanelView>
public layout(width: number, height: number, forceResize?: boolean): void { public layout(width: number, height: number, forceResize?: boolean): void {
const different = const different =
forceResize || width !== this.width || height !== this.height; forceResize ?? (width !== this.width || height !== this.height);
if (!different) { if (!different) {
return; return;

View File

@ -32,7 +32,7 @@ export abstract class BasePanelView<T extends PanelApiImpl>
{ {
private _height = 0; private _height = 0;
private _width = 0; private _width = 0;
private readonly _element: HTMLElement; private _element: HTMLElement;
protected part?: IFrameworkPart; protected part?: IFrameworkPart;
protected _params?: PanelInitParameters; protected _params?: PanelInitParameters;

View File

@ -19,7 +19,7 @@ import { CompositeDisposable, IDisposable, Disposable } from '../lifecycle';
export class BranchNode extends CompositeDisposable implements IView { export class BranchNode extends CompositeDisposable implements IView {
readonly element: HTMLElement; readonly element: HTMLElement;
private readonly splitview: Splitview; private splitview: Splitview;
private _orthogonalSize: number; private _orthogonalSize: number;
private _size: number; private _size: number;
private _childrenDisposable: IDisposable = Disposable.NONE; private _childrenDisposable: IDisposable = Disposable.NONE;
@ -33,12 +33,9 @@ export class BranchNode extends CompositeDisposable implements IView {
readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> = readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> =
this._onDidChange.event; this._onDidChange.event;
private readonly _onDidVisibilityChange = new Emitter<{ private readonly _onDidVisibilityChange = new Emitter<boolean>();
visible: boolean; readonly onDidVisibilityChange: Event<boolean> =
}>(); this._onDidVisibilityChange.event;
readonly onDidVisibilityChange: Event<{
visible: boolean;
}> = this._onDidVisibilityChange.event;
get width(): number { get width(): number {
return this.orientation === Orientation.HORIZONTAL return this.orientation === Orientation.HORIZONTAL
@ -142,20 +139,6 @@ export class BranchNode extends CompositeDisposable implements IView {
this.splitview.disabled = value; this.splitview.disabled = value;
} }
get margin(): number {
return this.splitview.margin;
}
set margin(value: number) {
this.splitview.margin = value;
this.children.forEach((child) => {
if (child instanceof BranchNode) {
child.margin = value;
}
});
}
constructor( constructor(
readonly orientation: Orientation, readonly orientation: Orientation,
readonly proportionalLayout: boolean, readonly proportionalLayout: boolean,
@ -163,7 +146,6 @@ export class BranchNode extends CompositeDisposable implements IView {
size: number, size: number,
orthogonalSize: number, orthogonalSize: number,
disabled: boolean, disabled: boolean,
margin: number | undefined,
childDescriptors?: INodeDescriptor[] childDescriptors?: INodeDescriptor[]
) { ) {
super(); super();
@ -171,14 +153,13 @@ export class BranchNode extends CompositeDisposable implements IView {
this._size = size; this._size = size;
this.element = document.createElement('div'); this.element = document.createElement('div');
this.element.className = 'dv-branch-node'; this.element.className = 'branch-node';
if (!childDescriptors) { if (!childDescriptors) {
this.splitview = new Splitview(this.element, { this.splitview = new Splitview(this.element, {
orientation: this.orientation, orientation: this.orientation,
proportionalLayout, proportionalLayout,
styles, styles,
margin,
}); });
this.splitview.layout(this.size, this.orthogonalSize); this.splitview.layout(this.size, this.orthogonalSize);
} else { } else {
@ -203,7 +184,6 @@ export class BranchNode extends CompositeDisposable implements IView {
descriptor, descriptor,
proportionalLayout, proportionalLayout,
styles, styles,
margin,
}); });
} }
@ -220,8 +200,10 @@ export class BranchNode extends CompositeDisposable implements IView {
this.setupChildrenEvents(); this.setupChildrenEvents();
} }
setVisible(_visible: boolean): void { setVisible(visible: boolean): void {
// noop for (const child of this.children) {
child.setVisible(visible);
}
} }
isChildVisible(index: number): boolean { isChildVisible(index: number): boolean {
@ -242,9 +224,7 @@ export class BranchNode extends CompositeDisposable implements IView {
} }
const wereAllChildrenHidden = this.splitview.contentSize === 0; const wereAllChildrenHidden = this.splitview.contentSize === 0;
this.splitview.setViewVisible(index, visible); this.splitview.setViewVisible(index, visible);
// }
const areAllChildrenHidden = this.splitview.contentSize === 0; const areAllChildrenHidden = this.splitview.contentSize === 0;
// If all children are hidden then the parent should hide the entire splitview // If all children are hidden then the parent should hide the entire splitview
@ -253,7 +233,7 @@ export class BranchNode extends CompositeDisposable implements IView {
(visible && wereAllChildrenHidden) || (visible && wereAllChildrenHidden) ||
(!visible && areAllChildrenHidden) (!visible && areAllChildrenHidden)
) { ) {
this._onDidVisibilityChange.fire({ visible }); this._onDidVisibilityChange.fire(visible);
} }
} }
@ -355,7 +335,7 @@ export class BranchNode extends CompositeDisposable implements IView {
}), }),
...this.children.map((c, i) => { ...this.children.map((c, i) => {
if (c instanceof BranchNode) { if (c instanceof BranchNode) {
return c.onDidVisibilityChange(({ visible }) => { return c.onDidVisibilityChange((visible) => {
this.setChildVisible(i, visible); this.setChildVisible(i, visible);
}); });
} }

View File

@ -1,5 +1,5 @@
.dv-grid-view, .grid-view,
.dv-branch-node { .branch-node {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }

View File

@ -42,8 +42,7 @@ function flipNode<T extends Node>(
node.styles, node.styles,
size, size,
orthogonalSize, orthogonalSize,
node.disabled, node.disabled
node.margin
); );
let totalSize = 0; let totalSize = 0;
@ -113,7 +112,7 @@ export function getGridLocation(element: HTMLElement): number[] {
throw new Error('Invalid grid element'); throw new Error('Invalid grid element');
} }
if (/\bdv-grid-view\b/.test(parentElement.className)) { if (/\bgrid-view\b/.test(parentElement.className)) {
return []; return [];
} }
@ -172,7 +171,6 @@ export interface IGridView {
readonly maximumWidth: number; readonly maximumWidth: number;
readonly minimumHeight: number; readonly minimumHeight: number;
readonly maximumHeight: number; readonly maximumHeight: number;
readonly isVisible: boolean;
priority?: LayoutPriority; priority?: LayoutPriority;
layout(width: number, height: number): void; layout(width: number, height: number): void;
toJSON(): object; toJSON(): object;
@ -265,21 +263,11 @@ export interface IViewDeserializer {
fromJSON: (data: ISerializedLeafNode) => IGridView; fromJSON: (data: ISerializedLeafNode) => IGridView;
} }
export interface SerializedNodeDescriptor {
location: number[];
}
export interface SerializedGridview<T> { export interface SerializedGridview<T> {
root: SerializedGridObject<T>; root: SerializedGridObject<T>;
width: number; width: number;
height: number; height: number;
orientation: Orientation; orientation: Orientation;
maximizedNode?: SerializedNodeDescriptor;
}
export interface MaximizedViewChanged {
view: IGridView;
isMaximized: boolean;
} }
export class Gridview implements IDisposable { export class Gridview implements IDisposable {
@ -287,7 +275,6 @@ export class Gridview implements IDisposable {
private _root: BranchNode | undefined; private _root: BranchNode | undefined;
private _locked = false; private _locked = false;
private _margin = 0;
private _maximizedNode: private _maximizedNode:
| { leaf: LeafNode; hiddenOnMaximize: LeafNode[] } | { leaf: LeafNode; hiddenOnMaximize: LeafNode[] }
| undefined = undefined; | undefined = undefined;
@ -300,11 +287,7 @@ export class Gridview implements IDisposable {
readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> = readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> =
this._onDidChange.event; this._onDidChange.event;
private readonly _onDidViewVisibilityChange = new Emitter<void>(); private readonly _onDidMaximizedNodeChange = new Emitter<void>();
readonly onDidViewVisibilityChange = this._onDidViewVisibilityChange.event;
private readonly _onDidMaximizedNodeChange =
new Emitter<MaximizedViewChanged>();
readonly onDidMaximizedNodeChange = this._onDidMaximizedNodeChange.event; readonly onDidMaximizedNodeChange = this._onDidMaximizedNodeChange.event;
public get length(): number { public get length(): number {
@ -373,15 +356,6 @@ export class Gridview implements IDisposable {
} }
} }
get margin(): number {
return this._margin;
}
set margin(value: number) {
this._margin = value;
this.root.margin = value;
}
maximizedView(): IGridView | undefined { maximizedView(): IGridView | undefined {
return this._maximizedNode?.leaf.view; return this._maximizedNode?.leaf.view;
} }
@ -406,8 +380,6 @@ export class Gridview implements IDisposable {
this.exitMaximizedView(); this.exitMaximizedView();
} }
serializeBranchNode(this.getView(), this.orientation);
const hiddenOnMaximize: LeafNode[] = []; const hiddenOnMaximize: LeafNode[] = [];
function hideAllViewsBut(parent: BranchNode, exclude: LeafNode): void { function hideAllViewsBut(parent: BranchNode, exclude: LeafNode): void {
@ -429,10 +401,7 @@ export class Gridview implements IDisposable {
hideAllViewsBut(this.root, node); hideAllViewsBut(this.root, node);
this._maximizedNode = { leaf: node, hiddenOnMaximize }; this._maximizedNode = { leaf: node, hiddenOnMaximize };
this._onDidMaximizedNodeChange.fire({ this._onDidMaximizedNodeChange.fire();
view: node.view,
isMaximized: true,
});
} }
exitMaximizedView(): void { exitMaximizedView(): void {
@ -457,68 +426,33 @@ export class Gridview implements IDisposable {
showViewsInReverseOrder(this.root); showViewsInReverseOrder(this.root);
const tmp = this._maximizedNode.leaf;
this._maximizedNode = undefined; this._maximizedNode = undefined;
this._onDidMaximizedNodeChange.fire({ this._onDidMaximizedNodeChange.fire();
view: tmp.view,
isMaximized: false,
});
} }
public serialize(): SerializedGridview<any> { public serialize(): SerializedGridview<any> {
const maximizedView = this.maximizedView();
let maxmizedViewLocation: number[] | undefined;
if (maximizedView) {
/**
* The minimum information we can get away with in order to serialize a maxmized view is it's location within the grid
* which is represented as a branch of indices
*/
maxmizedViewLocation = getGridLocation(maximizedView.element);
}
if (this.hasMaximizedView()) { if (this.hasMaximizedView()) {
/** /**
* the saved layout cannot be in its maxmized state otherwise all of the underlying * do not persist maximized view state
* view dimensions will be wrong * firstly exit any maximized views to ensure the correct dimensions are persisted
*
* To counteract this we temporaily remove the maximized view to compute the serialized output
* of the grid before adding back the maxmized view as to not alter the layout from the users
* perspective when `.toJSON()` is called
*/ */
this.exitMaximizedView(); this.exitMaximizedView();
} }
const root = serializeBranchNode(this.getView(), this.orientation); const root = serializeBranchNode(this.getView(), this.orientation);
const resullt: SerializedGridview<any> = { return {
root, root,
width: this.width, width: this.width,
height: this.height, height: this.height,
orientation: this.orientation, orientation: this.orientation,
}; };
if (maxmizedViewLocation) {
resullt.maximizedNode = {
location: maxmizedViewLocation,
};
}
if (maximizedView) {
// replace any maximzied view that was removed for serialization purposes
this.maximizeView(maximizedView);
}
return resullt;
} }
public dispose(): void { public dispose(): void {
this.disposable.dispose(); this.disposable.dispose();
this._onDidChange.dispose(); this._onDidChange.dispose();
this._onDidMaximizedNodeChange.dispose(); this._onDidMaximizedNodeChange.dispose();
this._onDidViewVisibilityChange.dispose();
this.root.dispose(); this.root.dispose();
this._maximizedNode = undefined; this._maximizedNode = undefined;
this.element.remove(); this.element.remove();
@ -532,8 +466,7 @@ export class Gridview implements IDisposable {
this.styles, this.styles,
this.root.size, this.root.size,
this.root.orthogonalSize, this.root.orthogonalSize,
this.locked, this._locked
this.margin
); );
} }
@ -551,24 +484,6 @@ export class Gridview implements IDisposable {
deserializer, deserializer,
height height
); );
/**
* The deserialied layout must be positioned through this.layout(...)
* before any maximizedNode can be positioned
*/
this.layout(json.width, json.height);
if (json.maximizedNode) {
const location = json.maximizedNode.location;
const [_, node] = this.getNode(location);
if (!(node instanceof LeafNode)) {
return;
}
this.maximizeView(node.view);
}
} }
private _deserialize( private _deserialize(
@ -612,17 +527,16 @@ export class Gridview implements IDisposable {
this.styles, this.styles,
node.size, // <- orthogonal size - flips at each depth node.size, // <- orthogonal size - flips at each depth
orthogonalSize, // <- size - flips at each depth, orthogonalSize, // <- size - flips at each depth,
this.locked, this._locked,
this.margin,
children children
); );
} else { } else {
const view = deserializer.fromJSON(node); result = new LeafNode(
if (typeof node.visible === 'boolean') { deserializer.fromJSON(node),
view.setVisible?.(node.visible); orientation,
} orthogonalSize,
node.size
result = new LeafNode(view, orientation, orthogonalSize, node.size); );
} }
return result; return result;
@ -666,8 +580,7 @@ export class Gridview implements IDisposable {
this.styles, this.styles,
this.root.orthogonalSize, this.root.orthogonalSize,
this.root.size, this.root.size,
this.locked, this._locked
this.margin
); );
if (oldRoot.children.length === 0) { if (oldRoot.children.length === 0) {
@ -773,24 +686,17 @@ export class Gridview implements IDisposable {
constructor( constructor(
readonly proportionalLayout: boolean, readonly proportionalLayout: boolean,
readonly styles: ISplitviewStyles | undefined, readonly styles: ISplitviewStyles | undefined,
orientation: Orientation, orientation: Orientation
locked?: boolean,
margin?: number
) { ) {
this.element = document.createElement('div'); this.element = document.createElement('div');
this.element.className = 'dv-grid-view'; this.element.className = 'grid-view';
this._locked = locked ?? false;
this._margin = margin ?? 0;
this.root = new BranchNode( this.root = new BranchNode(
orientation, orientation,
proportionalLayout, proportionalLayout,
styles, styles,
0, 0,
0, 0,
this.locked, this._locked
this.margin
); );
} }
@ -817,8 +723,6 @@ export class Gridview implements IDisposable {
throw new Error('Invalid from location'); throw new Error('Invalid from location');
} }
this._onDidViewVisibilityChange.fire();
parent.setChildVisible(index, visible); parent.setChildVisible(index, visible);
} }
@ -877,8 +781,7 @@ export class Gridview implements IDisposable {
this.styles, this.styles,
parent.size, parent.size,
parent.orthogonalSize, parent.orthogonalSize,
this.locked, this._locked
this.margin
); );
grandParent.addChild(newParent, parent.size, parentIndex); grandParent.addChild(newParent, parent.size, parentIndex);

View File

@ -23,6 +23,7 @@ import {
} from './gridviewPanel'; } from './gridviewPanel';
import { BaseComponentOptions, Parameters } from '../panel/types'; import { BaseComponentOptions, Parameters } from '../panel/types';
import { Orientation, Sizing } from '../splitview/splitview'; import { Orientation, Sizing } from '../splitview/splitview';
import { createComponent } from '../panel/componentFactory';
import { Emitter, Event } from '../events'; import { Emitter, Event } from '../events';
import { Position } from '../dnd/droptarget'; import { Position } from '../dnd/droptarget';
@ -48,10 +49,15 @@ export interface IGridPanelComponentView extends IGridPanelView {
init: (params: GridviewInitParameters) => void; init: (params: GridviewInitParameters) => void;
} }
export type GridviewComponentUpdateOptions = Pick<
GridviewComponentOptions,
'orientation' | 'components' | 'frameworkComponents'
>;
export interface IGridviewComponent extends IBaseGrid<GridviewPanel> { export interface IGridviewComponent extends IBaseGrid<GridviewPanel> {
readonly orientation: Orientation; readonly orientation: Orientation;
readonly onDidLayoutFromJSON: Event<void>; readonly onDidLayoutFromJSON: Event<void>;
updateOptions(options: Partial<GridviewComponentOptions>): void; updateOptions(options: Partial<GridviewComponentUpdateOptions>): void;
addPanel<T extends object = Parameters>( addPanel<T extends object = Parameters>(
options: AddComponentOptions<T> options: AddComponentOptions<T>
): IGridviewPanel; ): IGridviewPanel;
@ -113,15 +119,13 @@ export class GridviewComponent
this._deserializer = value; this._deserializer = value;
} }
constructor(container: HTMLElement, options: GridviewComponentOptions) { constructor(options: GridviewComponentOptions) {
super(container, { super({
proportionalLayout: options.proportionalLayout ?? true, parentElement: options.parentElement,
proportionalLayout: options.proportionalLayout,
orientation: options.orientation, orientation: options.orientation,
styles: options.hideBorders styles: options.styles,
? { separatorBorder: 'transparent' }
: undefined,
disableAutoResizing: options.disableAutoResizing, disableAutoResizing: options.disableAutoResizing,
className: options.className,
}); });
this._options = options; this._options = options;
@ -140,11 +144,16 @@ export class GridviewComponent
this._onDidActiveGroupChange.fire(event); this._onDidActiveGroupChange.fire(event);
}) })
); );
if (!this.options.components) {
this.options.components = {};
}
if (!this.options.frameworkComponents) {
this.options.frameworkComponents = {};
}
} }
override updateOptions(options: Partial<GridviewComponentOptions>): void { updateOptions(options: Partial<GridviewComponentUpdateOptions>): void {
super.updateOptions(options);
const hasOrientationChanged = const hasOrientationChanged =
typeof options.orientation === 'string' && typeof options.orientation === 'string' &&
this.gridview.orientation !== options.orientation; this.gridview.orientation !== options.orientation;
@ -210,11 +219,19 @@ export class GridviewComponent
this.gridview.deserialize(grid, { this.gridview.deserialize(grid, {
fromJSON: (node) => { fromJSON: (node) => {
const { data } = node; const { data } = node;
const view = createComponent(
const view = this.options.createComponent({ data.id,
id: data.id, data.component,
name: data.component, this.options.components ?? {},
}); this.options.frameworkComponents ?? {},
this.options.frameworkComponentFactory
? {
createComponent:
this.options.frameworkComponentFactory
.createComponent,
}
: undefined
);
queue.push(() => queue.push(() =>
view.init({ view.init({
@ -349,10 +366,19 @@ export class GridviewComponent
} }
} }
const view = this.options.createComponent({ const view = createComponent(
id: options.id, options.id,
name: options.component, options.component,
}); this.options.components ?? {},
this.options.frameworkComponents ?? {},
this.options.frameworkComponentFactory
? {
createComponent:
this.options.frameworkComponentFactory
.createComponent,
}
: undefined
);
view.init({ view.init({
params: options.params ?? {}, params: options.params ?? {},

View File

@ -1,5 +1,8 @@
import { PanelInitParameters } from '../panel/types'; import { PanelInitParameters } from '../panel/types';
import { IGridPanelComponentView } from './gridviewComponent'; import {
GridviewComponent,
IGridPanelComponentView,
} from './gridviewComponent';
import { FunctionOrValue } from '../types'; import { FunctionOrValue } from '../types';
import { import {
BasePanelView, BasePanelView,
@ -15,13 +18,6 @@ import { Emitter, Event } from '../events';
import { IViewSize } from './gridview'; import { IViewSize } from './gridview';
import { BaseGrid, IGridPanelView } from './baseComponentGridview'; import { BaseGrid, IGridPanelView } from './baseComponentGridview';
export interface Contraints {
minimumWidth?: number;
maximumWidth?: number;
minimumHeight?: number;
maximumHeight?: number;
}
export interface GridviewInitParameters extends PanelInitParameters { export interface GridviewInitParameters extends PanelInitParameters {
minimumWidth?: number; minimumWidth?: number;
maximumWidth?: number; maximumWidth?: number;
@ -74,38 +70,6 @@ export abstract class GridviewPanel<
} }
get minimumWidth(): number { get minimumWidth(): number {
/**
* defer to protected function to allow subclasses to override easily.
* see https://github.com/microsoft/TypeScript/issues/338
*/
return this.__minimumWidth();
}
get minimumHeight(): number {
/**
* defer to protected function to allow subclasses to override easily.
* see https://github.com/microsoft/TypeScript/issues/338
*/
return this.__minimumHeight();
}
get maximumHeight(): number {
/**
* defer to protected function to allow subclasses to override easily.
* see https://github.com/microsoft/TypeScript/issues/338
*/
return this.__maximumHeight();
}
get maximumWidth(): number {
/**
* defer to protected function to allow subclasses to override easily.
* see https://github.com/microsoft/TypeScript/issues/338
*/
return this.__maximumWidth();
}
protected __minimumWidth(): number {
const width = const width =
typeof this._minimumWidth === 'function' typeof this._minimumWidth === 'function'
? this._minimumWidth() ? this._minimumWidth()
@ -119,21 +83,7 @@ export abstract class GridviewPanel<
return width; return width;
} }
protected __maximumWidth(): number { get minimumHeight(): number {
const width =
typeof this._maximumWidth === 'function'
? this._maximumWidth()
: this._maximumWidth;
if (width !== this._evaluatedMaximumWidth) {
this._evaluatedMaximumWidth = width;
this.updateConstraints();
}
return width;
}
protected __minimumHeight(): number {
const height = const height =
typeof this._minimumHeight === 'function' typeof this._minimumHeight === 'function'
? this._minimumHeight() ? this._minimumHeight()
@ -147,7 +97,7 @@ export abstract class GridviewPanel<
return height; return height;
} }
protected __maximumHeight(): number { get maximumHeight(): number {
const height = const height =
typeof this._maximumHeight === 'function' typeof this._maximumHeight === 'function'
? this._maximumHeight() ? this._maximumHeight()
@ -161,12 +111,22 @@ export abstract class GridviewPanel<
return height; return height;
} }
get isActive(): boolean { get maximumWidth(): number {
return this.api.isActive; const width =
typeof this._maximumWidth === 'function'
? this._maximumWidth()
: this._maximumWidth;
if (width !== this._evaluatedMaximumWidth) {
this._evaluatedMaximumWidth = width;
this.updateConstraints();
}
return width;
} }
get isVisible(): boolean { get isActive(): boolean {
return this.api.isVisible; return this.api.isActive;
} }
constructor( constructor(

View File

@ -17,7 +17,7 @@ export class LeafNode implements IView {
this._onDidChange.event; this._onDidChange.event;
private _size: number; private _size: number;
private _orthogonalSize: number; private _orthogonalSize: number;
private readonly _disposable: IDisposable; private _disposable: IDisposable;
private get minimumWidth(): number { private get minimumWidth(): number {
return this.view.minimumWidth; return this.view.minimumWidth;

View File

@ -1,34 +1,21 @@
import { GridviewPanel } from './gridviewPanel'; import { GridviewPanel } from './gridviewPanel';
import { Orientation } from '../splitview/splitview'; import { ISplitviewStyles, Orientation } from '../splitview/splitview';
import { CreateComponentOptions } from '../dockview/options'; import {
ComponentConstructor,
FrameworkFactory,
} from '../panel/componentFactory';
export interface GridviewOptions { export interface GridviewComponentOptions {
disableAutoResizing?: boolean; disableAutoResizing?: boolean;
proportionalLayout?: boolean; proportionalLayout: boolean;
orientation: Orientation; orientation: Orientation;
className?: string; components?: {
hideBorders?: boolean; [componentName: string]: ComponentConstructor<GridviewPanel>;
}
export interface GridviewFrameworkOptions {
createComponent: (options: CreateComponentOptions) => GridviewPanel;
}
export type GridviewComponentOptions = GridviewOptions &
GridviewFrameworkOptions;
export const PROPERTY_KEYS_GRIDVIEW: (keyof GridviewOptions)[] = (() => {
/**
* by readong the keys from an empty value object TypeScript will error
* when we add or remove new properties to `DockviewOptions`
*/
const properties: Record<keyof GridviewOptions, undefined> = {
disableAutoResizing: undefined,
proportionalLayout: undefined,
orientation: undefined,
hideBorders: undefined,
className: undefined,
}; };
frameworkComponents?: {
return Object.keys(properties) as (keyof GridviewOptions)[]; [componentName: string]: any;
})(); };
frameworkComponentFactory?: FrameworkFactory<GridviewPanel>;
styles?: ISplitviewStyles;
parentElement: HTMLElement;
}

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