Merge branch 'master' of https://github.com/mathuo/dockview into 849-improved-dnd-overlay-model

This commit is contained in:
mathuo 2025-02-23 20:09:03 +00:00
commit 3530c9896e
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
24 changed files with 474 additions and 109 deletions

View File

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

View File

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

View File

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

View File

@ -1102,7 +1102,9 @@ describe('dockviewComponent', () => {
disposable.dispose();
});
test('events flow', () => {
test('events flow', async () => {
window.open = () => setupMockWindow();
dockview.layout(1000, 1000);
let events: {
@ -1295,7 +1297,42 @@ describe('dockviewComponent', () => {
expect(dockview.size).toBe(0);
expect(dockview.totalPanels).toBe(0);
events = [];
const panel8 = dockview.addPanel({
id: 'panel8',
component: 'default',
});
const panel9 = dockview.addPanel({
id: 'panel9',
component: 'default',
floating: true,
});
const panel10 = dockview.addPanel({
id: 'panel10',
component: 'default',
});
expect(await dockview.addPopoutGroup(panel10)).toBeTruthy();
expect(events).toEqual([
{ type: 'ADD_GROUP', group: panel8.group },
{ type: 'ADD_PANEL', panel: panel8 },
{ type: 'ACTIVE_GROUP', group: panel8.group },
{ type: 'ACTIVE_PANEL', panel: panel8 },
{ type: 'ADD_GROUP', group: panel9.group },
{ type: 'ADD_PANEL', panel: panel9 },
{ type: 'ACTIVE_GROUP', group: panel9.group },
{ type: 'ACTIVE_PANEL', panel: panel9 },
{ type: 'ADD_PANEL', panel: panel10 },
{ type: 'ACTIVE_PANEL', panel: panel10 },
{ type: 'ADD_GROUP', group: panel10.group },
]);
events = [];
disposable.dispose();
expect(events.length).toBe(0);
});
test('that removing a panel from a group reflects in the dockviewcomponent when searching for a panel', () => {
@ -5696,6 +5733,42 @@ describe('dockviewComponent', () => {
},
]);
});
test('dispose of dockview instance when popup is open', async () => {
const container = document.createElement('div');
window.open = () => setupMockWindow();
const dockview = new DockviewComponent(container, {
createComponent(options) {
switch (options.name) {
case 'default':
return new PanelContentPartTest(
options.id,
options.name
);
default:
throw new Error(`unsupported`);
}
},
});
dockview.layout(1000, 500);
dockview.addPanel({
id: 'panel_1',
component: 'default',
});
const panel2 = dockview.addPanel({
id: 'panel_2',
component: 'default',
});
expect(await dockview.addPopoutGroup(panel2.group)).toBeTruthy();
dockview.dispose();
});
});
describe('maximized group', () => {

View File

@ -29,12 +29,6 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer {
this._element.appendChild(this._content);
this._element.appendChild(this.action);
this.addDisposables(
addDisposableListener(this.action, 'pointerdown', (ev) => {
ev.preventDefault();
})
);
this.render();
}

View File

@ -50,8 +50,8 @@ export class Tab extends CompositeDisposable {
private readonly dropTarget: Droptarget;
private content: ITabRenderer | undefined = undefined;
private readonly _onChanged = new Emitter<MouseEvent>();
readonly onChanged: Event<MouseEvent> = this._onChanged.event;
private readonly _onPointDown = new Emitter<MouseEvent>();
readonly onPointerDown: Event<MouseEvent> = this._onPointDown.event;
private readonly _onDropped = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDropped.event;
@ -112,7 +112,7 @@ export class Tab extends CompositeDisposable {
this.onWillShowOverlay = this.dropTarget.onWillShowOverlay;
this.addDisposables(
this._onChanged,
this._onPointDown,
this._onDropped,
this._onDragStart,
dragHandler.onDragStart((event) => {
@ -137,11 +137,7 @@ export class Tab extends CompositeDisposable {
}),
dragHandler,
addDisposableListener(this._element, 'pointerdown', (event) => {
if (event.defaultPrevented) {
return;
}
this._onChanged.fire(event);
this._onPointDown.fire(event);
}),
this.dropTarget.onDrop((event) => {
this._onDropped.fire(event);

View File

@ -286,6 +286,10 @@ export class TabsContainer
const tabToRemove = this.tabs.splice(index, 1)[0];
if (!tabToRemove) {
throw new Error(`dockview: Tab not found`);
}
const { value, disposable } = tabToRemove;
disposable.dispose();
@ -316,7 +320,11 @@ export class TabsContainer
tab.onDragStart((event) => {
this._onTabDragStart.fire({ nativeEvent: event, panel });
}),
tab.onChanged((event) => {
tab.onPointerDown((event) => {
if (event.defaultPrevented) {
return;
}
const isFloatingGroupsEnabled =
!this.accessor.options.disableFloatingGroups;
@ -345,14 +353,12 @@ export class TabsContainer
return;
}
const isLeftClick = event.button === 0;
if (!isLeftClick || event.defaultPrevented) {
return;
}
if (this.group.activePanel !== panel) {
this.group.model.openPanel(panel);
switch (event.button) {
case 0: // left click or touch
if (this.group.activePanel !== panel) {
this.group.model.openPanel(panel);
}
break;
}
}),
tab.onDrop((event) => {

View File

@ -853,6 +853,10 @@ export class DockviewComponent
),
overlayRenderContainer,
Disposable.from(() => {
if (this.isDisposed) {
return; // cleanup may run after instance is disposed
}
if (
isGroupAddedToDom &&
this.getPanel(referenceGroup.id)

View File

@ -118,15 +118,16 @@
touch-action: none;
background-color: var(--dv-sash-color, transparent);
&:not(.disabled):active {
transition: background-color 0.1s ease-in-out;
background-color: var(--dv-active-sash-color, transparent);
}
&:not(.disabled):active,
&:not(.disabled):hover {
background-color: var(--dv-active-sash-color, transparent);
transition: background-color 0.1s ease-in-out;
transition-delay: 0.5s;
transition-property: background-color;
transition-timing-function: ease-in-out;
transition-duration: var(
--dv-active-sash-transition-duration,
0.1s
);
transition-delay: var(--dv-active-sash-transition-delay, 0.5s);
}
}
}

View File

@ -19,6 +19,8 @@
--dv-tab-margin: 0;
--dv-sash-color: transparent;
--dv-active-sash-color: transparent;
--dv-active-sash-transition-duration: 0.1s;
--dv-active-sash-transition-delay: 0.5s;
}
@mixin dockview-theme-dark-mixin {

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "dockview-vue",
"version": "3.0.2",
"version": "3.2.0",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews",
"keywords": [
"splitview",
@ -52,7 +52,7 @@
"test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview-vue --coverage"
},
"dependencies": {
"dockview-core": "^3.0.2"
"dockview-core": "^3.2.0"
},
"peerDependencies": {
"vue": "^3.4.0"

View File

@ -1,6 +1,6 @@
{
"name": "dockview",
"version": "3.0.2",
"version": "3.2.0",
"description": "Zero dependency layout manager supporting tabs, grids and splitviews",
"keywords": [
"splitview",
@ -54,7 +54,7 @@
"test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview --coverage"
},
"dependencies": {
"dockview-core": "^3.0.2"
"dockview-core": "^3.2.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"

View File

@ -10,7 +10,9 @@ import { Disposable } from 'dockview-core/dist/cjs/lifecycle';
describe('defaultTab', () => {
test('has close button by default', async () => {
const api = fromPartial<DockviewPanelApi>({
onDidTitleChange: jest.fn().mockImplementation(() => Disposable.NONE),
onDidTitleChange: jest
.fn()
.mockImplementation(() => Disposable.NONE),
});
const containerApi = fromPartial<DockviewApi>({});
const params = {};
@ -30,7 +32,9 @@ describe('defaultTab', () => {
test('that title is displayed', async () => {
const api = fromPartial<DockviewPanelApi>({
title: 'test_title',
onDidTitleChange: jest.fn().mockImplementation(() => Disposable.NONE),
onDidTitleChange: jest
.fn()
.mockImplementation(() => Disposable.NONE),
});
const containerApi = fromPartial<DockviewApi>({});
const params = {};
@ -84,7 +88,9 @@ describe('defaultTab', () => {
test('has no close button when hideClose=true', async () => {
const api = fromPartial<DockviewPanelApi>({
onDidTitleChange: jest.fn().mockImplementation(() => Disposable.NONE),
onDidTitleChange: jest
.fn()
.mockImplementation(() => Disposable.NONE),
});
const containerApi = fromPartial<DockviewApi>({});
const params = {};
@ -105,7 +111,9 @@ describe('defaultTab', () => {
test('that settings closeActionOverride skips api.close()', async () => {
const api = fromPartial<DockviewPanelApi>({
close: jest.fn(),
onDidTitleChange: jest.fn().mockImplementation(() => Disposable.NONE),
onDidTitleChange: jest
.fn()
.mockImplementation(() => Disposable.NONE),
});
const containerApi = fromPartial<DockviewApi>({});
const params = {};
@ -134,7 +142,9 @@ describe('defaultTab', () => {
test('that clicking close calls api.close()', async () => {
const api = fromPartial<DockviewPanelApi>({
close: jest.fn(),
onDidTitleChange: jest.fn().mockImplementation(() => Disposable.NONE),
onDidTitleChange: jest
.fn()
.mockImplementation(() => Disposable.NONE),
});
const containerApi = fromPartial<DockviewApi>({});
const params = {};
@ -158,7 +168,9 @@ describe('defaultTab', () => {
test('has close button when hideClose=false', async () => {
const api = fromPartial<DockviewPanelApi>({
onDidTitleChange: jest.fn().mockImplementation(() => Disposable.NONE),
onDidTitleChange: jest
.fn()
.mockImplementation(() => Disposable.NONE),
});
const containerApi = fromPartial<DockviewApi>({});
const params = {};
@ -175,32 +187,4 @@ describe('defaultTab', () => {
const element = await screen.getByTestId('dockview-dv-default-tab');
expect(element.querySelector('.dv-default-tab-action')).toBeTruthy();
});
test('that pointerDown on close button prevents panel becoming active', async () => {
const api = fromPartial<DockviewPanelApi>({
setActive: jest.fn(),
onDidTitleChange: jest.fn().mockImplementation(() => Disposable.NONE),
});
const containerApi = fromPartial<DockviewApi>({});
const params = {};
render(
<DockviewDefaultTab
api={api}
containerApi={containerApi}
params={params}
/>
);
const element = await screen.getByTestId('dockview-dv-default-tab');
const btn = element.querySelector(
'.dv-default-tab-action'
) as HTMLElement;
fireEvent.pointerDown(btn);
expect(api.setActive).toHaveBeenCalledTimes(0);
fireEvent.click(element);
expect(api.setActive).toHaveBeenCalledTimes(1);
});
});

View File

@ -32,10 +32,15 @@ export const DockviewDefaultTab: React.FunctionComponent<
params: _params,
hideClose,
closeActionOverride,
onPointerDown,
onPointerUp,
onPointerLeave,
...rest
}) => {
const title = useTitle(api);
const isMiddleMouseButton = React.useRef<boolean>(false);
const onClose = React.useCallback(
(event: React.MouseEvent<HTMLSpanElement>) => {
event.preventDefault();
@ -49,37 +54,52 @@ export const DockviewDefaultTab: React.FunctionComponent<
[api, closeActionOverride]
);
const onPointerDown = React.useCallback((e: React.MouseEvent) => {
e.preventDefault();
const onBtnPointerDown = React.useCallback((event: React.MouseEvent) => {
event.preventDefault();
}, []);
const onClick = React.useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
if (event.defaultPrevented) {
return;
}
api.setActive();
if (rest.onClick) {
rest.onClick(event);
}
const _onPointerDown = React.useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
isMiddleMouseButton.current = event.button === 1;
onPointerDown?.(event);
},
[api, rest.onClick]
[onPointerDown]
);
const _onPointerUp = React.useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
if (isMiddleMouseButton && event.button === 1 && !hideClose) {
isMiddleMouseButton.current = false;
onClose(event);
}
onPointerUp?.(event);
},
[onPointerUp, onClose, hideClose]
);
const _onPointerLeave = React.useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
isMiddleMouseButton.current = false;
onPointerLeave?.(event);
},
[onPointerLeave]
);
return (
<div
data-testid="dockview-dv-default-tab"
{...rest}
onClick={onClick}
onPointerDown={_onPointerDown}
onPointerUp={_onPointerUp}
onPointerLeave={_onPointerLeave}
className="dv-default-tab"
>
<span className="dv-default-tab-content">{title}</span>
{!hideClose && (
<div
className="dv-default-tab-action"
onPointerDown={onPointerDown}
onPointerDown={onBtnPointerDown}
onClick={onClose}
>
<CloseButton />

View File

@ -0,0 +1,22 @@
---
slug: dockview-3.1.0-release
title: Dockview 3.1.0
tags: [release]
---
# Release Notes
Please reference docs @ [dockview.dev](https://dockview.dev).
## 🚀 Features
- Close tab with middle mouse button [#847](https://github.com/mathuo/dockview/pull/847)
## 🛠 Miscs
- Bug: Fix crash on navigation with open popout group [#835](https://github.com/mathuo/dockview/pull/848) [#845](https://github.com/mathuo/dockview/pull/845)
- Bug: Subscribe to `onDidAcitvePanelChange` immediately, rather than deferred to `queueMicrotask` [#843](https://github.com/mathuo/dockview/pull/843)
- Bug: Minor theme fixup [#831](https://github.com/mathuo/dockview/pull/831)
## 🔥 Breaking changes

View File

@ -0,0 +1,18 @@
---
slug: dockview-3.1.1-release
title: Dockview 3.1.1
tags: [release]
---
# Release Notes
Please reference docs @ [dockview.dev](https://dockview.dev).
## 🚀 Features
## 🛠 Miscs
- Bug: Fix Middle mouse button to close tab [#835](https://github.com/mathuo/dockview/issues/853)
## 🔥 Breaking changes

View File

@ -0,0 +1,18 @@
---
slug: dockview-3.2.0-release
title: Dockview 3.2.0
tags: [release]
---
# Release Notes
Please reference docs @ [dockview.dev](https://dockview.dev).
## 🚀 Features
- Add CSS properties `--dv-active-sash-transition-duration` and `--dv-active-sash-transition-delay` [#835](https://github.com/mathuo/dockview/issues/859)
## 🛠 Miscs
## 🔥 Breaking changes

View File

@ -184,17 +184,8 @@ const config = {
label: 'API',
},
{ to: '/blog', label: 'Blog', position: 'left' },
{ href: '/templates', target:"_blank", label: 'Examples', position: 'left' },
{ to: '/demo', label: 'Demo', position: 'left' },
// {
// to: 'https://dockview.dev/typedocs',
// label: 'TSDoc',
// position: 'left',
// },
// {
// type: 'docsVersionDropdown',
// position: 'right',
// },
{
href: 'https://github.com/mathuo/dockview',
position: 'right',

View File

@ -1,6 +1,6 @@
{
"name": "dockview-docs",
"version": "3.0.2",
"version": "3.2.0",
"private": true,
"scripts": {
"build": "npm run build-templates && docusaurus build",
@ -38,7 +38,7 @@
"ag-grid-react": "^31.0.2",
"axios": "^1.6.3",
"clsx": "^2.1.0",
"dockview": "^3.0.2",
"dockview": "^3.2.0",
"prism-react-renderer": "^2.3.1",
"react-dnd": "^16.0.1",
"react-laag": "^2.0.5",

View File

@ -1,7 +1,6 @@
import fs from 'fs-extra';
import * as path from 'path';
import { argv } from 'process';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
@ -86,7 +85,8 @@ function createIndexHTML(options) {
.map(([key, value]) => `"${key}": "${value}"`)
.join(',\n')}`
)
.replace('{{app}}', options.app);
.replace('{{app}}', options.app)
.replace('{{githubLink}}', options.githubUrl)
}
const input_dir = path.join(__dirname, '../templates');
@ -97,6 +97,8 @@ const FRAMEWORKS = ['react', 'vue', 'typescript'];
const list = [];
const githubUrl = "https://github.com/mathuo/dockview/tree/master/packages/docs/templates"
for (const component of COMPONENTS) {
const componentDir = path.join(input_dir, component);
@ -115,6 +117,9 @@ for (const component of COMPONENTS) {
path.join(componentDir, folder, framework, 'src'),
path.join(output, component, folder, framework, 'src')
);
const templateGithubUrl = `${githubUrl}/${component}/${folder}/${framework}/src`
const template = createIndexHTML({
title: `Dockview | ${folder} ${framework}`,
app:
@ -127,6 +132,7 @@ for (const component of COMPONENTS) {
USE_LOCAL_CDN ? 'local' : 'remote'
],
},
githubUrl: templateGithubUrl
});
fs.writeFileSync(
path.join(output, component, folder, framework, 'index.html'),

View File

@ -12,7 +12,7 @@
<style media="only screen">
html,
body,
#app {
#root {
height: 100%;
width: 100%;
margin: 0;
@ -22,6 +22,26 @@
BlinkMacSystemFont, Segoe UI, Roboto;
}
#header {
height: 25px;
display: flex;
justify-content: flex-end;
align-items: center;
}
#header-btn {
height: 22px;
}
#gh-logo {
height: 22px;
width: 22px;
}
#app {
height: calc(100% - 25px);
}
html {
position: absolute;
top: 0;
@ -31,7 +51,7 @@
}
body {
padding: 16px;
padding: 8px;
overflow: auto;
}
</style>
@ -62,9 +82,18 @@
</head>
<body>
<div id="app">
<script type="systemjs-module" src="import:{{app}}"></script>
<div id="root">
<div id="header">
<a target="_blank" rel="noopener noreferrer" href="{{githubLink}}">
<button id="header-btn">
View Source
</button>
</a>
<img id="gh-logo" src="https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png"/>
</div>
<div id="app"></div>
</div>
<script type="systemjs-module" src="import:{{app}}"></script>
<object
id="loading-spinner"
style="

View File

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

View File

@ -0,0 +1,178 @@
import 'dockview-core/dist/styles/dockview.css';
import {
createDockview,
DockviewGroupPanel,
GroupPanelPartInitParameters,
IContentRenderer,
IGroupHeaderProps,
IHeaderActionsRenderer,
} from 'dockview-core';
import './index.css';
class Panel implements IContentRenderer {
private readonly _element: HTMLElement;
get element(): HTMLElement {
return this._element;
}
constructor() {
this._element = document.createElement('div');
this._element.style.display = 'flex';
this._element.style.justifyContent = 'center';
this._element.style.alignItems = 'center';
this._element.style.color = 'gray';
this._element.style.height = '100%';
}
init(parameters: GroupPanelPartInitParameters): void {
//
}
}
class PrefixHeader implements IHeaderActionsRenderer {
private readonly _element: HTMLElement;
get element(): HTMLElement {
return this._element;
}
constructor(group: DockviewGroupPanel) {
this._element = document.createElement('div');
this._element.className = 'dockview-groupcontrol-demo';
this._element.innerText = '🌲';
}
init(parameters: IGroupHeaderProps): void {
//
}
dispose(): void {
//
}
}
class RightHeaderActions implements IHeaderActionsRenderer {
private readonly _element: HTMLElement;
private readonly _disposables: (() => void)[] = [];
get element(): HTMLElement {
return this._element;
}
constructor(group: DockviewGroupPanel) {
this._element = document.createElement('div');
this._element.className = 'dockview-groupcontrol-demo';
}
init(parameters: IGroupHeaderProps): void {
const group = parameters.group;
const span = document.createElement('span');
span.className = 'dockview-groupcontrol-demo-group-active';
this._element.appendChild(span);
const d1 = group.api.onDidActiveChange(() => {
span.style.background = group.api.isActive ? 'green' : 'red';
span.innerText = `${
group.api.isActive ? 'Group Active' : 'Group Inactive'
}`;
});
span.style.background = group.api.isActive ? 'green' : 'red';
span.innerText = `${
group.api.isActive ? 'Group Active' : 'Group Inactive'
}`;
this._disposables.push(() => d1.dispose());
}
dispose(): void {
this._disposables.forEach((dispose) => dispose());
}
}
class LeftHeaderActions implements IHeaderActionsRenderer {
private readonly _element: HTMLElement;
private readonly _disposables: (() => void)[] = [];
get element(): HTMLElement {
return this._element;
}
constructor(group: DockviewGroupPanel) {
console.log('group', group);
this._element = document.createElement('div');
this._element.className = 'dockview-groupcontrol-demo';
}
init(parameters: IGroupHeaderProps): void {
const group = parameters.group;
const span = document.createElement('span');
span.className = 'dockview-groupcontrol-demo-active-panel';
this._element.appendChild(span);
const d1 = group.api.onDidActivePanelChange((event) => {
console.log('event', event);
span.innerText = `activePanel: ${event.panel?.id || 'null'}`;
});
console.log('group.activePanel', group.activePanel);
span.innerText = `activePanel: ${group.activePanel?.id || 'null'}`;
this._disposables.push(() => d1.dispose());
}
dispose(): void {
this._disposables.forEach((dispose) => dispose());
}
}
const api = createDockview(document.getElementById('app'), {
className: 'dockview-theme-abyss',
createComponent: (options): IContentRenderer => {
switch (options.name) {
case 'default':
return new Panel();
default:
throw new Error('Panel not found');
}
},
createPrefixHeaderActionComponent: (group): IHeaderActionsRenderer => {
return new PrefixHeader(group);
},
createLeftHeaderActionComponent: (group): IHeaderActionsRenderer => {
return new LeftHeaderActions(group);
},
createRightHeaderActionComponent: (group): IHeaderActionsRenderer => {
return new RightHeaderActions(group);
},
});
api.addPanel({
id: 'panel_1',
component: 'default',
title: 'Panel 1',
});
api.addPanel({
id: 'panel_2',
component: 'default',
title: 'Panel 2',
position: {
direction: 'right',
},
});
api.addPanel({
id: 'panel_3',
component: 'default',
title: 'Panel 3',
position: {
direction: 'below',
},
});