Merge pull request #177 from mathuo/176-dnd-to-edge-of-dockview

176 dnd to edge of dockview
This commit is contained in:
mathuo 2023-02-15 22:41:31 +07:00 committed by GitHub
commit 213d3ed235
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 2095 additions and 994 deletions

View File

@ -10,7 +10,11 @@ describe('groupPanelApi', () => {
title: 'test_title',
};
const accessor: Partial<DockviewComponent> = {};
const accessor: Partial<DockviewComponent> = {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
const groupViewPanel = new GroupPanel(
<DockviewComponent>accessor,
'',
@ -44,7 +48,11 @@ describe('groupPanelApi', () => {
id: 'test_id',
};
const accessor: Partial<DockviewComponent> = {};
const accessor: Partial<DockviewComponent> = {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
const groupViewPanel = new GroupPanel(
<DockviewComponent>accessor,
'',

View File

@ -1,16 +1,22 @@
import { Droptarget, Position } from '../../dnd/droptarget';
import {
calculateQuadrantAsPercentage,
calculateQuadrantAsPixels,
directionToPosition,
Droptarget,
Position,
} from '../../dnd/droptarget';
import { fireEvent } from '@testing-library/dom';
function createOffsetDragOverEvent(params: {
offsetX: number;
offsetY: number;
clientX: number;
clientY: number;
}): Event {
const event = new Event('dragover', {
bubbles: true,
cancelable: true,
});
Object.defineProperty(event, 'offsetX', { get: () => params.offsetX });
Object.defineProperty(event, 'offsetY', { get: () => params.offsetY });
Object.defineProperty(event, 'clientX', { get: () => params.clientX });
Object.defineProperty(event, 'clientY', { get: () => params.clientY });
return event;
}
@ -27,12 +33,23 @@ describe('droptarget', () => {
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 200);
});
test('directionToPosition', () => {
expect(directionToPosition('above')).toBe('top');
expect(directionToPosition('below')).toBe('bottom');
expect(directionToPosition('left')).toBe('left');
expect(directionToPosition('right')).toBe('right');
expect(directionToPosition('within')).toBe('center');
expect(() => directionToPosition('bad_input' as any)).toThrow(
"invalid direction 'bad_input'"
);
});
test('non-directional', () => {
let position: Position | undefined = undefined;
droptarget = new Droptarget(element, {
canDisplayOverlay: () => true,
validOverlays: 'none',
acceptedTargetZones: ['center'],
});
droptarget.onDrop((event) => {
@ -46,7 +63,7 @@ describe('droptarget', () => {
'.drop-target-dropzone'
) as HTMLElement;
fireEvent.drop(target);
expect(position).toBe(Position.Center);
expect(position).toBe('center');
});
test('drop', () => {
@ -54,7 +71,7 @@ describe('droptarget', () => {
droptarget = new Droptarget(element, {
canDisplayOverlay: () => true,
validOverlays: 'all',
acceptedTargetZones: ['top', 'left', 'right', 'bottom', 'center'],
});
droptarget.onDrop((event) => {
@ -73,18 +90,21 @@ describe('droptarget', () => {
fireEvent(
target,
createOffsetDragOverEvent({ offsetX: 19, offsetY: 0 })
createOffsetDragOverEvent({
clientX: 19,
clientY: 0,
})
);
expect(position).toBeUndefined();
fireEvent.drop(target);
expect(position).toBe(Position.Left);
expect(position).toBe('left');
});
test('default', () => {
droptarget = new Droptarget(element, {
canDisplayOverlay: () => true,
validOverlays: 'all',
acceptedTargetZones: ['top', 'left', 'right', 'bottom', 'center'],
});
expect(droptarget.state).toBeUndefined();
@ -106,57 +126,204 @@ describe('droptarget', () => {
fireEvent(
target,
createOffsetDragOverEvent({ offsetX: 19, offsetY: 0 })
createOffsetDragOverEvent({ clientX: 19, clientY: 0 })
);
viewQuery = element.querySelectorAll(
'.drop-target > .drop-target-dropzone > .drop-target-selection.left'
'.drop-target > .drop-target-dropzone > .drop-target-selection'
);
expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe(Position.Left);
expect(droptarget.state).toBe('left');
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('translateX(-25%) scaleX(0.5)');
fireEvent(
target,
createOffsetDragOverEvent({ offsetX: 40, offsetY: 19 })
createOffsetDragOverEvent({ clientX: 40, clientY: 19 })
);
viewQuery = element.querySelectorAll(
'.drop-target > .drop-target-dropzone > .drop-target-selection.top'
'.drop-target > .drop-target-dropzone > .drop-target-selection'
);
expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe(Position.Top);
expect(droptarget.state).toBe('top');
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('translateY(-25%) scaleY(0.5)');
fireEvent(
target,
createOffsetDragOverEvent({ offsetX: 160, offsetY: 81 })
createOffsetDragOverEvent({ clientX: 160, clientY: 81 })
);
viewQuery = element.querySelectorAll(
'.drop-target > .drop-target-dropzone > .drop-target-selection.bottom'
'.drop-target > .drop-target-dropzone > .drop-target-selection'
);
expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe(Position.Bottom);
expect(droptarget.state).toBe('bottom');
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('translateY(25%) scaleY(0.5)');
fireEvent(
target,
createOffsetDragOverEvent({ offsetX: 161, offsetY: 0 })
createOffsetDragOverEvent({ clientX: 161, clientY: 0 })
);
viewQuery = element.querySelectorAll(
'.drop-target > .drop-target-dropzone > .drop-target-selection.right'
'.drop-target > .drop-target-dropzone > .drop-target-selection'
);
expect(viewQuery.length).toBe(1);
expect(droptarget.state).toBe(Position.Right);
expect(droptarget.state).toBe('right');
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('translateX(25%) scaleX(0.5)');
fireEvent(
target,
createOffsetDragOverEvent({ offsetX: 100, offsetY: 50 })
createOffsetDragOverEvent({ clientX: 100, clientY: 50 })
);
expect(droptarget.state).toBe(Position.Center);
expect(droptarget.state).toBe('center');
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('');
fireEvent.dragLeave(target);
expect(droptarget.state).toBeUndefined();
expect(droptarget.state).toBe('center');
viewQuery = element.querySelectorAll('.drop-target');
expect(viewQuery.length).toBe(0);
});
describe('calculateQuadrantAsPercentage', () => {
test('variety of cases', () => {
const inputs: Array<{
directions: Position[];
x: number;
y: number;
result: Position | null;
}> = [
{ directions: ['left', 'right'], x: 19, y: 50, result: 'left' },
{
directions: ['left', 'right'],
x: 81,
y: 50,
result: 'right',
},
{
directions: ['top', 'bottom'],
x: 50,
y: 19,
result: 'top',
},
{
directions: ['top', 'bottom'],
x: 50,
y: 81,
result: 'bottom',
},
{
directions: ['left', 'right', 'top', 'bottom', 'center'],
x: 50,
y: 50,
result: 'center',
},
{
directions: ['left', 'right', 'top', 'bottom'],
x: 50,
y: 50,
result: null,
},
];
for (const input of inputs) {
expect(
calculateQuadrantAsPercentage(
new Set(input.directions),
input.x,
input.y,
100,
100,
20
)
).toBe(input.result);
}
});
});
describe('calculateQuadrantAsPixels', () => {
test('variety of cases', () => {
const inputs: Array<{
directions: Position[];
x: number;
y: number;
result: Position | null;
}> = [
{ directions: ['left', 'right'], x: 19, y: 50, result: 'left' },
{
directions: ['left', 'right'],
x: 81,
y: 50,
result: 'right',
},
{
directions: ['top', 'bottom'],
x: 50,
y: 19,
result: 'top',
},
{
directions: ['top', 'bottom'],
x: 50,
y: 81,
result: 'bottom',
},
{
directions: ['left', 'right', 'top', 'bottom', 'center'],
x: 50,
y: 50,
result: 'center',
},
{
directions: ['left', 'right', 'top', 'bottom'],
x: 50,
y: 50,
result: null,
},
];
for (const input of inputs) {
expect(
calculateQuadrantAsPixels(
new Set(input.directions),
input.x,
input.y,
100,
100,
20
)
).toBe(input.result);
}
});
});
});

View File

@ -32,7 +32,7 @@ class PanelContentPartTest implements IContentRenderer {
isDisposed: boolean = false;
constructor(public readonly id: string, component: string) {
constructor(public readonly id: string, public readonly component: string) {
this.element.classList.add(`testpanel-${id}`);
}
@ -53,7 +53,7 @@ class PanelContentPartTest implements IContentRenderer {
}
toJSON(): object {
return { id: this.id };
return { id: this.component };
}
focus(): void {
@ -253,9 +253,9 @@ describe('dockviewComponent', () => {
const panel4 = dockview.getGroupPanel('panel4');
const group1 = panel1!.group;
dockview.moveGroupOrPanel(group1, group1.id, 'panel1', Position.Right);
dockview.moveGroupOrPanel(group1, group1.id, 'panel1', 'right');
const group2 = panel1!.group;
dockview.moveGroupOrPanel(group2, group1.id, 'panel3', Position.Center);
dockview.moveGroupOrPanel(group2, group1.id, 'panel3', 'center');
expect(dockview.activeGroup).toBe(group2);
expect(dockview.activeGroup!.model.activePanel).toBe(panel3);
@ -302,12 +302,12 @@ describe('dockviewComponent', () => {
component: 'default',
});
const panel1 = dockview.getGroupPanel('panel1');
const panel2 = dockview.getGroupPanel('panel2');
const panel1 = dockview.getGroupPanel('panel1')!;
const panel2 = dockview.getGroupPanel('panel2')!;
const group1 = panel1.group;
dockview.moveGroupOrPanel(group1, group1.id, 'panel1', Position.Right);
dockview.moveGroupOrPanel(group1, group1.id, 'panel1', 'right');
const group2 = panel1.group;
dockview.moveGroupOrPanel(group2, group1.id, 'panel3', Position.Center);
dockview.moveGroupOrPanel(group2, group1.id, 'panel3', 'center');
expect(dockview.size).toBe(2);
expect(dockview.totalPanels).toBe(4);
@ -345,10 +345,10 @@ describe('dockviewComponent', () => {
component: 'default',
});
const panel1 = dockview.getGroupPanel('panel1');
const panel2 = dockview.getGroupPanel('panel2');
const panel3 = dockview.getGroupPanel('panel3');
const panel4 = dockview.getGroupPanel('panel4');
const panel1 = dockview.getGroupPanel('panel1')!;
const panel2 = dockview.getGroupPanel('panel2')!;
const panel3 = dockview.getGroupPanel('panel3')!;
const panel4 = dockview.getGroupPanel('panel4')!;
expect(panel1.api.isActive).toBeFalsy();
expect(panel2.api.isActive).toBeFalsy();
@ -370,9 +370,9 @@ describe('dockviewComponent', () => {
expect(panel4.api.isActive).toBeFalsy();
const group1 = panel1.group;
dockview.moveGroupOrPanel(group1, group1.id, 'panel1', Position.Right);
dockview.moveGroupOrPanel(group1, group1.id, 'panel1', 'right');
const group2 = panel1.group;
dockview.moveGroupOrPanel(group2, group1.id, 'panel3', Position.Center);
dockview.moveGroupOrPanel(group2, group1.id, 'panel3', 'center');
expect(dockview.size).toBe(2);
expect(panel1.group).toBe(panel3.group);
@ -425,9 +425,8 @@ describe('dockviewComponent', () => {
expect(dockview.size).toBe(1);
expect(dockview.totalPanels).toBe(2);
const panel1 = dockview.getGroupPanel('panel1');
const panel2 = dockview.getGroupPanel('panel2');
const panel1 = dockview.getGroupPanel('panel1')!;
const panel2 = dockview.getGroupPanel('panel2')!;
expect(panel1.group).toBe(panel2.group);
const group = panel1.group;
@ -440,7 +439,7 @@ describe('dockviewComponent', () => {
expect(group.model.indexOf(panel1)).toBe(0);
expect(group.model.indexOf(panel2)).toBe(1);
dockview.moveGroupOrPanel(group, group.id, 'panel1', Position.Right);
dockview.moveGroupOrPanel(group, group.id, 'panel1', 'right');
expect(dockview.size).toBe(2);
expect(dockview.totalPanels).toBe(2);
@ -489,8 +488,8 @@ describe('dockviewComponent', () => {
expect(viewQuery.length).toBe(1);
const group = dockview.getGroupPanel('panel1').group;
dockview.moveGroupOrPanel(group, group.id, 'panel1', Position.Right);
const group = dockview.getGroupPanel('panel1')!.group;
dockview.moveGroupOrPanel(group, group.id, 'panel1', 'right');
viewQuery = container.querySelectorAll(
'.branch-node > .split-view-container > .view-container > .view'
@ -975,7 +974,7 @@ describe('dockviewComponent', () => {
panel2.group!,
panel5.group!.id,
panel5.id,
Position.Center
'center'
);
expect(events).toEqual([
{ type: 'REMOVE_PANEL', panel: panel5 },
@ -994,7 +993,7 @@ describe('dockviewComponent', () => {
panel2.group!,
panel4.group!.id,
panel4.id,
Position.Center
'center'
);
expect(events).toEqual([
@ -1314,7 +1313,7 @@ describe('dockviewComponent', () => {
panel1.group,
panel2.group.id,
'panel2',
Position.Left
'left'
);
expect(panel1Spy).not.toHaveBeenCalled();
@ -1355,7 +1354,7 @@ describe('dockviewComponent', () => {
panel1.group,
panel2.group.id,
'panel2',
Position.Center
'center'
);
expect(panel1Spy).not.toHaveBeenCalled();
@ -1394,7 +1393,7 @@ describe('dockviewComponent', () => {
panel1.group,
panel1.group.id,
'panel1',
Position.Center,
'center',
0
);
@ -1515,6 +1514,53 @@ describe('dockviewComponent', () => {
expect(panel2Spy).toBeCalledTimes(1);
});
test('move entire group into another group', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(container, {
components: { default: PanelContentPartTest },
});
dockview.layout(500, 1000);
const panel1 = dockview.addPanel({
id: 'panel1',
component: 'default',
tabComponent: 'default',
});
const panel2 = dockview.addPanel({
id: 'panel2',
component: 'default',
tabComponent: 'default',
position: {
referencePanel: panel1,
},
});
const panel3 = dockview.addPanel({
id: 'panel3',
component: 'default',
tabComponent: 'default',
position: {
referencePanel: panel1,
direction: 'right',
},
});
const panel1Spy = jest.spyOn(panel1.group, 'dispose');
expect(dockview.groups.length).toBe(2);
dockview.moveGroupOrPanel(
panel3.group,
panel1.group.id,
undefined,
'center'
);
expect(dockview.groups.length).toBe(1);
expect(panel1Spy).toBeCalledTimes(1);
});
test('fromJSON events should still fire', () => {
jest.useFakeTimers();
@ -1635,8 +1681,6 @@ describe('dockviewComponent', () => {
jest.runAllTimers();
console.log(activePanel.map((_) => _?.id).join(' '));
expect(addGroup.length).toBe(4);
expect(removeGroup.length).toBe(0);
expect(activeGroup.length).toBe(1);
@ -1973,4 +2017,379 @@ describe('dockviewComponent', () => {
// load a layout with a default tab identifier when react default is present
// load a layout with invialid panel identifier
test('orthogonal realigment #1', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(container, {
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
orientation: Orientation.HORIZONTAL,
});
dockview.deserializer = new ReactPanelDeserialzier(dockview);
expect(dockview.orientation).toBe(Orientation.HORIZONTAL);
dockview.fromJSON({
activeGroup: 'group-1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 500,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.VERTICAL,
},
panels: {
panel1: {
id: 'panel1',
view: { content: { id: 'default' } },
title: 'panel1',
},
},
});
expect(dockview.orientation).toBe(Orientation.VERTICAL);
dockview.addPanel({
id: 'panel2',
component: 'default',
position: {
direction: 'left',
},
});
expect(dockview.orientation).toBe(Orientation.HORIZONTAL);
expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({
activeGroup: '1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel2'],
id: '1',
activeView: 'panel2',
},
size: 500,
},
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 1000,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.HORIZONTAL,
},
panels: {
panel1: {
id: 'panel1',
view: { content: { id: 'default' } },
title: 'panel1',
},
panel2: {
id: 'panel2',
view: { content: { id: 'default' } },
title: 'panel2',
},
},
options: {},
});
});
test('orthogonal realigment #2', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(container, {
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
orientation: Orientation.HORIZONTAL,
});
dockview.deserializer = new ReactPanelDeserialzier(dockview);
expect(dockview.orientation).toBe(Orientation.HORIZONTAL);
dockview.fromJSON({
activeGroup: 'group-1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 500,
},
{
type: 'leaf',
data: {
views: ['panel2'],
id: 'group-2',
activeView: 'panel2',
},
size: 500,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.VERTICAL,
},
panels: {
panel1: {
id: 'panel1',
view: { content: { id: 'default' } },
title: 'panel1',
},
panel2: {
id: 'panel2',
view: { content: { id: 'default' } },
title: 'panel2',
},
},
});
expect(dockview.orientation).toBe(Orientation.VERTICAL);
dockview.addPanel({
id: 'panel3',
component: 'default',
position: {
direction: 'left',
},
});
expect(dockview.orientation).toBe(Orientation.HORIZONTAL);
expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({
activeGroup: '1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel3'],
id: '1',
activeView: 'panel3',
},
size: 500,
},
{
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 500,
},
{
type: 'leaf',
data: {
views: ['panel2'],
id: 'group-2',
activeView: 'panel2',
},
size: 500,
},
],
size: 500,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.HORIZONTAL,
},
panels: {
panel1: {
id: 'panel1',
view: { content: { id: 'default' } },
title: 'panel1',
},
panel2: {
id: 'panel2',
view: { content: { id: 'default' } },
title: 'panel2',
},
panel3: {
id: 'panel3',
view: { content: { id: 'default' } },
title: 'panel3',
},
},
options: {},
});
});
test('orthogonal realigment #3', () => {
const container = document.createElement('div');
const dockview = new DockviewComponent(container, {
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
orientation: Orientation.HORIZONTAL,
});
dockview.deserializer = new ReactPanelDeserialzier(dockview);
expect(dockview.orientation).toBe(Orientation.HORIZONTAL);
dockview.fromJSON({
activeGroup: 'group-1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 500,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.VERTICAL,
},
panels: {
panel1: {
id: 'panel1',
view: { content: { id: 'default' } },
title: 'panel1',
},
},
});
expect(dockview.orientation).toBe(Orientation.VERTICAL);
dockview.addPanel({
id: 'panel2',
component: 'default',
position: {
direction: 'above',
},
});
dockview.addPanel({
id: 'panel3',
component: 'default',
position: {
direction: 'below',
},
});
expect(dockview.orientation).toBe(Orientation.VERTICAL);
expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({
activeGroup: '2',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel2'],
id: '1',
activeView: 'panel2',
},
size: 333,
},
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 333,
},
{
type: 'leaf',
data: {
views: ['panel3'],
id: '2',
activeView: 'panel3',
},
size: 334,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.VERTICAL,
},
panels: {
panel1: {
id: 'panel1',
view: { content: { id: 'default' } },
title: 'panel1',
},
panel2: {
id: 'panel2',
view: { content: { id: 'default' } },
title: 'panel2',
},
panel3: {
id: 'panel3',
view: { content: { id: 'default' } },
title: 'panel3',
},
},
options: {},
});
});
});

View File

@ -1,7 +1,7 @@
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { DockviewApi } from '../../api/component.api';
import { IGroupPanelView } from '../../dockview/defaultGroupPanelView';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { DockviewPanel } from '../../dockview/dockviewPanel';
import { GroupPanel } from '../../groupview/groupviewPanel';
describe('dockviewGroupPanel', () => {
@ -20,7 +20,7 @@ describe('dockviewGroupPanel', () => {
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = new groupMock();
const cut = new DockviewGroupPanel('fake-id', accessor, api, group);
const cut = new DockviewPanel('fake-id', accessor, api, group);
let latestTitle: string | undefined = undefined;
@ -55,7 +55,7 @@ describe('dockviewGroupPanel', () => {
const accessor = new accessorMock();
const group = new groupMock();
const cut = new DockviewGroupPanel('fake-id', accessor, api, group);
const cut = new DockviewPanel('fake-id', accessor, api, group);
const viewMock = jest.fn<IGroupPanelView, []>(() => {
return {
@ -86,7 +86,7 @@ describe('dockviewGroupPanel', () => {
const api = new dockviewApiMock();
const accessor = new accessorMock();
const group = new groupMock();
const cut = new DockviewGroupPanel('fake-id', accessor, api, group);
const cut = new DockviewPanel('fake-id', accessor, api, group);
expect(cut.params).toEqual(undefined);

View File

@ -1,3 +1,4 @@
import { GridviewPanelApiImpl } from '../../api/gridviewPanelApi';
import { GridviewComponent } from '../../gridview/gridviewComponent';
import { GridviewPanel } from '../../gridview/gridviewPanel';
import { CompositeDisposable } from '../../lifecycle';
@ -6,7 +7,9 @@ import { Orientation } from '../../splitview/core/splitview';
class TestGridview extends GridviewPanel {
constructor(id: string, componentName: string) {
super(id, componentName);
super(id, componentName, new GridviewPanelApiImpl(id));
this.api.initialize(this);
this.element.id = id;
}
@ -65,7 +68,7 @@ describe('gridview', () => {
expect(gridview.size).toBe(1);
const panel1 = gridview.getPanel('panel1');
const panel1 = gridview.getPanel('panel1')!;
gridview.removePanel(panel1);
@ -101,9 +104,9 @@ describe('gridview', () => {
component: 'default',
});
const panel1 = gridview.getPanel('panel1');
const panel2 = gridview.getPanel('panel2');
const panel3 = gridview.getPanel('panel3');
const panel1 = gridview.getPanel('panel1')!;
const panel2 = gridview.getPanel('panel2')!;
const panel3 = gridview.getPanel('panel3')!;
expect(panel1.api.isActive).toBeFalsy();
expect(panel2.api.isActive).toBeFalsy();
@ -192,9 +195,9 @@ describe('gridview', () => {
});
gridview.layout(800, 400, true);
const panel1 = gridview.getPanel('panel_1');
const panel2 = gridview.getPanel('panel_2');
const panel3 = gridview.getPanel('panel_3');
const panel1 = gridview.getPanel('panel_1')!;
const panel2 = gridview.getPanel('panel_2')!;
const panel3 = gridview.getPanel('panel_3')!;
expect(panel1?.api.isVisible).toBeTruthy();
expect(panel1?.api.id).toBe('panel_1');
@ -330,7 +333,7 @@ describe('gridview', () => {
component: 'default',
});
const panel1 = gridview.getPanel('panel_1');
const panel1 = gridview.getPanel('panel_1')!;
expect(events).toEqual([
{
@ -349,7 +352,7 @@ describe('gridview', () => {
component: 'default',
});
const panel2 = gridview.getPanel('panel_2');
const panel2 = gridview.getPanel('panel_2')!;
expect(events).toEqual([
{
@ -368,7 +371,7 @@ describe('gridview', () => {
component: 'default',
});
const panel3 = gridview.getPanel('panel_3');
const panel3 = gridview.getPanel('panel_3')!;
expect(events).toEqual([
{
@ -1685,8 +1688,8 @@ describe('gridview', () => {
component: 'default',
});
const panel1 = gridview.getPanel('panel1');
const panel2 = gridview.getPanel('panel2');
const panel1 = gridview.getPanel('panel1')!;
const panel2 = gridview.getPanel('panel2')!;
const panel1Spy = jest.spyOn(panel1, 'dispose');
const panel2Spy = jest.spyOn(panel2, 'dispose');
@ -1714,8 +1717,8 @@ describe('gridview', () => {
component: 'default',
});
const panel1 = gridview.getPanel('panel1');
const panel2 = gridview.getPanel('panel2');
const panel1 = gridview.getPanel('panel1')!;
const panel2 = gridview.getPanel('panel2')!;
const panel1Spy = jest.spyOn(panel1, 'dispose');
const panel2Spy = jest.spyOn(panel2, 'dispose');
@ -1743,8 +1746,8 @@ describe('gridview', () => {
component: 'default',
});
const panel1 = gridview.getPanel('panel1');
const panel2 = gridview.getPanel('panel2');
const panel1 = gridview.getPanel('panel1')!;
const panel2 = gridview.getPanel('panel2')!;
const panel1Spy = jest.spyOn(panel1, 'dispose');
const panel2Spy = jest.spyOn(panel2, 'dispose');

View File

@ -4,7 +4,11 @@ import { GroupPanel } from '../../groupview/groupviewPanel';
describe('gridviewPanel', () => {
test('get panel', () => {
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any;
return {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
} as any;
});
const accessor = new accessorMock();

View File

@ -225,6 +225,8 @@ describe('groupview', () => {
id: 'dockview-1',
removePanel: removePanelMock,
removeGroup: removeGroupMock,
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
}) as DockviewComponent;
options = {
@ -616,6 +618,8 @@ describe('groupview', () => {
showDndOverlay: jest.fn(),
},
getPanel: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
};
});
const accessor = new accessorMock() as DockviewComponent;
@ -671,6 +675,8 @@ describe('groupview', () => {
},
getPanel: jest.fn(),
doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
};
});
const accessor = new accessorMock() as DockviewComponent;
@ -724,7 +730,7 @@ describe('groupview', () => {
).toBe(0);
});
test('that should allow drop when not dropping on self for same component id', () => {
test('that should not allow drop when dropping on self for same component id', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
@ -733,6 +739,8 @@ describe('groupview', () => {
},
getPanel: jest.fn(),
doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
};
});
const accessor = new accessorMock() as DockviewComponent;
@ -784,7 +792,7 @@ describe('groupview', () => {
expect(
element.getElementsByClassName('drop-target-dropzone').length
).toBe(1);
).toBe(0);
});
test('that should not allow drop when not dropping for different component id', () => {
@ -796,6 +804,8 @@ describe('groupview', () => {
},
getPanel: jest.fn(),
doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
};
});
const accessor = new accessorMock() as DockviewComponent;

View File

@ -12,7 +12,11 @@ import { TestPanel } from '../groupview.spec';
describe('tabsContainer', () => {
test('that an external event does not render a drop target and calls through to the group mode', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {};
return {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
});
const groupviewMock = jest.fn<Partial<Groupview>, []>(() => {
return {
@ -31,7 +35,7 @@ describe('tabsContainer', () => {
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as GroupPanel;
const cut = new TabsContainer(accessor, groupPanel, {});
const cut = new TabsContainer(accessor, groupPanel);
const emptySpace = cut.element
.getElementsByClassName('void-container')
@ -62,6 +66,9 @@ describe('tabsContainer', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
});
const groupviewMock = jest.fn<Partial<Groupview>, []>(() => {
@ -83,7 +90,7 @@ describe('tabsContainer', () => {
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as GroupPanel;
const cut = new TabsContainer(accessor, groupPanel, {});
const cut = new TabsContainer(accessor, groupPanel);
const emptySpace = cut.element
.getElementsByClassName('void-container')
@ -125,6 +132,9 @@ describe('tabsContainer', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
});
const groupviewMock = jest.fn<Partial<Groupview>, []>(() => {
@ -146,7 +156,7 @@ describe('tabsContainer', () => {
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as GroupPanel;
const cut = new TabsContainer(accessor, groupPanel, {});
const cut = new TabsContainer(accessor, groupPanel);
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
@ -185,6 +195,9 @@ describe('tabsContainer', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
});
const groupviewMock = jest.fn<Partial<Groupview>, []>(() => {
@ -206,7 +219,7 @@ describe('tabsContainer', () => {
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as GroupPanel;
const cut = new TabsContainer(accessor, groupPanel, {});
const cut = new TabsContainer(accessor, groupPanel);
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));
@ -245,6 +258,9 @@ describe('tabsContainer', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
});
const groupviewMock = jest.fn<Partial<Groupview>, []>(() => {
@ -265,7 +281,7 @@ describe('tabsContainer', () => {
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as GroupPanel;
const cut = new TabsContainer(accessor, groupPanel, {});
const cut = new TabsContainer(accessor, groupPanel);
cut.openPanel(new TestPanel('panel1', jest.fn() as any));
cut.openPanel(new TestPanel('panel2', jest.fn() as any));

View File

@ -325,6 +325,10 @@ export class GridviewApi implements CommonApi<SerializedGridview> {
}
export class DockviewApi implements CommonApi<SerializedDockview> {
get id(): string {
return this.component.id;
}
get width(): number {
return this.component.width;
}
@ -435,8 +439,8 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.addPanel(options);
}
addEmptyGroup(options?: AddGroupOptions): void {
this.component.addEmptyGroup(options);
addGroup(options?: AddGroupOptions): IGroupviewPanel {
return this.component.addGroup(options);
}
moveToNext(options?: MovementOptions): void {

View File

@ -1,4 +1,5 @@
import { Emitter, Event } from '../events';
import { IPanel } from '../panel/types';
import { FunctionOrValue } from '../types';
import { PanelApiImpl, PanelApi } from './panelApi';
@ -48,7 +49,7 @@ export class GridviewPanelApiImpl
readonly onDidSizeChange: Event<SizeEvent> = this._onDidSizeChange.event;
//
constructor(id: string) {
constructor(id: string, panel?: IPanel) {
super(id);
this.addDisposables(
@ -56,6 +57,10 @@ export class GridviewPanelApiImpl
this._onDidConstraintsChange,
this._onDidSizeChange
);
if (panel) {
this.initialize(panel);
}
}
public setConstraints(value: GridConstraintChangeEvent) {

View File

@ -71,6 +71,9 @@ export class DockviewPanelApiImpl
constructor(private panel: IDockviewPanel, group: GroupPanel) {
super(panel.id);
this.initialize(panel);
this._group = group;
this.addDisposables(

View File

@ -1,5 +1,6 @@
import { Emitter, Event } from '../events';
import { CompositeDisposable } from '../lifecycle';
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { IPanel, Parameters } from '../panel/types';
export interface FocusEvent {
readonly isFocused: boolean;
@ -25,6 +26,7 @@ export interface PanelApi {
readonly onDidActiveChange: Event<ActiveEvent>;
setVisible(isVisible: boolean): void;
setActive(): void;
updateParameters(parameters: Parameters): void;
/**
* The id of the panel that would have been assigned when the panel was created
*/
@ -61,6 +63,8 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
private _width = 0;
private _height = 0;
private readonly panelUpdatesDisposable = new MutableDisposable();
readonly _onDidDimensionChange = new Emitter<PanelDimensionChangeEvent>({
replay: true,
});
@ -94,6 +98,10 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
readonly _onActiveChange = new Emitter<void>();
readonly onActiveChange: Event<void> = this._onActiveChange.event;
//
readonly _onUpdateParameters = new Emitter<Parameters>();
readonly onUpdateParameters: Event<Parameters> =
this._onUpdateParameters.event;
//
get isFocused() {
return this._isFocused;
@ -118,6 +126,7 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
super();
this.addDisposables(
this.panelUpdatesDisposable,
this._onDidDimensionChange,
this._onDidChangeFocus,
this._onDidVisibilityChange,
@ -125,6 +134,7 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
this._onFocusEvent,
this._onActiveChange,
this._onVisibilityChange,
this._onUpdateParameters,
this.onDidFocusChange((event) => {
this._isFocused = event.isFocused;
}),
@ -141,6 +151,18 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
);
}
public initialize(panel: IPanel): void {
this.panelUpdatesDisposable.value = this._onUpdateParameters.event(
(parameters) => {
panel.update({
params: {
params: parameters,
},
});
}
);
}
setVisible(isVisible: boolean) {
this._onVisibilityChange.fire({ isVisible });
}
@ -149,6 +171,10 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
this._onActiveChange.fire();
}
updateParameters(parameters: Parameters): void {
this._onUpdateParameters.fire(parameters);
}
dispose() {
super.dispose();
}

View File

@ -6,16 +6,11 @@ export interface IDragAndDropObserverCallbacks {
onDragLeave: (e: DragEvent) => void;
onDrop: (e: DragEvent) => void;
onDragEnd: (e: DragEvent) => void;
onDragOver?: (e: DragEvent) => void;
}
export class DragAndDropObserver extends CompositeDisposable {
// A helper to fix issues with repeated DRAG_ENTER / DRAG_LEAVE
// calls see https://github.com/microsoft/vscode/issues/14470
// when the element has child elements where the events are fired
// repeadedly.
private counter = 0;
private target: EventTarget | null = null;
constructor(
private element: HTMLElement,
@ -28,28 +23,37 @@ export class DragAndDropObserver extends CompositeDisposable {
private registerListeners(): void {
this.addDisposables(
addDisposableListener(this.element, 'dragenter', (e: DragEvent) => {
this.counter++;
this.callbacks.onDragEnter(e);
})
addDisposableListener(
this.element,
'dragenter',
(e: DragEvent) => {
this.target = e.target;
this.callbacks.onDragEnter(e);
},
true
)
);
this.addDisposables(
addDisposableListener(this.element, 'dragover', (e: DragEvent) => {
e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
addDisposableListener(
this.element,
'dragover',
(e: DragEvent) => {
e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
if (this.callbacks.onDragOver) {
this.callbacks.onDragOver(e);
}
})
if (this.callbacks.onDragOver) {
this.callbacks.onDragOver(e);
}
},
true
)
);
this.addDisposables(
addDisposableListener(this.element, 'dragleave', (e: DragEvent) => {
this.counter--;
if (this.target === e.target) {
this.target = null;
if (this.counter === 0) {
this.callbacks.onDragLeave(e);
}
})
@ -57,14 +61,13 @@ export class DragAndDropObserver extends CompositeDisposable {
this.addDisposables(
addDisposableListener(this.element, 'dragend', (e: DragEvent) => {
this.counter = 0;
this.target = null;
this.callbacks.onDragEnd(e);
})
);
this.addDisposables(
addDisposableListener(this.element, 'drop', (e: DragEvent) => {
this.counter = 0;
this.callbacks.onDrop(e);
})
);

View File

@ -19,22 +19,6 @@
will-change: transform;
pointer-events: none;
&.left {
transform: translateX(-25%) scaleX(0.5)
}
&.right {
transform: translateX(25%) scaleX(0.5)
}
&.top {
transform: translateY(-25%) scaleY(0.5);
}
&.bottom {
transform: translateY(25%) scaleY(0.5);
}
&.small-top {
border-top: 1px solid var(--dv-drag-over-border-color);
}

View File

@ -2,23 +2,36 @@ import { toggleClass } from '../dom';
import { Emitter, Event } from '../events';
import { CompositeDisposable } from '../lifecycle';
import { DragAndDropObserver } from './dnd';
import { clamp } from '../math';
import { Direction } from '../gridview/baseComponentGridview';
export enum Position {
Top = 'Top',
Left = 'Left',
Bottom = 'Bottom',
Right = 'Right',
Center = 'Center',
function numberOrFallback(maybeNumber: any, fallback: number): number {
return typeof maybeNumber === 'number' ? maybeNumber : fallback;
}
export type Quadrant = 'top' | 'bottom' | 'left' | 'right';
export function directionToPosition(direction: Direction): Position {
switch (direction) {
case 'above':
return 'top';
case 'below':
return 'bottom';
case 'left':
return 'left';
case 'right':
return 'right';
case 'within':
return 'center';
default:
throw new Error(`invalid direction '${direction}'`);
}
}
export interface DroptargetEvent {
position: Position;
nativeEvent: DragEvent;
readonly position: Position;
readonly nativeEvent: DragEvent;
}
export type DropTargetDirections = 'vertical' | 'horizontal' | 'all' | 'none';
export type Position = 'top' | 'bottom' | 'left' | 'right' | 'center';
function isBooleanValue(
canDisplayOverlay: CanDisplayOverlay
@ -28,7 +41,7 @@ function isBooleanValue(
export type CanDisplayOverlay =
| boolean
| ((dragEvent: DragEvent, state: Quadrant | null) => boolean);
| ((dragEvent: DragEvent, state: Position) => boolean);
export class Droptarget extends CompositeDisposable {
private target: HTMLElement | undefined;
@ -38,27 +51,31 @@ export class Droptarget extends CompositeDisposable {
private readonly _onDrop = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
get state() {
get state(): Position | undefined {
return this._state;
}
set validOverlays(value: DropTargetDirections) {
this.options.validOverlays = value;
}
set canDisplayOverlay(value: CanDisplayOverlay) {
this.options.canDisplayOverlay = value;
}
constructor(
private readonly element: HTMLElement,
private readonly options: {
canDisplayOverlay: CanDisplayOverlay;
validOverlays: DropTargetDirections;
acceptedTargetZones: Position[];
overlayModel?: {
size?: { value: number; type: 'pixels' | 'percentage' };
activationSize?: {
value: number;
type: 'pixels' | 'percentage';
};
};
}
) {
super();
// use a set to take advantage of #<set>.has
const acceptedTargetZonesSet = new Set(
this.options.acceptedTargetZones
);
this.addDisposables(
this._onDrop,
new DragAndDropObserver(this.element, {
@ -71,17 +88,25 @@ export class Droptarget extends CompositeDisposable {
return; // avoid div!0
}
const x = e.offsetX;
const y = e.offsetY;
const xp = (100 * x) / width;
const yp = (100 * y) / height;
const rect = (
e.currentTarget as HTMLElement
).getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const quadrant = this.calculateQuadrant(
this.options.validOverlays,
xp,
yp
acceptedTargetZonesSet,
x,
y,
width,
height
);
if (quadrant === null) {
this.removeDropTarget();
return;
}
if (isBooleanValue(this.options.canDisplayOverlay)) {
if (!this.options.canDisplayOverlay) {
return;
@ -95,14 +120,14 @@ export class Droptarget extends CompositeDisposable {
this.target.className = 'drop-target-dropzone';
this.overlay = document.createElement('div');
this.overlay.className = 'drop-target-selection';
this._state = Position.Center;
this._state = 'center';
this.target.appendChild(this.overlay);
this.element.classList.add('drop-target');
this.element.append(this.target);
}
if (this.options.validOverlays === 'none') {
if (this.options.acceptedTargetZones.length === 0) {
return;
}
@ -110,10 +135,7 @@ export class Droptarget extends CompositeDisposable {
return;
}
const isSmallX = width < 100;
const isSmallY = height < 100;
this.toggleClasses(quadrant, isSmallX, isSmallY);
this.toggleClasses(quadrant, width, height);
this.setState(quadrant);
},
@ -139,28 +161,69 @@ export class Droptarget extends CompositeDisposable {
);
}
public dispose() {
public dispose(): void {
this.removeDropTarget();
}
private toggleClasses(
quadrant: Quadrant | null,
isSmallX: boolean,
isSmallY: boolean
) {
quadrant: Position,
width: number,
height: number
): void {
if (!this.overlay) {
return;
}
const isSmallX = width < 100;
const isSmallY = height < 100;
const isLeft = quadrant === 'left';
const isRight = quadrant === 'right';
const isTop = quadrant === 'top';
const isBottom = quadrant === 'bottom';
toggleClass(this.overlay, 'right', !isSmallX && isRight);
toggleClass(this.overlay, 'left', !isSmallX && isLeft);
toggleClass(this.overlay, 'top', !isSmallY && isTop);
toggleClass(this.overlay, 'bottom', !isSmallY && isBottom);
const rightClass = !isSmallX && isRight;
const leftClass = !isSmallX && isLeft;
const topClass = !isSmallY && isTop;
const bottomClass = !isSmallY && isBottom;
let size = 0.5;
if (this.options.overlayModel?.size?.type === 'percentage') {
size = clamp(this.options.overlayModel.size.value, 0, 100) / 100;
}
if (this.options.overlayModel?.size?.type === 'pixels') {
if (rightClass || leftClass) {
size =
clamp(0, this.options.overlayModel.size.value, width) /
width;
}
if (topClass || bottomClass) {
size =
clamp(0, this.options.overlayModel.size.value, height) /
height;
}
}
const translate = (1 - size) / 2;
const scale = size;
let transform: string;
if (rightClass) {
transform = `translateX(${100 * translate}%) scaleX(${scale})`;
} else if (leftClass) {
transform = `translateX(-${100 * translate}%) scaleX(${scale})`;
} else if (topClass) {
transform = `translateY(-${100 * translate}%) scaleY(${scale})`;
} else if (bottomClass) {
transform = `translateY(${100 * translate}%) scaleY(${scale})`;
} else {
transform = '';
}
this.overlay.style.transform = transform;
toggleClass(this.overlay, 'small-right', isSmallX && isRight);
toggleClass(this.overlay, 'small-left', isSmallX && isLeft);
@ -168,60 +231,61 @@ export class Droptarget extends CompositeDisposable {
toggleClass(this.overlay, 'small-bottom', isSmallY && isBottom);
}
private setState(quadrant: Quadrant | null) {
private setState(quadrant: Position): void {
switch (quadrant) {
case 'top':
this._state = Position.Top;
this._state = 'top';
break;
case 'left':
this._state = Position.Left;
this._state = 'left';
break;
case 'bottom':
this._state = Position.Bottom;
this._state = 'bottom';
break;
case 'right':
this._state = Position.Right;
this._state = 'right';
break;
default:
this._state = Position.Center;
case 'center':
this._state = 'center';
break;
}
}
private calculateQuadrant(
overlayType: DropTargetDirections,
xp: number,
yp: number
): Quadrant | null {
switch (overlayType) {
case 'all':
if (xp < 20) {
return 'left';
}
if (xp > 80) {
return 'right';
}
if (yp < 20) {
return 'top';
}
if (yp > 80) {
return 'bottom';
}
break;
case 'vertical':
if (yp < 50) {
return 'top';
}
return 'bottom';
overlayType: Set<Position>,
x: number,
y: number,
width: number,
height: number
): Position | null {
const isPercentage =
this.options.overlayModel?.activationSize === undefined ||
this.options.overlayModel?.activationSize?.type === 'percentage';
case 'horizontal':
if (xp < 50) {
return 'left';
}
return 'right';
const value = numberOrFallback(
this.options?.overlayModel?.activationSize?.value,
20
);
if (isPercentage) {
return calculateQuadrantAsPercentage(
overlayType,
x,
y,
width,
height,
value
);
}
return null;
return calculateQuadrantAsPixels(
overlayType,
x,
y,
width,
height,
value
);
}
private removeDropTarget() {
@ -229,7 +293,67 @@ export class Droptarget extends CompositeDisposable {
this._state = undefined;
this.element.removeChild(this.target);
this.target = undefined;
this.overlay = undefined;
this.element.classList.remove('drop-target');
}
}
}
export function calculateQuadrantAsPercentage(
overlayType: Set<Position>,
x: number,
y: number,
width: number,
height: number,
threshold: number
): Position | null {
const xp = (100 * x) / width;
const yp = (100 * y) / height;
if (overlayType.has('left') && xp < threshold) {
return 'left';
}
if (overlayType.has('right') && xp > 100 - threshold) {
return 'right';
}
if (overlayType.has('top') && yp < threshold) {
return 'top';
}
if (overlayType.has('bottom') && yp > 100 - threshold) {
return 'bottom';
}
if (!overlayType.has('center')) {
return null;
}
return 'center';
}
export function calculateQuadrantAsPixels(
overlayType: Set<Position>,
x: number,
y: number,
width: number,
height: number,
threshold: number
): Position | null {
if (overlayType.has('left') && x < threshold) {
return 'left';
}
if (overlayType.has('right') && x > width - threshold) {
return 'right';
}
if (overlayType.has('top') && y < threshold) {
return 'top';
}
if (overlayType.has('bottom') && y > height - threshold) {
return 'bottom';
}
if (!overlayType.has('center')) {
return null;
}
return 'center';
}

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@ import { Parameters } from '../panel/types';
import { IGroupPanelView } from './defaultGroupPanelView';
import { DockviewComponent } from './dockviewComponent';
export class DockviewGroupPanel
export class DockviewPanel
extends CompositeDisposable
implements IDockviewPanel
{
@ -38,7 +38,7 @@ export class DockviewGroupPanel
return this._group;
}
get view() {
get view(): IGroupPanelView | undefined {
return this._view;
}

View File

@ -14,6 +14,7 @@ import { FrameworkFactory } from '../types';
import { DockviewDropTargets } from '../groupview/dnd';
import { PanelTransfer } from '../dnd/dataTransfer';
import { IGroupControlRenderer } from '../react/dockview/groupControlsRenderer';
import { Position } from '../dnd/droptarget';
export interface GroupPanelFrameworkComponentFactory {
content: FrameworkFactory<IContentRenderer>;
@ -54,7 +55,8 @@ export interface ViewFactoryData {
export interface DockviewDndOverlayEvent {
nativeEvent: DragEvent;
target: DockviewDropTargets;
group: GroupPanel;
position: Position;
group?: GroupPanel;
getData: () => PanelTransfer | undefined;
}
@ -68,6 +70,7 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions {
defaultTabComponent?: string;
showDndOverlay?: (event: DockviewDndOverlayEvent) => boolean;
createGroupControlElement?: (group: GroupPanel) => IGroupControlRenderer;
singleTabMode?: 'fullwidth' | 'default';
}
export interface PanelOptions {
@ -78,19 +81,81 @@ export interface PanelOptions {
title?: string;
}
type RelativePanel = {
direction?: Direction;
referencePanel: string | IDockviewPanel;
};
type RelativeGroup = {
direction?: Direction;
referenceGroup: string | GroupPanel;
};
type AbsolutePosition = {
direction: Omit<Direction, 'within'>;
};
export type AddPanelPositionOptions =
| RelativePanel
| RelativeGroup
| AbsolutePosition;
export function isPanelOptionsWithPanel(
data: AddPanelPositionOptions
): data is RelativePanel {
if ((data as RelativePanel).referencePanel) {
return true;
}
return false;
}
export function isPanelOptionsWithGroup(
data: AddPanelPositionOptions
): data is RelativeGroup {
if ((data as RelativeGroup).referenceGroup) {
return true;
}
return false;
}
export interface AddPanelOptions
extends Omit<PanelOptions, 'component' | 'tabComponent'> {
component: string;
tabComponent?: string;
position?: {
direction?: Direction;
referencePanel?: string;
};
position?: AddPanelPositionOptions;
}
export interface AddGroupOptions {
direction?: 'left' | 'right' | 'above' | 'below';
referencePanel: string;
type AddGroupOptionsWithPanel = {
referencePanel: string | IDockviewPanel;
direction?: Omit<Direction, 'within'>;
};
type AddGroupOptionsWithGroup = {
referenceGroup: string | GroupPanel;
direction?: Omit<Direction, 'within'>;
};
export type AddGroupOptions =
| AddGroupOptionsWithGroup
| AddGroupOptionsWithPanel
| AbsolutePosition;
export function isGroupOptionsWithPanel(
data: AddGroupOptions
): data is AddGroupOptionsWithPanel {
if ((data as AddGroupOptionsWithPanel).referencePanel) {
return true;
}
return false;
}
export function isGroupOptionsWithGroup(
data: AddGroupOptions
): data is AddGroupOptionsWithGroup {
if ((data as AddGroupOptionsWithGroup).referenceGroup) {
return true;
}
return false;
}
export interface MovementOptions2 {

View File

@ -15,19 +15,19 @@ const nextLayoutId = sequentialNumberGenerator();
export type Direction = 'left' | 'right' | 'above' | 'below' | 'within';
export function toTarget(direction: Direction) {
export function toTarget(direction: Direction): Position {
switch (direction) {
case 'left':
return Position.Left;
return 'left';
case 'right':
return Position.Right;
return 'right';
case 'above':
return Position.Top;
return 'top';
case 'below':
return Position.Bottom;
return 'bottom';
case 'within':
default:
return Position.Center;
return 'center';
}
}

View File

@ -6,7 +6,7 @@ import {
PanelInitParameters,
IPanel,
} from '../panel/types';
import { PanelApiImpl } from '../api/panelApi';
import { PanelApi, PanelApiImpl } from '../api/panelApi';
export interface BasePanelViewState {
id: string;
@ -14,7 +14,7 @@ export interface BasePanelViewState {
params?: Record<string, any>;
}
export interface BasePanelViewExported<T extends PanelApiImpl> {
export interface BasePanelViewExported<T extends PanelApi> {
readonly id: string;
readonly api: T;
readonly width: number;

View File

@ -9,13 +9,13 @@ import {
Orientation,
Sizing,
} from '../splitview/core/splitview';
import { Position } from '../dnd/droptarget';
import { tail } from '../array';
import { LeafNode } from './leafNode';
import { BranchNode } from './branchNode';
import { Node } from './types';
import { Emitter, Event } from '../events';
import { IDisposable, MutableDisposable } from '../lifecycle';
import { Position } from '../dnd/droptarget';
function findLeaf(candiateNode: Node, last: boolean): LeafNode {
if (candiateNode instanceof LeafNode) {
@ -132,22 +132,19 @@ export function getRelativeLocation(
const [rest, _index] = tail(location);
let index = _index;
if (direction === Position.Right || direction === Position.Bottom) {
if (direction === 'right' || direction === 'bottom') {
index += 1;
}
return [...rest, index];
} else {
const index =
direction === Position.Right || direction === Position.Bottom
? 1
: 0;
const index = direction === 'right' || direction === 'bottom' ? 1 : 0;
return [...location, index];
}
}
export function getDirectionOrientation(direction: Position): Orientation {
return direction === Position.Top || direction === Position.Bottom
return direction === 'top' || direction === 'bottom'
? Orientation.VERTICAL
: Orientation.HORIZONTAL;
}
@ -276,6 +273,10 @@ export class Gridview implements IDisposable {
readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> =
this._onDidChange.event;
public get length(): number {
return this._root ? this._root.children.length : 0;
}
public serialize() {
const root = serializeBranchNode(this.getView(), this.orientation);
@ -410,6 +411,43 @@ export class Gridview implements IDisposable {
});
}
/**
* If the root is orientated as a VERTICAL node then nest the existing root within a new HORIZIONTAL root node
* If the root is orientated as a HORIZONTAL node then nest the existing root within a new VERITCAL root node
*/
public insertOrthogonalSplitviewAtRoot(): void {
if (!this._root) {
return;
}
const oldRoot = this.root;
oldRoot.element.remove();
this._root = new BranchNode(
orthogonal(oldRoot.orientation),
this.proportionalLayout,
this.styles,
this.root.orthogonalSize,
this.root.size
);
if (oldRoot.children.length === 1) {
// can remove one level of redundant branching if there is only a single child
const childReference = oldRoot.children[0];
oldRoot.removeChild(0); // remove to prevent disposal when disposing of unwanted root
oldRoot.dispose();
this._root.addChild(childReference, Sizing.Distribute, 0);
} else {
this._root.addChild(oldRoot, Sizing.Distribute, 0);
}
this.element.appendChild(this._root.element);
this.disposable.value = this._root.onDidChange((e) => {
this._onDidChange.fire(e);
});
}
public next(location: number[]) {
return this.progmaticSelect(location);
}

View File

@ -3,7 +3,6 @@ import {
SerializedGridObject,
getGridLocation,
} from './gridview';
import { Position } from '../dnd/droptarget';
import { tail, sequenceEquals } from '../array';
import { CompositeDisposable } from '../lifecycle';
import { IPanelDeserializer } from '../dockview/deserializer';
@ -25,6 +24,7 @@ import { BaseComponentOptions } from '../panel/types';
import { Orientation, Sizing } from '../splitview/core/splitview';
import { createComponent } from '../panel/componentFactory';
import { Emitter, Event } from '../events';
import { Position } from '../dnd/droptarget';
export interface SerializedGridview {
grid: {
@ -265,7 +265,7 @@ export class GridviewComponent
}
const target = toTarget(options.direction);
if (target === Position.Center) {
if (target === 'center') {
throw new Error(`${target} not supported as an option`);
} else {
const location = getGridLocation(referenceGroup.element);
@ -294,7 +294,7 @@ export class GridviewComponent
}
const target = toTarget(options.position.direction);
if (target === Position.Center) {
if (target === 'center') {
throw new Error(`${target} not supported as an option`);
} else {
const location = getGridLocation(referenceGroup.element);

View File

@ -9,7 +9,10 @@ import {
BasePanelViewExported,
BasePanelViewState,
} from './basePanelView';
import { GridviewPanelApiImpl } from '../api/gridviewPanelApi';
import {
GridviewPanelApi,
GridviewPanelApiImpl,
} from '../api/gridviewPanelApi';
import { LayoutPriority } from '../splitview/core/splitview';
import { Emitter, Event } from '../events';
import { IViewSize } from './gridview';
@ -26,7 +29,7 @@ export interface GridviewInitParameters extends PanelInitParameters {
}
export interface IGridviewPanel
extends BasePanelViewExported<GridviewPanelApiImpl> {
extends BasePanelViewExported<GridviewPanelApi> {
readonly minimumWidth: number;
readonly maximumWidth: number;
readonly minimumHeight: number;
@ -123,13 +126,11 @@ export abstract class GridviewPanel
return this.api.isActive;
}
constructor(
id: string,
component: string,
api = new GridviewPanelApiImpl(id)
) {
constructor(id: string, component: string, api: GridviewPanelApiImpl) {
super(id, component, api);
this.api.initialize(this); // TODO: required to by-pass 'super before this' requirement
this.addDisposables(
this._onDidChange,
this.api.onVisibilityChange((event) => {

View File

@ -2,4 +2,5 @@ export enum DockviewDropTargets {
Tab,
Panel,
TabContainer,
Edge,
}

View File

@ -110,7 +110,11 @@ export interface IGroupview extends IDisposable, IGridPanelView {
panel?: IDockviewPanel;
suppressRoll?: boolean;
}): void;
canDisplayOverlay(event: DragEvent, target: DockviewDropTargets): boolean;
canDisplayOverlay(
event: DragEvent,
position: Position,
target: DockviewDropTargets
): boolean;
}
export class Groupview extends CompositeDisposable implements IGroupview {
@ -167,6 +171,8 @@ export class Groupview extends CompositeDisposable implements IGroupview {
set locked(value: boolean) {
this._locked = value;
toggleClass(this.container, 'locked-groupview', value);
}
get isActive(): boolean {
@ -226,45 +232,48 @@ export class Groupview extends CompositeDisposable implements IGroupview {
private accessor: DockviewComponent,
public id: string,
private readonly options: GroupOptions,
private readonly parent: GroupPanel
private readonly groupPanel: GroupPanel
) {
super();
this.container.classList.add('groupview');
this.tabsContainer = new TabsContainer(this.accessor, this.parent, {
tabHeight: options.tabHeight,
});
this.tabsContainer = new TabsContainer(this.accessor, this.groupPanel);
this.contentContainer = new ContentContainer();
this.dropTarget = new Droptarget(this.contentContainer.element, {
validOverlays: 'all',
canDisplayOverlay: (event, quadrant) => {
if (this.locked && !quadrant) {
acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'],
canDisplayOverlay: (event, position) => {
if (this.locked && position === 'center') {
return false;
}
const data = getPanelData();
if (
data &&
data.panelId === null &&
data.viewId === this.accessor.id &&
data.groupId !== this.id
) {
// prevent dropping on self for group dnd
return true;
}
if (data && data.viewId === this.accessor.id) {
if (data.groupId === this.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._panels.length === 1 && data.groupId === this.id;
return !groupHasOnePanelAndIsActiveDragElement;
}
return this.canDisplayOverlay(event, DockviewDropTargets.Panel);
return this.canDisplayOverlay(
event,
position,
DockviewDropTargets.Panel
);
},
});
@ -284,10 +293,10 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this._onDidRemovePanel,
this._onDidActivePanelChange,
this.tabsContainer.onDrop((event) => {
this.handleDropEvent(event.event, Position.Center, event.index);
this.handleDropEvent(event.event, 'center', event.index);
}),
this.contentContainer.onDidFocus(() => {
this.accessor.doSetGroupActive(this.parent, true);
this.accessor.doSetGroupActive(this.groupPanel, true);
}),
this.contentContainer.onDidBlur(() => {
// noop
@ -316,12 +325,12 @@ export class Groupview extends CompositeDisposable implements IGroupview {
if (this.accessor.options.createGroupControlElement) {
this._control = this.accessor.options.createGroupControlElement(
this.parent
this.groupPanel
);
this.addDisposables(this._control);
this._control.init({
containerApi: new DockviewApi(this.accessor),
api: this.parent.api,
api: this.groupPanel.api,
});
this.tabsContainer.setActionElement(this._control.element);
}
@ -441,11 +450,11 @@ export class Groupview extends CompositeDisposable implements IGroupview {
const skipSetGroupActive = !!options.skipSetGroupActive;
// ensure the group is updated before we fire any events
panel.updateParentGroup(this.parent, true);
panel.updateParentGroup(this.groupPanel, true);
if (this._activePanel === panel) {
if (!skipSetGroupActive) {
this.accessor.doSetGroupActive(this.parent);
this.accessor.doSetGroupActive(this.groupPanel);
}
return;
}
@ -457,7 +466,10 @@ export class Groupview extends CompositeDisposable implements IGroupview {
}
if (!skipSetGroupActive) {
this.accessor.doSetGroupActive(this.parent, !!options.skipFocus);
this.accessor.doSetGroupActive(
this.groupPanel,
!!options.skipFocus
);
}
this.updateContainer();
@ -486,7 +498,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.doClose(panel);
}
} else {
this.accessor.removeGroup(this.parent);
this.accessor.removeGroup(this.groupPanel);
}
}
@ -643,7 +655,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
toggleClass(this.container, 'empty', this.isEmpty);
this.panels.forEach((panel) =>
panel.updateParentGroup(this.parent, this.isActive)
panel.updateParentGroup(this.groupPanel, this.isActive)
);
if (this.isEmpty && !this.watermark) {
@ -658,14 +670,14 @@ export class Groupview extends CompositeDisposable implements IGroupview {
addDisposableListener(this.watermark.element, 'click', () => {
if (!this.isActive) {
this.accessor.doSetGroupActive(this.parent);
this.accessor.doSetGroupActive(this.groupPanel);
}
});
this.tabsContainer.hide();
this.contentContainer.element.appendChild(this.watermark.element);
this.watermark.updateParentGroup(this.parent, true);
this.watermark.updateParentGroup(this.groupPanel, true);
}
if (!this.isEmpty && this.watermark) {
this.watermark.element.remove();
@ -675,13 +687,18 @@ export class Groupview extends CompositeDisposable implements IGroupview {
}
}
canDisplayOverlay(event: DragEvent, target: DockviewDropTargets): boolean {
canDisplayOverlay(
event: DragEvent,
position: Position,
target: DockviewDropTargets
): boolean {
// custom overlay handler
if (this.accessor.options.showDndOverlay) {
return this.accessor.options.showDndOverlay({
nativeEvent: event,
target,
group: this.accessor.getPanel(this.id)!,
position,
getData: getPanelData,
});
}

View File

@ -8,28 +8,16 @@ import {
import { toggleClass } from '../dom';
import { IDockviewComponent } from '../dockview/dockviewComponent';
import { ITabRenderer } from './types';
import { IDockviewPanel } from './groupPanel';
import { GroupPanel } from './groupviewPanel';
import { DroptargetEvent, Droptarget } from '../dnd/droptarget';
import { DockviewDropTargets } from './dnd';
import { DragHandler } from '../dnd/abstractDragHandler';
export enum MouseEventKind {
CLICK = 'CLICK',
}
export interface LayoutMouseEvent {
readonly kind: MouseEventKind;
readonly event: MouseEvent;
readonly panel?: IDockviewPanel;
readonly tab?: boolean;
}
export interface ITab {
readonly panelId: string;
readonly element: HTMLElement;
setContent: (element: ITabRenderer) => void;
onChanged: Event<LayoutMouseEvent>;
onChanged: Event<MouseEvent>;
onDrop: Event<DroptargetEvent>;
setActive(isActive: boolean): void;
}
@ -39,13 +27,13 @@ export class Tab extends CompositeDisposable implements ITab {
private readonly droptarget: Droptarget;
private content?: ITabRenderer;
private readonly _onChanged = new Emitter<LayoutMouseEvent>();
readonly onChanged: Event<LayoutMouseEvent> = this._onChanged.event;
private readonly _onChanged = new Emitter<MouseEvent>();
readonly onChanged: Event<MouseEvent> = this._onChanged.event;
private readonly _onDropped = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDropped.event;
public get element() {
public get element(): HTMLElement {
return this._element;
}
@ -104,20 +92,34 @@ export class Tab extends CompositeDisposable implements ITab {
*/
event.stopPropagation();
this._onChanged.fire({ kind: MouseEventKind.CLICK, event });
this._onChanged.fire(event);
})
);
this.droptarget = new Droptarget(this._element, {
validOverlays: 'none',
canDisplayOverlay: (event) => {
acceptedTargetZones: ['center'],
canDisplayOverlay: (event, position) => {
if (this.group.locked) {
return false;
}
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.panelId !== data.panelId;
}
return this.group.model.canDisplayOverlay(
event,
position,
DockviewDropTargets.Tab
);
},
@ -130,12 +132,12 @@ export class Tab extends CompositeDisposable implements ITab {
);
}
public setActive(isActive: boolean) {
public setActive(isActive: boolean): void {
toggleClass(this.element, 'active-tab', isActive);
toggleClass(this.element, 'inactive-tab', !isActive);
}
public setContent(part: ITabRenderer) {
public setContent(part: ITabRenderer): void {
if (this.content) {
this._element.removeChild(this.content.element);
}
@ -143,7 +145,7 @@ export class Tab extends CompositeDisposable implements ITab {
this._element.appendChild(this.content.element);
}
public dispose() {
public dispose(): void {
super.dispose();
this.droptarget.dispose();
}

View File

@ -10,6 +10,20 @@
display: none;
}
&.dv-single-tab.dv-full-width-single-tab {
.tabs-container {
flex-grow: 1;
.tab {
flex-grow: 1;
}
}
.void-container {
flex-grow: 0;
}
}
.void-container {
display: flex;
flex-grow: 1;

View File

@ -4,11 +4,12 @@ import {
IValueDisposable,
} from '../../lifecycle';
import { addDisposableListener, Emitter, Event } from '../../events';
import { ITab, MouseEventKind, Tab } from '../tab';
import { ITab, Tab } from '../tab';
import { IDockviewPanel } from '../groupPanel';
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { GroupPanel } from '../groupviewPanel';
import { VoidContainer } from './voidContainer';
import { toggleClass } from '../../dom';
export interface TabDropIndexEvent {
event: DragEvent;
@ -134,8 +135,7 @@ export class TabsContainer
constructor(
private readonly accessor: DockviewComponent,
private readonly group: GroupPanel,
readonly options: { tabHeight?: number }
private readonly group: GroupPanel
) {
super();
@ -144,7 +144,34 @@ export class TabsContainer
this._element = document.createElement('div');
this._element.className = 'tabs-and-actions-container';
this.height = options.tabHeight;
this.height = accessor.options.tabHeight;
toggleClass(
this._element,
'dv-full-width-single-tab',
this.accessor.options.singleTabMode === 'fullwidth'
);
this.addDisposables(
this.accessor.onDidAddPanel((e) => {
if (e.api.group === this.group) {
toggleClass(
this._element,
'dv-single-tab',
this.size === 1
);
}
}),
this.accessor.onDidRemovePanel((e) => {
if (e.api.group === this.group) {
toggleClass(
this._element,
'dv-single-tab',
this.size === 1
);
}
})
);
this.actionContainer = document.createElement('div');
this.actionContainer.className = 'action-container';
@ -242,17 +269,15 @@ export class TabsContainer
panel.id === this.group.model.activePanel?.id &&
this.group.model.isContentFocused;
const isLeftClick = event.event.button === 0;
const isLeftClick = event.button === 0;
if (!isLeftClick || event.event.defaultPrevented) {
if (!isLeftClick || event.defaultPrevented) {
return;
}
if (event.kind === MouseEventKind.CLICK) {
this.group.model.openPanel(panel, {
skipFocus: alreadyFocused,
});
}
this.group.model.openPanel(panel, {
skipFocus: alreadyFocused,
});
}),
tabToAdd.onDrop((event) => {
this._onDrop.fire({

View File

@ -41,17 +41,26 @@ export class VoidContainer extends CompositeDisposable {
const handler = new GroupDragHandler(this._element, accessor.id, group);
this.voidDropTarget = new Droptarget(this._element, {
validOverlays: 'none',
canDisplayOverlay: (event) => {
acceptedTargetZones: ['center'],
canDisplayOverlay: (event, position) => {
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 group.model.canDisplayOverlay(
event,
position,
DockviewDropTargets.Panel
);
},

View File

@ -27,7 +27,7 @@ export * from './react'; // TODO: should be conditional on whether user wants th
export { Event } from './events';
export { IDisposable } from './lifecycle';
export { Position } from './dnd/droptarget';
export { Position as DropTargetDirections } from './dnd/droptarget';
export {
FocusEvent,
PanelDimensionChangeEvent,

View File

@ -4,7 +4,7 @@ import {
LocalSelectionTransfer,
PaneTransfer,
} from '../dnd/dataTransfer';
import { Droptarget, DroptargetEvent, Position } from '../dnd/droptarget';
import { Droptarget, DroptargetEvent } from '../dnd/droptarget';
import { Emitter } from '../events';
import { IDisposable } from '../lifecycle';
import { Orientation } from '../splitview/core/splitview';
@ -70,7 +70,10 @@ export abstract class DraggablePaneviewPanel extends PaneviewPanel {
})(this.header);
this.target = new Droptarget(this.element, {
validOverlays: 'vertical',
acceptedTargetZones: ['top', 'bottom'],
overlayModel: {
activationSize: { type: 'percentage', value: 50 },
},
canDisplayOverlay: (event) => {
const data = getPaneData();
@ -139,16 +142,10 @@ export abstract class DraggablePaneviewPanel extends PaneviewPanel {
const fromIndex = allPanels.indexOf(existingPanel);
let toIndex = containerApi.panels.indexOf(this);
if (
event.position === Position.Left ||
event.position === Position.Top
) {
if (event.position === 'left' || event.position === 'top') {
toIndex = Math.max(0, toIndex - 1);
}
if (
event.position === Position.Right ||
event.position === Position.Bottom
) {
if (event.position === 'right' || event.position === 'bottom') {
if (fromIndex > toIndex) {
toIndex++;
}

View File

@ -164,6 +164,8 @@ export abstract class PaneviewPanel
) {
super(id, component, new PaneviewPanelApiImpl(id));
this.api.pane = this; // TODO cannot use 'this' before 'super'
this.api.initialize(this);
this._isExpanded = isExpanded;
this._headerVisible = isHeaderVisible;

View File

@ -1,6 +1,6 @@
import { DockviewComponent } from '../dockview/dockviewComponent';
import { GroupviewPanelState, IDockviewPanel } from '../groupview/groupPanel';
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { DockviewPanel } from '../dockview/dockviewPanel';
import { IPanelDeserializer } from '../dockview/deserializer';
import { createComponent } from '../panel/componentFactory';
import { DockviewApi } from '../api/component.api';
@ -56,7 +56,7 @@ export class ReactPanelDeserialzier implements IPanelDeserializer {
tab,
});
const panel = new DockviewGroupPanel(
const panel = new DockviewPanel(
panelId,
this.layout,
new DockviewApi(this.layout),

View File

@ -69,6 +69,7 @@ export interface IDockviewReactProps {
disableAutoResizing?: boolean;
defaultTabComponent?: React.FunctionComponent<IDockviewPanelHeaderProps>;
groupControlComponent?: React.FunctionComponent<IDockviewGroupControlProps>;
singleTabMode?: 'fullwidth' | 'default';
}
export const DockviewReact = React.forwardRef(
@ -161,6 +162,7 @@ export const DockviewReact = React.forwardRef(
props.groupControlComponent,
{ addPortal }
),
singleTabMode: props.singleTabMode,
});
domRef.current?.appendChild(dockview.element);

View File

@ -1,4 +1,5 @@
import { GridviewApi } from '../../api/component.api';
import { GridviewPanelApiImpl } from '../../api/gridviewPanelApi';
import {
GridviewPanel,
GridviewInitParameters,
@ -14,7 +15,7 @@ export class ReactGridPanelView extends GridviewPanel {
private readonly reactComponent: React.FunctionComponent<IGridviewPanelProps>,
private readonly reactPortalStore: ReactPortalStore
) {
super(id, component);
super(id, component, new GridviewPanelApiImpl(id));
}
getComponent(): IFrameworkPart {

View File

@ -85,6 +85,8 @@ export abstract class SplitviewPanel
constructor(id: string, componentName: string) {
super(id, componentName, new SplitviewPanelApiImpl(id));
this.api.initialize(this);
this.addDisposables(
this._onDidChange,
this.api.onVisibilityChange((event) => {