Merge branch 'master' of https://github.com/mathuo/dockview into 610-feature-request-dropdown-menu-to-handle-overflow-tabs-4

This commit is contained in:
mathuo 2025-02-23 20:19:12 +00:00
commit 5754ddc3f4
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
67 changed files with 1745 additions and 464 deletions

8
SECURITY.md Normal file
View File

@ -0,0 +1,8 @@
# 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/*"
],
"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

@ -16,10 +16,10 @@ describe('droptarget', () => {
beforeEach(() => {
element = document.createElement('div');
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 200);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 200);
});
test('that dragover events are marked', () => {

View File

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

View File

@ -42,16 +42,16 @@ describe('tabsContainer', () => {
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0);
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -73,15 +73,14 @@ describe('tabsContainer', () => {
options: {},
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const dropTargetContainer = document.createElement('div');
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
// dropTargetContainer: new DropTargetAnchorContainer(
// dropTargetContainer
// ),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
@ -97,16 +96,16 @@ describe('tabsContainer', () => {
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0);
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -129,6 +128,10 @@ describe('tabsContainer', () => {
expect(
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(1);
// expect(
// dropTargetContainer.getElementsByClassName('dv-drop-target-anchor')
// .length
// ).toBe(1);
});
test('that dropping over the empty space should render a drop target', () => {
@ -166,16 +169,16 @@ describe('tabsContainer', () => {
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0);
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -229,16 +232,16 @@ describe('tabsContainer', () => {
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0);
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);
@ -291,16 +294,16 @@ describe('tabsContainer', () => {
const emptySpace = cut.element
.getElementsByClassName('dv-void-container')
.item(0);
.item(0) as HTMLElement;
if (!emptySpace!) {
fail('element not found');
}
jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation(
jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation(
() => 100
);

View File

@ -133,11 +133,11 @@ describe('dockviewComponent', () => {
},
className: 'test-a test-b',
});
expect(dockview.element.className).toBe('test-a test-b');
expect(dockview.element.className).toBe('test-a test-b dockview-theme-abyss');
dockview.updateOptions({ className: 'test-b test-c' });
expect(dockview.element.className).toBe('test-b test-c');
expect(dockview.element.className).toBe('dockview-theme-abyss test-b test-c');
});
describe('memory leakage', () => {
@ -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', () => {
@ -3339,10 +3376,10 @@ describe('dockviewComponent', () => {
position: { direction: 'right' },
});
Object.defineProperty(dockview.element, 'clientWidth', {
Object.defineProperty(dockview.element, 'offsetWidth', {
get: () => 100,
});
Object.defineProperty(dockview.element, 'clientHeight', {
Object.defineProperty(dockview.element, 'offsetHeight', {
get: () => 100,
});
@ -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', () => {
@ -6652,36 +6725,4 @@ describe('dockviewComponent', () => {
expect(api.panels.length).toBe(3);
expect(api.groups.length).toBe(3);
});
describe('updateOptions', () => {
test('gap', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(container, {
createComponent(options) {
switch (options.name) {
case 'default':
return new PanelContentPartTest(
options.id,
options.name
);
default:
throw new Error(`unsupported`);
}
},
gap: 6,
});
expect(dockview.gap).toBe(6);
dockview.updateOptions({ gap: 10 });
expect(dockview.gap).toBe(10);
dockview.updateOptions({});
expect(dockview.gap).toBe(10);
dockview.updateOptions({ gap: 15 });
expect(dockview.gap).toBe(15);
});
});
});

View File

@ -2,10 +2,12 @@ import { DockviewComponent } from '../../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { fromPartial } from '@total-typescript/shoehorn';
import { GroupOptions } from '../../dockview/dockviewGroupPanelModel';
import { DockviewPanel } from '../../dockview/dockviewPanel';
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', () => {
@ -24,6 +26,50 @@ describe('dockviewGroupPanel', () => {
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(),
});
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(),

View File

@ -684,12 +684,12 @@ describe('dockviewGroupPanelModel', () => {
const element = container
.getElementsByClassName('dv-content-container')
.item(0)!;
.item(0)! as HTMLElement;
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
fireEvent.dragEnter(element);
fireEvent.dragOver(element);
@ -744,12 +744,12 @@ describe('dockviewGroupPanelModel', () => {
const element = container
.getElementsByClassName('dv-content-container')
.item(0)!;
.item(0)! as HTMLElement;
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
function run(value: number) {
fireEvent.dragEnter(element);
@ -792,7 +792,7 @@ describe('dockviewGroupPanelModel', () => {
fireEvent.dragEnd(element);
});
test('that should not show drop target if dropping on self', () => {
test('that should show drop target if dropping on self', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {},
@ -806,15 +806,9 @@ describe('dockviewGroupPanelModel', () => {
),
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
@ -842,12 +836,12 @@ describe('dockviewGroupPanelModel', () => {
const element = container
.getElementsByClassName('dv-content-container')
.item(0)!;
.item(0)! as HTMLElement;
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')],
@ -861,10 +855,10 @@ describe('dockviewGroupPanelModel', () => {
expect(
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
).toBe(1);
});
test('that should not allow drop when dropping on self for same component id', () => {
test('that should allow drop when dropping on self for same component id', () => {
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {},
@ -915,12 +909,12 @@ describe('dockviewGroupPanelModel', () => {
const element = container
.getElementsByClassName('dv-content-container')
.item(0)!;
.item(0) as HTMLElement;
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')],
@ -934,7 +928,7 @@ describe('dockviewGroupPanelModel', () => {
expect(
element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
).toBe(1);
});
test('that should not allow drop when not dropping for different component id', () => {
@ -988,12 +982,12 @@ describe('dockviewGroupPanelModel', () => {
const element = container
.getElementsByClassName('dv-content-container')
.item(0)!;
.item(0) as HTMLElement;
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
jest.spyOn(element, 'offsetHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100);
jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100);
LocalSelectionTransfer.getInstance().setData(
[new PanelTransfer('anothercomponentid', 'groupviewid', 'panel1')],

View File

@ -629,10 +629,20 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.totalPanels;
}
/**
* @deprecated dockview: dockviewComponent.gap has been deprecated. Use `theme` instead. This will be removed in a future version.
*/
get gap(): number {
return this.component.gap;
}
/**
* @deprecated dockview: dockviewComponent.setGap has been deprecated. Use `theme` instead. This will be removed in a future version.
*/
setGap(gap: number | undefined): void {
this.component.updateOptions({ gap: gap });
}
/**
* Invoked when the active group changes. May be undefined if no group is active.
*/
@ -914,10 +924,6 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.addPopoutGroup(item, options);
}
setGap(gap: number | undefined): void {
this.component.updateOptions({ gap });
}
updateOptions(options: Partial<DockviewComponentOptions>) {
this.component.updateOptions(options);
}

View File

@ -6,7 +6,6 @@ import {
DockviewGroupLocation,
} from '../dockview/dockviewGroupPanelModel';
import { Emitter, Event } from '../events';
import { MutableDisposable } from '../lifecycle';
import { GridviewPanelApi, GridviewPanelApiImpl } from './gridviewPanelApi';
export interface DockviewGroupMoveParams {
@ -41,8 +40,6 @@ const NOT_INITIALIZED_MESSAGE =
'dockview: DockviewGroupPanelApiImpl not initialized';
export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
private readonly _mutableDisposable = new MutableDisposable();
private _group: DockviewGroupPanel | undefined;
readonly _onDidLocationChange =
@ -50,8 +47,7 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
readonly onDidLocationChange: Event<DockviewGroupPanelFloatingChangeEvent> =
this._onDidLocationChange.event;
private readonly _onDidActivePanelChange =
new Emitter<DockviewGroupChangeEvent>();
readonly _onDidActivePanelChange = new Emitter<DockviewGroupChangeEvent>();
readonly onDidActivePanelChange = this._onDidActivePanelChange.event;
get location(): DockviewGroupLocation {
@ -66,8 +62,7 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
this.addDisposables(
this._onDidLocationChange,
this._onDidActivePanelChange,
this._mutableDisposable
this._onDidActivePanelChange
);
}
@ -140,21 +135,6 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
}
initialize(group: DockviewGroupPanel): void {
/**
* TODO: Annoying initialization order caveat, find a better way to initialize and avoid needing null checks
*
* 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.
*/
this._group = group;
queueMicrotask(() => {
this._mutableDisposable.value =
this._group!.model.onDidActivePanelChange((event) => {
this._onDidActivePanelChange.fire(event);
});
});
}
}

View File

@ -67,7 +67,7 @@ export abstract class DragHandler extends CompositeDisposable {
* 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
* dnd logic. You can see the code at
* https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542
P * https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542
*/
event.dataTransfer.setData('text/plain', '');
}
@ -75,7 +75,9 @@ export abstract class DragHandler extends CompositeDisposable {
}),
addDisposableListener(this.el, 'dragend', () => {
this.pointerEventsDisposable.dispose();
this.dataDisposable.dispose();
setTimeout(() => {
this.dataDisposable.dispose(); // allow the data to be read by other handlers before disposing
}, 0);
})
);
}

View File

@ -0,0 +1,23 @@
.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

@ -0,0 +1,102 @@
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,5 +1,6 @@
.dv-drop-target {
position: relative;
--dv-transition-duration: 70ms;
> .dv-drop-target-dropzone {
position: absolute;
@ -15,10 +16,13 @@
box-sizing: border-box;
height: 100%;
width: 100%;
border: var(--dv-drag-over-border);
background-color: var(--dv-drag-over-background-color);
transition: top 70ms ease-out, left 70ms ease-out,
width 70ms ease-out, height 70ms ease-out,
opacity 0.15s ease-out;
transition: 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,
opacity var(--dv-transition-duration) ease-out;
will-change: transform;
pointer-events: none;

View File

@ -93,10 +93,26 @@ const DEFAULT_SIZE: MeasuredValue = {
const SMALL_WIDTH_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 {
canDisplayOverlay: CanDisplayOverlay;
acceptedTargetZones: Position[];
overlayModel?: DroptargetOverlayModel;
getOverrideTarget?: () => DropTargetTargetModel | undefined;
className?: string;
getOverlayOutline?: () => HTMLElement | null;
}
export class Droptarget extends CompositeDisposable {
@ -116,6 +132,18 @@ export class Droptarget extends CompositeDisposable {
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 {
return this._state;
}
@ -126,21 +154,35 @@ export class Droptarget extends CompositeDisposable {
) {
super();
this._disabled = false;
// use a set to take advantage of #<set>.has
this._acceptedTargetZonesSet = new Set(
this.options.acceptedTargetZones
);
this.dnd = new DragAndDropObserver(this.element, {
onDragEnter: () => undefined,
onDragEnter: () => {
this.options.getOverrideTarget?.()?.getElements();
},
onDragOver: (e) => {
Droptarget.ACTUAL_TARGET = this;
const overrideTraget = this.options.getOverrideTarget?.();
if (this._acceptedTargetZonesSet.size === 0) {
if (overrideTraget) {
return;
}
this.removeDropTarget();
return;
}
const width = this.element.clientWidth;
const height = this.element.clientHeight;
const target =
this.options.getOverlayOutline?.() ?? this.element;
const width = target.offsetWidth;
const height = target.offsetHeight;
if (width === 0 || height === 0) {
return; // avoid div!0
@ -149,8 +191,8 @@ export class Droptarget extends CompositeDisposable {
const rect = (
e.currentTarget as HTMLElement
).getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const x = (e.clientX ?? 0) - rect.left;
const y = (e.clientY ?? 0) - rect.top;
const quadrant = this.calculateQuadrant(
this._acceptedTargetZonesSet,
@ -172,6 +214,9 @@ export class Droptarget extends CompositeDisposable {
}
if (!this.options.canDisplayOverlay(e, quadrant)) {
if (overrideTraget) {
return;
}
this.removeDropTarget();
return;
}
@ -194,7 +239,9 @@ export class Droptarget extends CompositeDisposable {
this.markAsUsed(e);
if (!this.targetElement) {
if (overrideTraget) {
//
} else if (!this.targetElement) {
this.targetElement = document.createElement('div');
this.targetElement.className = 'dv-drop-target-dropzone';
this.overlayElement = document.createElement('div');
@ -202,8 +249,16 @@ export class Droptarget extends CompositeDisposable {
this._state = 'center';
this.targetElement.appendChild(this.overlayElement);
this.element.classList.add('dv-drop-target');
this.element.append(this.targetElement);
target.classList.add('dv-drop-target');
target.append(this.targetElement);
// this.overlayElement.style.opacity = '0';
// requestAnimationFrame(() => {
// if (this.overlayElement) {
// this.overlayElement.style.opacity = '';
// }
// });
}
this.toggleClasses(quadrant, width, height);
@ -211,10 +266,32 @@ export class Droptarget extends CompositeDisposable {
this._state = quadrant;
},
onDragLeave: () => {
const target = this.options.getOverrideTarget?.();
if (target) {
return;
}
this.removeDropTarget();
},
onDragEnd: () => {
onDragEnd: (e) => {
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();
target?.clear();
},
onDrop: (e) => {
e.preventDefault();
@ -223,6 +300,8 @@ export class Droptarget extends CompositeDisposable {
this.removeDropTarget();
this.options.getOverrideTarget?.()?.clear();
if (state) {
// only stop the propagation of the event if we are dealing with it
// which is only when the target has state
@ -268,7 +347,9 @@ export class Droptarget extends CompositeDisposable {
width: number,
height: number
): void {
if (!this.overlayElement) {
const target = this.options.getOverrideTarget?.();
if (!target && !this.overlayElement) {
return;
}
@ -300,6 +381,103 @@ 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%' };
/**
@ -396,10 +574,12 @@ export class Droptarget extends CompositeDisposable {
private removeDropTarget(): void {
if (this.targetElement) {
this._state = undefined;
this.element.removeChild(this.targetElement);
this.targetElement.parentElement?.classList.remove(
'dv-drop-target'
);
this.targetElement.remove();
this.targetElement = undefined;
this.overlayElement = undefined;
this.element.classList.remove('dv-drop-target');
}
}
}

View File

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

View File

@ -72,9 +72,11 @@ export class GroupDragHandler extends DragHandler {
ghostElement.style.lineHeight = '20px';
ghostElement.style.borderRadius = '12px';
ghostElement.style.position = 'absolute';
ghostElement.style.pointerEvents = 'none';
ghostElement.style.top = '-9999px';
ghostElement.textContent = `Multiple Panels (${this.group.size})`;
addGhostImage(dataTransfer, ghostElement);
addGhostImage(dataTransfer, ghostElement, { y: -10, x: 30 });
}
return {

View File

@ -55,7 +55,15 @@ export class ContentContainer
this.addDisposables(this._onDidFocus, this._onDidBlur);
const target = group.dropTargetContainer;
this.dropTarget = new Droptarget(this.element, {
getOverlayOutline: () => {
return accessor.options.theme?.includeHeaderWhenHoverOverContent
? this.element.parentElement
: null;
},
className: 'dv-drop-target-content',
acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'],
canDisplayOverlay: (event, position) => {
if (
@ -76,26 +84,12 @@ export class ContentContainer
}
if (data && data.viewId === this.accessor.id) {
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 true;
}
return this.group.canDisplayOverlay(event, position, 'content');
},
getOverrideTarget: target ? () => target.model : undefined,
});
this.addDisposables(this.dropTarget);

View File

@ -58,15 +58,13 @@
position: relative;
height: 100%;
display: flex;
min-width: 80px;
align-items: center;
padding: 0px 8px;
white-space: nowrap;
text-overflow: ellipsis;
.dv-default-tab-content {
padding: 0px 8px;
flex-grow: 1;
margin-right: 4px;
}
.dv-default-tab-action {

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

@ -16,6 +16,7 @@ import {
} from '../../../dnd/droptarget';
import { DragHandler } from '../../../dnd/abstractDragHandler';
import { IDockviewPanel } from '../../dockviewPanel';
import { addGhostImage } from '../../../dnd/ghost';
class TabDragHandler extends DragHandler {
private readonly panelTransfer =
@ -49,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;
@ -86,7 +87,8 @@ export class Tab extends CompositeDisposable {
);
this.dropTarget = new Droptarget(this._element, {
acceptedTargetZones: ['center'],
acceptedTargetZones: ['left', 'right'],
overlayModel: { activationSize: { value: 50, type: 'percentage' } },
canDisplayOverlay: (event, position) => {
if (this.group.locked) {
return false;
@ -95,15 +97,7 @@ export class Tab extends CompositeDisposable {
const data = getPanelData();
if (data && this.accessor.id === data.viewId) {
if (
data.panelId === null &&
data.groupId === this.group.id
) {
// don't allow group move to drop on self
return false;
}
return this.panel.id !== data.panelId;
return true;
}
return this.group.model.canDisplayOverlay(
@ -112,24 +106,38 @@ export class Tab extends CompositeDisposable {
'tab'
);
},
getOverrideTarget: () => group.model.dropTargetContainer?.model,
});
this.onWillShowOverlay = this.dropTarget.onWillShowOverlay;
this.addDisposables(
this._onChanged,
this._onPointDown,
this._onDropped,
this._onDragStart,
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);
}),
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

@ -22,10 +22,20 @@
.dv-tab {
-webkit-user-drag: element;
outline: none;
min-width: 75px;
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);
&:first-child {
margin-right: 0;
}
&:not(:nth-last-child(1)) {
margin-left: 0;
}
&:not(:first-child)::before {
content: ' ';

View File

@ -6,7 +6,10 @@ import { VoidContainer } from './voidContainer';
import { toggleClass } from '../../../dom';
import { IDockviewPanel } from '../../dockviewPanel';
import { DockviewComponent } from '../../dockviewComponent';
import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel';
import {
DockviewGroupPanelModel,
WillShowOverlayLocationEvent,
} from '../../dockviewGroupPanelModel';
import { getPanelData } from '../../../dnd/dataTransfer';
import { Tabs } from './tabs';

View File

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

View File

@ -54,6 +54,7 @@ import { Parameters } from '../panel/types';
import { Overlay } from '../overlay/overlay';
import {
addTestId,
Classnames,
getDockviewTheme,
toggleClass,
watchElementResize,
@ -75,6 +76,8 @@ import {
import { PopoutWindow } from '../popoutWindow';
import { StrictEventsSequencing } from './strictEventsSequencing';
import { PopupService } from './components/popupService';
import { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer';
import { themeAbyss } from './theme';
const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = {
activationSize: { type: 'pixels', value: 10 },
@ -192,6 +195,9 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
readonly totalPanels: number;
readonly panels: IDockviewPanel[];
readonly orientation: Orientation;
/**
* @deprecated use `theme` instead. This will be removed in a future version
*/
readonly gap: number;
readonly onDidDrop: Event<DockviewDidDropEvent>;
readonly onWillDrop: Event<DockviewWillDropEvent>;
@ -254,10 +260,12 @@ export class DockviewComponent
private readonly _deserializer = new DefaultDockviewDeserialzier(this);
private readonly _api: DockviewApi;
private _options: Exclude<DockviewComponentOptions, 'orientation'>;
private watermark: IWatermarkRenderer | null = null;
private _watermark: IWatermarkRenderer | null = null;
private readonly _themeClassnames: Classnames;
readonly overlayRenderContainer: OverlayRenderContainer;
readonly popupService: PopupService;
readonly rootDropTargetContainer: DropTargetAnchorContainer;
private readonly _onWillDragPanel = new Emitter<TabDragEvent>();
readonly onWillDragPanel: Event<TabDragEvent> = this._onWillDragPanel.event;
@ -363,6 +371,9 @@ export class DockviewComponent
}
get gap(): number {
console.warn(
'dockview: dockviewComponent.gap has been deprecated. Use `theme` instead. This will be removed in a future version.'
);
return this.gridview.margin;
}
@ -379,12 +390,20 @@ export class DockviewComponent
: undefined,
disableAutoResizing: options.disableAutoResizing,
locked: options.locked,
margin: options.gap,
margin: options.theme?.gap ?? 0,
className: options.className,
});
this.popupService = new PopupService(this.element);
this.updateDropTargetModel(options);
this._themeClassnames = new Classnames(this.element);
this.rootDropTargetContainer = new DropTargetAnchorContainer(
this.element,
{ disabled: true }
);
this.overlayRenderContainer = new OverlayRenderContainer(
this.gridview.element,
this
@ -398,6 +417,7 @@ export class DockviewComponent
}
this.addDisposables(
this.rootDropTargetContainer,
this.overlayRenderContainer,
this._onWillDragPanel,
this._onWillDragGroup,
@ -468,8 +488,10 @@ export class DockviewComponent
);
this._options = options;
this.updateTheme();
this._rootDropTarget = new Droptarget(this.element, {
className: 'dv-drop-target-edge',
canDisplayOverlay: (event, position) => {
const data = getPanelData();
@ -510,6 +532,7 @@ export class DockviewComponent
acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'],
overlayModel:
this.options.rootOverlayModel ?? DEFAULT_ROOT_OVERLAY_MODEL,
getOverrideTarget: () => this.rootDropTargetContainer?.model,
});
this.addDisposables(
@ -760,6 +783,15 @@ export class DockviewComponent
popoutContainer.appendChild(group.element);
const anchor = document.createElement('div');
const dropTargetContainer = new DropTargetAnchorContainer(
anchor,
{ disabled: this.rootDropTargetContainer.disabled }
);
popoutContainer.appendChild(anchor);
group.model.dropTargetContainer = dropTargetContainer;
group.model.location = {
type: 'popout',
getWindow: () => _window.window!,
@ -825,6 +857,10 @@ export class DockviewComponent
),
overlayRenderContainer,
Disposable.from(() => {
if (this.isDisposed) {
return; // cleanup may run after instance is disposed
}
if (
isGroupAddedToDom &&
this.getPanel(referenceGroup.id)
@ -848,6 +884,8 @@ export class DockviewComponent
} else if (this.getPanel(group.id)) {
group.model.renderContainer =
this.overlayRenderContainer;
group.model.dropTargetContainer =
this.rootDropTargetContainer;
returnedGroup = group;
const alreadyRemoved = !this._popoutGroups.find(
@ -1138,6 +1176,13 @@ export class DockviewComponent
override updateOptions(options: Partial<DockviewComponentOptions>): void {
super.updateOptions(options);
if ('gap' in options) {
console.warn(
'dockview: dockviewComponent.setGap has been deprecated. Use `theme` instead. This will be removed in a future version.'
);
this.gridview.margin = options.gap ?? 0;
}
if ('floatingGroupBounds' in options) {
for (const group of this._floatingGroups) {
switch (options.floatingGroupBounds) {
@ -1162,18 +1207,14 @@ export class DockviewComponent
}
}
if ('rootOverlayModel' in options) {
this._rootDropTarget.setOverlayModel(
options.rootOverlayModel ?? DEFAULT_ROOT_OVERLAY_MODEL
);
}
if ('gap' in options) {
this.gridview.margin = options.gap ?? 0;
}
this.updateDropTargetModel(options);
this._options = { ...this.options, ...options };
if ('theme' in options) {
this.updateTheme();
}
this.layout(this.gridview.width, this.gridview.height, true);
}
@ -1749,24 +1790,24 @@ export class DockviewComponent
(x) => x.api.location.type === 'grid' && x.api.isVisible
).length === 0
) {
if (!this.watermark) {
this.watermark = this.createWatermarkComponent();
if (!this._watermark) {
this._watermark = this.createWatermarkComponent();
this.watermark.init({
this._watermark.init({
containerApi: new DockviewApi(this),
});
const watermarkContainer = document.createElement('div');
watermarkContainer.className = 'dv-watermark-container';
addTestId(watermarkContainer, 'watermark-component');
watermarkContainer.appendChild(this.watermark.element);
watermarkContainer.appendChild(this._watermark.element);
this.gridview.element.appendChild(watermarkContainer);
}
} else if (this.watermark) {
this.watermark.element.parentElement!.remove();
this.watermark.dispose?.();
this.watermark = null;
} else if (this._watermark) {
this._watermark.element.parentElement!.remove();
this._watermark.dispose?.();
this._watermark = null;
}
}
@ -2408,9 +2449,11 @@ export class DockviewComponent
if (this._moving) {
return;
}
if (event.panel !== this.activePanel) {
return;
}
if (this._onDidActivePanelChange.value !== event.panel) {
this._onDidActivePanelChange.fire(event.panel);
}
@ -2493,4 +2536,44 @@ export class DockviewComponent
? rootOrientation
: orthogonal(rootOrientation);
}
private updateDropTargetModel(options: Partial<DockviewComponentOptions>) {
if ('dndEdges' in options) {
this._rootDropTarget.disabled =
typeof options.dndEdges === 'boolean' &&
options.dndEdges === false;
if (
typeof options.dndEdges === 'object' &&
options.dndEdges !== null
) {
this._rootDropTarget.setOverlayModel(options.dndEdges);
} else {
this._rootDropTarget.setOverlayModel(
DEFAULT_ROOT_OVERLAY_MODEL
);
}
}
if ('rootOverlayModel' in options) {
this.updateDropTargetModel({ dndEdges: options.dndEdges });
}
}
private updateTheme(): void {
const theme = this._options.theme ?? themeAbyss;
this._themeClassnames.setClassNames(theme.className);
this.gridview.margin = theme.gap ?? 0;
switch (theme.dndOverlayMounting) {
case 'absolute':
this.rootDropTargetContainer.disabled = false;
break;
case 'relative':
default:
this.rootDropTargetContainer.disabled = true;
break;
}
}
}

View File

@ -124,6 +124,12 @@ export class DockviewGroupPanel
options,
this
);
this.addDisposables(
this.model.onDidActivePanelChange((event) => {
this.api._onDidActivePanelChange.fire(event);
})
);
}
override focus(): void {

View File

@ -39,6 +39,7 @@ import {
import { OverlayRenderContainer } from '../overlay/overlayRenderContainer';
import { TitleEvent } from '../api/dockviewPanelApi';
import { Contraints } from '../gridview/gridviewPanel';
import { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer';
interface GroupMoveEvent {
groupId: string;
@ -265,6 +266,8 @@ export class DockviewGroupPanelModel
private mostRecentlyUsed: IDockviewPanel[] = [];
private _overwriteRenderContainer: OverlayRenderContainer | null = null;
private _overwriteDropTargetContainer: DropTargetAnchorContainer | null =
null;
private readonly _onDidChange = new Emitter<IViewSize | undefined>();
readonly onDidChange: Event<IViewSize | undefined> =
@ -535,6 +538,17 @@ export class DockviewGroupPanelModel
);
}
set dropTargetContainer(value: DropTargetAnchorContainer | null) {
this._overwriteDropTargetContainer = value;
}
get dropTargetContainer(): DropTargetAnchorContainer | null {
return (
this._overwriteDropTargetContainer ??
this.accessor.rootDropTargetContainer
);
}
initialize(): void {
if (this.options.panels) {
this.options.panels.forEach((panel) => {
@ -1049,6 +1063,29 @@ export class DockviewGroupPanelModel
const data = getPanelData();
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) {
// this is a group move dnd event
const { groupId } = data;

View File

@ -17,6 +17,7 @@ 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 {
readonly element: HTMLElement;
@ -51,19 +52,26 @@ export interface DockviewOptions {
};
popoutUrl?: string;
defaultRenderer?: DockviewPanelRenderer;
debug?: boolean;
rootOverlayModel?: DroptargetOverlayModel;
locked?: boolean;
disableDnd?: boolean;
className?: string;
/**
* Pixel gap between groups
* @deprecated dockview: dockviewComponent.gap has been deprecated. Use `theme` instead. This will be removed in a future version.
*/
gap?: number;
debug?: boolean;
// #start dnd
dndEdges?: false | DroptargetOverlayModel;
/**
* @deprecated use `dndEdges` instead. To be removed in a future version.
* */
rootOverlayModel?: DroptargetOverlayModel;
disableDnd?: boolean;
// #end dnd
locked?: boolean;
className?: string;
/**
* Define the behaviour of the dock when there are no panels to display. Defaults to `watermark`.
*/
noPanelsOverlay?: 'emptyGroup' | 'watermark';
theme?: DockviewTheme;
}
export interface DockviewDndOverlayEvent extends IAcceptableEvent {
@ -106,9 +114,11 @@ export const PROPERTY_KEYS_DOCKVIEW: (keyof DockviewOptions)[] = (() => {
rootOverlayModel: undefined,
locked: undefined,
disableDnd: undefined,
gap: undefined,
className: undefined,
noPanelsOverlay: undefined,
dndEdges: undefined,
theme: undefined,
gap: undefined,
};
return Object.keys(properties) as (keyof DockviewOptions)[];

View File

@ -0,0 +1,54 @@
export interface DockviewTheme {
name: string;
className: string;
gap?: number;
dndOverlayMounting?: 'absolute' | 'relative';
includeHeaderWhenHoverOverContent?: boolean;
}
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',
includeHeaderWhenHoverOverContent: true,
};
export const themeLightSpaced: DockviewTheme = {
name: 'lightSpaced',
className: 'dockview-theme-light-spaced',
gap: 10,
dndOverlayMounting: 'absolute',
includeHeaderWhenHoverOverContent: true,
};

View File

@ -64,6 +64,7 @@ export {
} from './dockview/framework';
export * from './dockview/options';
export * from './dockview/theme';
export * from './dockview/dockviewPanel';
export { DefaultTab } from './dockview/components/tab/defaultTab';
export {

View File

@ -116,16 +116,18 @@
-moz-user-select: none; // Firefox
-ms-user-select: none; // IE 10 and IE 11
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

@ -219,6 +219,8 @@ export class Splitview {
set margin(value: number) {
this._margin = value;
toggleClass(this.element, 'dv-splitview-has-margin', value !== 0);
}
constructor(

View File

@ -1,17 +1,31 @@
@import 'theme/_sash-handle-mixin';
@import 'theme/_drop-target-static-mixin';
@import 'theme/_space-mixin';
@mixin dockview-theme-core-mixin {
--dv-paneview-active-outline-color: dodgerblue;
--dv-tabs-and-actions-container-font-size: 13px;
--dv-tabs-and-actions-container-height: 35px;
--dv-drag-over-background-color: rgba(83, 89, 93, 0.5);
--dv-drag-over-border-color: white;
--dv-drag-over-border-color: transparent;
--dv-tabs-container-scrollbar-color: #888;
--dv-icon-hover-background-color: rgba(90, 93, 94, 0.31);
--dv-floating-box-shadow: 8px 8px 8px 0px rgba(83, 89, 93, 0.5);
--dv-overlay-z-index: 999;
//
--dv-tab-font-size: inherit;
--dv-border-radius: 0px;
--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 {
@include dockview-theme-core-mixin();
@include dockview-drop-target-no-travel();
//
--dv-group-view-background-color: #1e1e1e;
@ -35,6 +49,8 @@
@mixin dockview-theme-light-mixin {
@include dockview-theme-core-mixin();
@include dockview-drop-target-no-travel();
//
--dv-group-view-background-color: white;
//
@ -131,30 +147,49 @@
@mixin dockview-theme-abyss-mixin {
@include dockview-theme-core-mixin();
@include dockview-drop-target-no-travel();
--dv-color-abyss-dark: #000c18;
--dv-color-abyss: #10192c;
--dv-color-abyss-light: #1c1c2a;
--dv-color-abyss-lighter: #2b2b4a;
--dv-color-abyss-accent: rgb(91, 30, 207);
--dv-color-abyss-primary-text: white;
--dv-color-abyss-secondary-text: rgb(148, 151, 169);
//
--dv-group-view-background-color: #000c18;
--dv-group-view-background-color: var(--dv-color-abyss-dark);
//
--dv-tabs-and-actions-container-background-color: #1c1c2a;
--dv-tabs-and-actions-container-background-color: var(
--dv-color-abyss-light
);
//
--dv-activegroup-visiblepanel-tab-background-color: #000c18;
--dv-activegroup-hiddenpanel-tab-background-color: #10192c;
--dv-inactivegroup-visiblepanel-tab-background-color: #000c18;
--dv-inactivegroup-hiddenpanel-tab-background-color: #10192c;
--dv-tab-divider-color: #2b2b4a;
--dv-activegroup-visiblepanel-tab-background-color: var(
--dv-color-abyss-dark
);
--dv-activegroup-hiddenpanel-tab-background-color: var(--dv-color-abyss);
--dv-inactivegroup-visiblepanel-tab-background-color: var(
--dv-color-abyss-dark
);
--dv-inactivegroup-hiddenpanel-tab-background-color: var(--dv-color-abyss);
--dv-tab-divider-color: var(--dv-color-abyss-lighter);
//
--dv-activegroup-visiblepanel-tab-color: white;
--dv-activegroup-hiddenpanel-tab-color: rgba(255, 255, 255, 0.5);
--dv-inactivegroup-visiblepanel-tab-color: rgba(255, 255, 255, 0.5);
--dv-inactivegroup-hiddenpanel-tab-color: rgba(255, 255, 255, 0.25);
//
--dv-separator-border: #2b2b4a;
--dv-paneview-header-border-color: #2b2b4a;
--dv-separator-border: var(--dv-color-abyss-lighter);
--dv-paneview-header-border-color: var(--dv-color-abyss-lighter);
--dv-paneview-active-outline-color: #596f99;
}
@mixin dockview-theme-dracula-mixin {
@include dockview-theme-core-mixin();
@include dockview-drop-target-no-travel();
//
--dv-group-view-background-color: #282a36;
//
@ -229,10 +264,17 @@
}
@mixin dockview-design-replit-mixin {
@include dockview-drop-target-no-travel();
.dv-resize-container:has(> .dv-groupview) {
border-radius: 8px;
}
.dv-resize-container {
border-radius: 10px !important;
border: none;
}
.dv-groupview {
overflow: hidden;
border-radius: 10px;
@ -266,59 +308,16 @@
border: 1px solid transparent;
}
}
.vertical > .sash-container > .sash {
&:not(.disabled) {
&::after {
content: '';
height: 4px;
width: 40px;
border-radius: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--dv-separator-handle-background-color);
position: absolute;
}
&:hover {
&::after {
background-color: var(
--dv-separator-handle-hover-background-color
);
}
}
}
}
.dv-horizontal > .dv-sash-container > .dv-sash {
&:not(.disabled) {
&::after {
content: '';
height: 40px;
width: 4px;
border-radius: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--dv-separator-handle-background-color);
position: absolute;
}
&:hover {
&::after {
background-color: var(
--dv-separator-handle-hover-background-color
);
}
}
}
}
}
.dockview-theme-replit {
@include dockview-theme-core-mixin();
@include dockview-design-replit-mixin();
@include dockview-design-handle-mixin();
padding: 10px;
background-color: #ebeced;
//
--dv-group-view-background-color: #ebeced;
//
@ -339,6 +338,115 @@
--dv-paneview-header-border-color: rgb(51, 51, 51);
/////
--dv-separator-handle-background-color: #cfd1d3;
--dv-separator-handle-hover-background-color: #babbbb;
--dv-sash-color: #cfd1d3;
--dv-active-sash-color: #babbbb;
}
.dockview-theme-abyss-spaced {
@include dockview-theme-core-mixin();
@include dockview-design-space-mixin();
// stylesheet
--dv-color-abyss-dark: rgb(11, 6, 17);
--dv-color-abyss: #16121f;
--dv-color-abyss-light: #201d2b;
--dv-color-abyss-lighter: #2a2837;
--dv-color-abyss-accent: rgb(91, 30, 207);
--dv-color-abyss-primary-text: white;
--dv-color-abyss-secondary-text: rgb(148, 151, 169);
//
--dv-drag-over-border: 2px solid var(--dv-color-abyss-accent);
--dv-drag-over-background-color: '';
//
//
--dv-group-view-background-color: var(--dv-color-abyss-dark);
//
--dv-tabs-and-actions-container-background-color: var(--dv-color-abyss);
//
--dv-activegroup-visiblepanel-tab-background-color: var(
--dv-color-abyss-lighter
);
--dv-activegroup-hiddenpanel-tab-background-color: var(
--dv-color-abyss-light
);
--dv-inactivegroup-visiblepanel-tab-background-color: var(
--dv-color-abyss-lighter
);
--dv-inactivegroup-hiddenpanel-tab-background-color: var(
--dv-color-abyss-light
);
--dv-tab-divider-color: transparent;
//
--dv-activegroup-visiblepanel-tab-color: var(--dv-color-abyss-primary-text);
--dv-activegroup-hiddenpanel-tab-color: var(
--dv-color-abyss-secondary-text
);
--dv-inactivegroup-visiblepanel-tab-color: var(
--dv-color-abyss-primary-text
);
--dv-inactivegroup-hiddenpanel-tab-color: var(
--dv-color-abyss-secondary-text
);
//
--dv-separator-border: transparent;
--dv-paneview-header-border-color: rgb(51, 51, 51);
/////
--dv-active-sash-color: var(--dv-color-abyss-accent);
//
--dv-floating-box-shadow: 8px 8px 8px 0px rgba(0, 0, 0, 0.5);
padding: 10px;
background-color: var(--dv-color-abyss-dark);
.dv-resize-container {
.dv-groupview {
border: 2px solid var(--dv-color-abyss-dark);
}
}
}
.dockview-theme-light-spaced {
@include dockview-theme-core-mixin();
@include dockview-design-space-mixin();
//
--dv-drag-over-border: 2px solid rgb(91, 30, 207);
--dv-drag-over-background-color: '';
//
//
--dv-group-view-background-color: #f6f5f9;
//
--dv-tabs-and-actions-container-background-color: white;
//
--dv-activegroup-visiblepanel-tab-background-color: #ededf0;
--dv-activegroup-hiddenpanel-tab-background-color: #f9f9fa;
--dv-inactivegroup-visiblepanel-tab-background-color: #ededf0;
--dv-inactivegroup-hiddenpanel-tab-background-color: #f9f9fa;
--dv-tab-divider-color: transparent;
//
--dv-activegroup-visiblepanel-tab-color: rgb(104, 107, 130);
--dv-activegroup-hiddenpanel-tab-color: rgb(148, 151, 169);
--dv-inactivegroup-visiblepanel-tab-color: rgb(104, 107, 130);
--dv-inactivegroup-hiddenpanel-tab-color: rgb(148, 151, 169);
//
--dv-separator-border: transparent;
--dv-paneview-header-border-color: rgb(51, 51, 51);
/////
--dv-active-sash-color: rgb(91, 30, 207);
//
--dv-floating-box-shadow: 8px 8px 8px 0px rgba(0, 0, 0, 0.1);
padding: 10px;
background-color: #f6f5f9;
.dv-resize-container {
.dv-groupview {
border: 2px solid rgb(255, 255, 255, 0.1);
}
}
}

View File

@ -0,0 +1,10 @@
@mixin dockview-drop-target-no-travel {
.dv-drop-target-container {
.dv-drop-target-anchor {
&.dv-drop-target-anchor-container-changed {
opacity: 0;
transition: none;
}
}
}
}

View File

@ -0,0 +1,53 @@
@mixin dockview-design-handle-mixin {
.dv-vertical > .dv-sash-container > .dv-sash {
background-color: transparent;
&:not(.disabled) {
&::after {
content: '';
height: 4px;
width: 40px;
border-radius: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--dv-sash-color);
position: absolute;
}
&:hover,
&:active {
background-color: transparent;
&::after {
background-color: var(--dv-active-sash-color);
}
}
}
}
.dv-horizontal > .dv-sash-container > .dv-sash {
background-color: transparent;
&:not(.disabled) {
&::after {
content: '';
height: 40px;
width: 4px;
border-radius: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--dv-sash-color);
position: absolute;
}
&:hover,
&:active {
background-color: transparent;
&::after {
background-color: var(--dv-active-sash-color);
}
}
}
}
}

View File

@ -0,0 +1,52 @@
@mixin dockview-design-space-mixin {
--dv-tab-font-size: 12px;
--dv-border-radius: 20px;
--dv-tab-margin: 0.5rem 0.25rem;
--dv-tabs-and-actions-container-height: 44px;
--dv-border-radius
.dv-resize-container:has(> .dv-groupview) {
border-radius: 8px;
}
.dv-sash {
border-radius: 4px;
}
.dv-drop-target-anchor {
border-radius: calc(var(--dv-border-radius) / 4);
&.dv-drop-target-content {
border-radius: var(--dv-border-radius);
}
}
.dv-resize-container {
border-radius: var(--dv-border-radius) !important;
border: none;
}
.dv-groupview {
border-radius: var(--dv-border-radius);
.dv-tabs-and-actions-container {
padding: 0px calc(var(--dv-border-radius) / 2);
.dv-tab {
border-radius: 8px;
.dv-svg {
height: 8px;
width: 8px;
}
}
}
.dv-content-container {
background-color: var(
--dv-tabs-and-actions-container-background-color
);
}
}
}

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

@ -7,7 +7,7 @@ export const CloseButton = () => (
viewBox="0 0 28 28"
aria-hidden={'false'}
focusable={false}
className="dockview-svg"
className="dv-svg"
>
<path d="M2.1 27.3L0 25.2L11.55 13.65L0 2.1L2.1 0L13.65 11.55L25.2 0L27.3 2.1L15.75 13.65L27.3 25.2L25.2 27.3L13.65 15.75L2.1 27.3Z"></path>
</svg>
@ -21,7 +21,7 @@ export const ExpandMore = () => {
viewBox="0 0 24 15"
aria-hidden={'false'}
focusable={false}
className="dockview-svg"
className="dv-svg"
>
<path d="M12 14.15L0 2.15L2.15 0L12 9.9L21.85 0.0499992L24 2.2L12 14.15Z" />
</svg>

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

@ -7,7 +7,9 @@ title: Theme
import { CSSVariablesTable, ThemeTable } from '@site/src/components/cssVariables';
Theming is controlled through CSS and is highly customizable.
Dockview components accept a `theme` property which is highly customizable, the theme is largly controlled through CSS however some properties can only be adjusted
by direct editing variables of the `theme` object.
Firstly, you should import `dockview.css`:
@ -38,7 +40,7 @@ To use a `dockview` theme the CSS must encapsulate the component. The current li
<ThemeTable/>
:::info
The source code for all themes can be found [here](https://github.com/mathuo/dockview/blob/master/packages/dockview-core/src/theme.scss).
The source code for all themes can be found [here](https://github.com/mathuo/dockview/blob/master/packages/dockview-core/src/theme.scss) and the associated CSS [here](https://github.com/mathuo/dockview/blob/master/packages/dockview-core/src/theme.scss).
:::
## Customizing Theme

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

@ -11,6 +11,7 @@
&:hover {
border-radius: 2px;
color: var(--dv-activegroup-visiblepanel-tab-color);
background-color: var(--dv-icon-hover-background-color);
}
}

View File

@ -5,6 +5,7 @@ import {
IDockviewPanelHeaderProps,
IDockviewPanelProps,
DockviewApi,
DockviewTheme,
} from 'dockview';
import * as React from 'react';
import './app.scss';
@ -80,6 +81,7 @@ const components = {
);
},
nested: (props: IDockviewPanelProps) => {
const theme = React.useContext(ThemeContext);
return (
<DockviewReact
components={components}
@ -95,7 +97,7 @@ const components = {
console.log('remove', e);
});
}}
className={'dockview-theme-abyss'}
theme={theme}
/>
);
},
@ -141,7 +143,9 @@ const WatermarkComponent = () => {
return <div>custom watermark</div>;
};
const DockviewDemo = (props: { theme?: string }) => {
const ThemeContext = React.createContext<DockviewTheme | undefined>(undefined);
const DockviewDemo = (props: { theme?: DockviewTheme }) => {
const [logLines, setLogLines] = React.useState<
{ text: string; timestamp?: Date; backgroundColor?: string }[]
>([]);
@ -380,18 +384,22 @@ const DockviewDemo = (props: { theme?: string }) => {
}}
>
<DebugContext.Provider value={debug}>
<DockviewReact
components={components}
defaultTabComponent={headerComponents.default}
rightHeaderActionsComponent={RightControls}
leftHeaderActionsComponent={LeftControls}
prefixHeaderActionsComponent={PrefixHeaderControls}
watermarkComponent={
watermark ? WatermarkComponent : undefined
}
onReady={onReady}
className={props.theme || 'dockview-theme-abyss'}
/>
<ThemeContext.Provider value={props.theme}>
<DockviewReact
components={components}
defaultTabComponent={headerComponents.default}
rightHeaderActionsComponent={RightControls}
leftHeaderActionsComponent={LeftControls}
prefixHeaderActionsComponent={
PrefixHeaderControls
}
watermarkComponent={
watermark ? WatermarkComponent : undefined
}
onReady={onReady}
theme={props.theme}
/>
</ThemeContext.Provider>
</DebugContext.Provider>
</div>

View File

@ -81,7 +81,7 @@ export const RightControls = (props: IDockviewHeaderActionsProps) => {
alignItems: 'center',
padding: '0px 8px',
height: '100%',
color: 'var(--dv-activegroup-visiblepanel-tab-color)',
color: 'var(--dv-activegroup-hiddenpanel-tab-color)',
}}
>
{props.isGroupActive && <Icon icon="star" />}

View File

@ -151,11 +151,20 @@ export const GridActions = (props: {
props.api?.addGroup();
};
const [gap, setGap] = React.useState(0);
// const [gap, setGap] = React.useState<number | undefined>(undefined);
React.useEffect(() => {
props.api?.setGap(gap);
}, [gap, props.api]);
const [overlayMode, setOverlayMode] = React.useState<boolean>(false);
// React.useEffect(() => {
// if (!props.api) {
// return;
// }
// if (typeof gap === 'number') {
// props.api.setGap(gap);
// } else {
// setGap(props.api.gap);
// }
// }, [gap, props.api]);
return (
<div className="action-container">
@ -191,6 +200,23 @@ export const GridActions = (props: {
Use Custom Watermark
</button>
</span>
{/* <span className="button-action">
<button
className={
overlayMode ? 'demo-button selected' : 'demo-button'
}
onClick={() => {
props.api?.updateOptions({
dndOverlayMode: !overlayMode
? 'static'
: 'transitional',
});
setOverlayMode(!overlayMode);
}}
>
Use static overlay
</button>
</span> */}
<button className="text-button" onClick={onClear}>
Clear
</button>
@ -204,7 +230,7 @@ export const GridActions = (props: {
Reset
</button>
<span style={{ flexGrow: 1 }} />
<div style={{ display: 'flex', alignItems: 'center' }}>
{/* <div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ paddingRight: '4px' }}>Grid Gap</span>
<input
style={{ width: 40 }}
@ -212,11 +238,11 @@ export const GridActions = (props: {
min={0}
max={99}
step={1}
value={gap}
value={gap ?? 0}
onChange={(event) => setGap(Number(event.target.value))}
/>
<button onClick={() => setGap(0)}>Reset</button>
</div>
</div> */}
</div>
);
};

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

@ -1,13 +1,16 @@
.DropdownMenuContent {
/* min-width: 220px; */
background-color: rgba(255, 255, 255, 0.1);
background-color: var(--ifm-dropdown-background-color);
color: var(--ifm-color-primary);
border: var(--ifm-dropdown-border);
border-radius: 6px;
padding: 5px;
box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2);
animation-duration: 400ms;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
will-change: transform, opacity;
z-index: 99;
}
.DropdownMenuContent[data-side='top'],
.DropdownMenuSubContent[data-side='top'] {
@ -39,25 +42,33 @@
display: flex;
align-items: center;
justify-content: space-between;
width: 100px;
height: 25px;
width: 120px;
/* height: 25px; */
padding: 4px 8px;
font-size: 13px;
font-size: 1rem;
font-weight: normal;
cursor: pointer;
color: var(--ifm-menu-color);
&:hover {
background-color: var(--ifm-hover-overlay);
}
}
.framework-menu-item-select {
display: flex;
align-items: center;
justify-content: space-between;
width: 120px;
width: 130px;
height: 35px;
padding: 4px 8px;
border-radius: 6px;
font-size: 13px;
background-color: rgba(255, 255, 255, 0.1);
font-size: 1rem;
font-weight: normal;
cursor: pointer;
border: 1px solid rgba(0,0,0, 0.1);
border: 1px solid rgba(60, 60, 66,0.5);
}
@keyframes slideUpAndFade {

View File

@ -1,6 +1,8 @@
import BrowserOnly from '@docusaurus/BrowserOnly';
import { DockviewEmitter } from 'dockview';
import * as React from 'react';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import useBaseUrl from '@docusaurus/useBaseUrl';
import './frameworkSpecific.css';
export interface FrameworkDescriptor {
@ -51,8 +53,7 @@ export function useActiveFramework(): [
return [option, setter];
}
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import useBaseUrl from '@docusaurus/useBaseUrl';
const FrameworkSelector1 = () => {
const [activeFramework, setActiveFramework] = useActiveFramework();

View File

@ -28,7 +28,7 @@
}
}
.dockview-svg {
.dv-svg {
display: inline-block;
fill: currentcolor;
line-height: 1;

View File

@ -17,7 +17,7 @@ const createSvgElementFromPath = (params: {
width={params.width}
viewBox={params.viewbox}
focusable={false}
className={'dockview-svg'}
className={'dv-svg'}
>
<path d={params.path} />
</svg>
@ -54,7 +54,7 @@ export const CodeSandboxButton = (props: {
<a
href={url}
target={'_blank'}
rel='noopener'
rel="noopener"
className="codesandbox-button-content"
>
<span

View File

@ -57,7 +57,7 @@ export const Container = (props: {
const ReactIcon = (props: { height: number; width: number }) => {
return (
<img
// className="dockview-svg"
// className="dv-svg"
style={{ marginRight: '0px 4px' }}
height={props.height}
width={props.width}
@ -69,7 +69,7 @@ const ReactIcon = (props: { height: number; width: number }) => {
const JavascriptIcon = (props: { height: number; width: number }) => {
return (
<img
// className="dockview-svg "
// className="dv-svg "
style={{ marginRight: '0px 4px' }}
height={props.height}
width={props.width}
@ -85,6 +85,7 @@ const themes = [
'dockview-theme-vs',
'dockview-theme-dracula',
'dockview-theme-replit',
'dockview-theme-kraken',
];
function useLocalStorageItem(key: string, defaultValue: string): string {
@ -122,7 +123,6 @@ export const ThemePicker = () => {
return (
<div
style={{
height: '20px',
display: 'flex',
alignItems: 'center',

View File

@ -1,10 +1,11 @@
import * as React from 'react';
import { CodeSandboxButton } from './codeSandboxButton';
import BrowserOnly from '@docusaurus/BrowserOnly';
import { DockviewTheme } from 'dockview';
const ExampleFrame = (props: {
framework: string;
theme?: string;
theme?: DockviewTheme;
id: string;
height?: string;
}) => {

View File

@ -1,33 +1,54 @@
import {
themeAbyss,
themeDark,
themeDracula,
themeAbyssSpaced,
themeLightSpaced,
themeLight,
themeReplit,
themeVisualStudio,
} from 'dockview';
export const themeConfig = [
{
id: 'dockview-theme-dark',
id: themeDark,
key: '**[dockview-theme-dark](/demo?theme=dockview-theme-dark)**',
text: '',
},
{
id: 'dockview-theme-light',
id: themeLight,
key: '**[dockview-theme-light](/demo?theme=dockview-theme-light)**',
text: '',
},
{
id: 'dockview-theme-vs',
id: themeVisualStudio,
key: '**[dockview-theme-vs](/demo?theme=dockview-theme-vs)**',
text: 'Based on [Visual Studio](https://visualstudio.microsoft.com)',
},
{
id: 'dockview-theme-abyss',
id: themeAbyss,
key: '**[dockview-theme-abyss](/demo?theme=dockview-theme-abyss)**',
text: 'Based on [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/themes) abyss theme',
},
{
id: 'dockview-theme-dracula',
id: themeDracula,
key: '**[dockview-theme-dracula](/demo?theme=dockview-theme-dracula)**',
text: 'Based on [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/themes) dracula theme',
},
{
id: 'dockview-theme-replit',
id: themeReplit,
key: '**[dockview-theme-replit](/demo?theme=dockview-theme-replit)**',
text: 'Based on [Replit](https://replit.com)',
},
{
id: themeLightSpaced,
key: '**[dockview-theme-replit](/demo?theme=dockview-theme-kraken)**',
text: '',
},
{
id: themeAbyssSpaced,
key: '**[dockview-theme-replit](/demo?theme=dockview-theme-kraken)**',
text: '',
},
];

View File

@ -11,10 +11,10 @@
/* You can override the default Infima variables here. */
:root {
--ifm-font-family-base: "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system,
BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans,
sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol,
Noto Color Emoji;
--ifm-font-family-base: 'IBM Plex Sans', ui-sans-serif, system-ui,
-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue,
Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji,
Segoe UI Symbol, Noto Color Emoji;
--ifm-font-weight-bold: 600;
@ -36,6 +36,9 @@
--ifm-color-primary: black;
--ifm-dropdown-background-color: white;
--ifm-dropdown-border: 1px solid var(--ifm-color-primary-darkest);
--ifm-navbar-link-color: white;
--ifm-navbar-link-hover-color: white;
@ -54,15 +57,18 @@
}
/* --ifm-color-primary: #0c111d; */
--ifm-color-primary: #25c2a0;
--ifm-color-primary-dark: #21af90;
--ifm-color-primary-darker: #1fa588;
--ifm-color-primary-darkest: #1a8870;
--ifm-color-primary-light: #29d5b0;
--ifm-color-primary-lighter: #32d8b4;
--ifm-color-primary-lightest: #4fddbf;
--ifm-color-primary: #98a2b3;
--ifm-color-primary-dark: #828a99;
--ifm-color-primary-darker: #6a707c;
--ifm-color-primary-darkest: #474b53;
--ifm-color-primary-light: #acb7ca;
--ifm-color-primary-lighter: #bcc9df;
--ifm-color-primary-lightest: #d2e1fa;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
--ifm-dropdown-background-color: #373d4b;
--ifm-dropdown-border: 1px solid var(--ifm-color-primary-darkest);
--dv-docs-markdown-text-color: #cdced8;
}

View File

@ -3,11 +3,29 @@ import Layout from '@theme/Layout';
import { themeConfig } from '../config/theme.config';
import ExampleFrame from '../components/ui/exampleFrame';
import BrowserOnly from '@docusaurus/BrowserOnly';
import { DockviewTheme, themeAbyss } from 'dockview';
const updateTheme = (theme: DockviewTheme) => {
const urlParams = new URLSearchParams(window.location.search);
urlParams.set('theme', theme.name);
const newUrl = window.location.pathname + '?' + urlParams.toString();
window.history.pushState({ path: newUrl }, '', newUrl);
};
const ThemeToggle: React.FC = () => {
const [theme, setTheme] = React.useState<string>(
new URLSearchParams(location.search).get('theme') ?? themeConfig[3].id
);
const [theme, setTheme] = React.useState<DockviewTheme>(themeAbyss);
React.useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const themeName = urlParams.get('theme');
const newTheme =
themeConfig.find((c) => c.id.name === themeName)?.id ?? themeAbyss;
setTheme(newTheme);
updateTheme(newTheme);
}, []);
return (
<>
@ -16,20 +34,48 @@ const ThemeToggle: React.FC = () => {
height: '40px',
display: 'flex',
alignItems: 'center',
padding: '0px 15px',
}}
>
<select
<div style={{ display: 'flex', alignItems: 'center' }}>
<div
style={{
paddingRight: 8,
color: 'var(--ifm-color-primary)',
fontSize: '0.9em',
}}
>
{'Theme: '}
</div>
<ThemeSelector
value={theme.name}
options={themeConfig.map((theme) => theme.id.name)}
onChanged={(value) => {
const theme =
themeConfig.find(
(theme) => theme.id.name === value
)?.id ?? themeAbyss;
setTheme(theme);
updateTheme(theme);
}}
/>
</div>
{/* <select
onChange={(event) => {
const url = new URL(window.location.href);
url.searchParams.set('theme', event.target.value);
window.location.href = url.toString();
const theme = themeConfig.find(
(theme) => theme.id.name === event.target.value
).id;
setTheme(theme);
updateTheme(theme);
}}
value={theme}
value={theme.name}
>
{themeConfig.map((theme) => {
return <option key={theme.id}>{theme.id}</option>;
return (
<option key={theme.id.name}>{theme.id.name}</option>
);
})}
</select>
</select> */}
</div>
<ExampleFrame
theme={theme}
@ -58,3 +104,72 @@ export default function Popout() {
</Layout>
);
}
import {
DropdownMenu,
DropdownMenuItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@radix-ui/react-dropdown-menu';
const ThemeSelector = (props: {
options: string[];
value: string;
onChanged: (value: string) => void;
}) => {
const ref = React.useRef<HTMLDivElement>(null);
return (
<div ref={ref}>
<DropdownMenu
onOpenChange={(open) => {
if (!open) {
return;
}
if (!ref.current) {
return;
}
requestAnimationFrame(() => {
const el = ref.current!.querySelector(
`[data-dropdown-menu-value="${props.value}"]`
);
if (el) {
(el as HTMLElement).focus();
}
});
}}
>
<DropdownMenuTrigger asChild={true}>
<div className="framework-menu-item-select">
{props.value}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
side="bottom"
align="end"
sideOffset={10}
className="DropdownMenuContent"
>
{props.options.map((option) => {
return (
<DropdownMenuItem
data-dropdown-menu-value={option}
onClick={() => props.onChanged(option)}
className="DropdownMenuItem"
>
<div className="framework-menu-item">
<span>{option}</span>
<span>
{option === props.value ? '✓' : ''}
</span>
</div>
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};

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',
},
});