refactor: improve droptarget component

This commit is contained in:
mathuo 2023-02-01 22:03:12 +07:00
parent c34e03f158
commit c41158cea6
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
9 changed files with 154 additions and 134 deletions

View File

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

View File

@ -2,15 +2,15 @@ import { Droptarget, Position } from '../../dnd/droptarget';
import { fireEvent } from '@testing-library/dom'; import { fireEvent } from '@testing-library/dom';
function createOffsetDragOverEvent(params: { function createOffsetDragOverEvent(params: {
offsetX: number; clientX: number;
offsetY: number; clientY: number;
}): Event { }): Event {
const event = new Event('dragover', { const event = new Event('dragover', {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
}); });
Object.defineProperty(event, 'offsetX', { get: () => params.offsetX }); Object.defineProperty(event, 'clientX', { get: () => params.clientX });
Object.defineProperty(event, 'offsetY', { get: () => params.offsetY }); Object.defineProperty(event, 'clientY', { get: () => params.clientY });
return event; return event;
} }
@ -32,7 +32,7 @@ describe('droptarget', () => {
droptarget = new Droptarget(element, { droptarget = new Droptarget(element, {
canDisplayOverlay: () => true, canDisplayOverlay: () => true,
validOverlays: 'none', acceptedTargetZones: ['center'],
}); });
droptarget.onDrop((event) => { droptarget.onDrop((event) => {
@ -54,7 +54,7 @@ describe('droptarget', () => {
droptarget = new Droptarget(element, { droptarget = new Droptarget(element, {
canDisplayOverlay: () => true, canDisplayOverlay: () => true,
validOverlays: 'all', acceptedTargetZones: ['top', 'left', 'right', 'bottom', 'center'],
}); });
droptarget.onDrop((event) => { droptarget.onDrop((event) => {
@ -73,7 +73,10 @@ describe('droptarget', () => {
fireEvent( fireEvent(
target, target,
createOffsetDragOverEvent({ offsetX: 19, offsetY: 0 }) createOffsetDragOverEvent({
clientX: 19,
clientY: 0,
})
); );
expect(position).toBeUndefined(); expect(position).toBeUndefined();
@ -84,7 +87,7 @@ describe('droptarget', () => {
test('default', () => { test('default', () => {
droptarget = new Droptarget(element, { droptarget = new Droptarget(element, {
canDisplayOverlay: () => true, canDisplayOverlay: () => true,
validOverlays: 'all', acceptedTargetZones: ['top', 'left', 'right', 'bottom', 'center'],
}); });
expect(droptarget.state).toBeUndefined(); expect(droptarget.state).toBeUndefined();
@ -106,56 +109,91 @@ describe('droptarget', () => {
fireEvent( fireEvent(
target, target,
createOffsetDragOverEvent({ offsetX: 19, offsetY: 0 }) createOffsetDragOverEvent({ clientX: 19, clientY: 0 })
); );
viewQuery = element.querySelectorAll( 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(viewQuery.length).toBe(1);
expect(droptarget.state).toBe(Position.Left); expect(droptarget.state).toBe(Position.Left);
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('translateX(-25%) scaleX(0.5)');
fireEvent( fireEvent(
target, target,
createOffsetDragOverEvent({ offsetX: 40, offsetY: 19 }) createOffsetDragOverEvent({ clientX: 40, clientY: 19 })
); );
viewQuery = element.querySelectorAll( 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(viewQuery.length).toBe(1);
expect(droptarget.state).toBe(Position.Top); expect(droptarget.state).toBe(Position.Top);
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('translateY(-25%) scaleY(0.5)');
fireEvent( fireEvent(
target, target,
createOffsetDragOverEvent({ offsetX: 160, offsetY: 81 }) createOffsetDragOverEvent({ clientX: 160, clientY: 81 })
); );
viewQuery = element.querySelectorAll( 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(viewQuery.length).toBe(1);
expect(droptarget.state).toBe(Position.Bottom); expect(droptarget.state).toBe(Position.Bottom);
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('translateY(25%) scaleY(0.5)');
fireEvent( fireEvent(
target, target,
createOffsetDragOverEvent({ offsetX: 161, offsetY: 0 }) createOffsetDragOverEvent({ clientX: 161, clientY: 0 })
); );
viewQuery = element.querySelectorAll( 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(viewQuery.length).toBe(1);
expect(droptarget.state).toBe(Position.Right); expect(droptarget.state).toBe(Position.Right);
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('translateX(25%) scaleX(0.5)');
fireEvent( fireEvent(
target, target,
createOffsetDragOverEvent({ offsetX: 100, offsetY: 50 }) createOffsetDragOverEvent({ clientX: 100, clientY: 50 })
); );
expect(droptarget.state).toBe(Position.Center); expect(droptarget.state).toBe(Position.Center);
expect(
(
element
.getElementsByClassName('drop-target-selection')
.item(0) as HTMLDivElement
).style.transform
).toBe('');
fireEvent.dragLeave(target); fireEvent.dragLeave(target);
expect(droptarget.state).toBeUndefined(); expect(droptarget.state).toBe(Position.Center);
viewQuery = element.querySelectorAll('.drop-target'); viewQuery = element.querySelectorAll('.drop-target');
expect(viewQuery.length).toBe(0); expect(viewQuery.length).toBe(0);
}); });

View File

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

View File

@ -221,6 +221,8 @@ describe('groupview', () => {
id: 'dockview-1', id: 'dockview-1',
removePanel: removePanelMock, removePanel: removePanelMock,
removeGroup: removeGroupMock, removeGroup: removeGroupMock,
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
}) as DockviewComponent; }) as DockviewComponent;
options = { options = {
@ -612,6 +614,8 @@ describe('groupview', () => {
showDndOverlay: jest.fn(), showDndOverlay: jest.fn(),
}, },
getPanel: jest.fn(), getPanel: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
}; };
}); });
const accessor = new accessorMock() as DockviewComponent; const accessor = new accessorMock() as DockviewComponent;
@ -667,6 +671,8 @@ describe('groupview', () => {
}, },
getPanel: jest.fn(), getPanel: jest.fn(),
doSetGroupActive: jest.fn(), doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
}; };
}); });
const accessor = new accessorMock() as DockviewComponent; const accessor = new accessorMock() as DockviewComponent;
@ -729,6 +735,8 @@ describe('groupview', () => {
}, },
getPanel: jest.fn(), getPanel: jest.fn(),
doSetGroupActive: jest.fn(), doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
}; };
}); });
const accessor = new accessorMock() as DockviewComponent; const accessor = new accessorMock() as DockviewComponent;
@ -792,6 +800,8 @@ describe('groupview', () => {
}, },
getPanel: jest.fn(), getPanel: jest.fn(),
doSetGroupActive: jest.fn(), doSetGroupActive: jest.fn(),
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
}; };
}); });
const accessor = new accessorMock() as DockviewComponent; const accessor = new accessorMock() as DockviewComponent;

View File

@ -12,7 +12,10 @@ import { TestPanel } from '../groupview.spec';
describe('tabsContainer', () => { describe('tabsContainer', () => {
test('that an external event does not render a drop target and calls through to the group mode', () => { test('that an external event does not render a drop target and calls through to the group mode', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => { const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {}; return {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
};
}); });
const groupviewMock = jest.fn<Partial<Groupview>, []>(() => { const groupviewMock = jest.fn<Partial<Groupview>, []>(() => {
return { return {
@ -62,6 +65,8 @@ describe('tabsContainer', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => { const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return { return {
id: 'testcomponentid', id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
}; };
}); });
const groupviewMock = jest.fn<Partial<Groupview>, []>(() => { const groupviewMock = jest.fn<Partial<Groupview>, []>(() => {
@ -125,6 +130,8 @@ describe('tabsContainer', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => { const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return { return {
id: 'testcomponentid', id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
}; };
}); });
const groupviewMock = jest.fn<Partial<Groupview>, []>(() => { const groupviewMock = jest.fn<Partial<Groupview>, []>(() => {
@ -185,6 +192,8 @@ describe('tabsContainer', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => { const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return { return {
id: 'testcomponentid', id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
}; };
}); });
const groupviewMock = jest.fn<Partial<Groupview>, []>(() => { const groupviewMock = jest.fn<Partial<Groupview>, []>(() => {
@ -245,6 +254,8 @@ describe('tabsContainer', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => { const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return { return {
id: 'testcomponentid', id: 'testcomponentid',
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
}; };
}); });
const groupviewMock = jest.fn<Partial<Groupview>, []>(() => { const groupviewMock = jest.fn<Partial<Groupview>, []>(() => {

View File

@ -54,10 +54,11 @@ export class Droptarget extends CompositeDisposable {
canDisplayOverlay: CanDisplayOverlay; canDisplayOverlay: CanDisplayOverlay;
acceptedTargetZones: DropTargetDirections[]; acceptedTargetZones: DropTargetDirections[];
overlayModel?: { overlayModel?: {
units?: 'pixels' | 'percentage'; size?: { value: number; type: 'pixels' | 'percentage' };
type?: 'modal' | 'line'; activationSize?: {
cover?: number; value: number;
directionalThreshold?: number; type: 'pixels' | 'percentage';
};
}; };
} }
) { ) {
@ -122,14 +123,7 @@ export class Droptarget extends CompositeDisposable {
return; return;
} }
const isSmallX = this.toggleClasses(quadrant, width, height);
this.options.overlayModel?.type === 'line' ||
width < 100;
const isSmallY =
this.options.overlayModel?.type === 'line' ||
height < 100;
this.toggleClasses(quadrant, isSmallX, isSmallY);
this.setState(quadrant); this.setState(quadrant);
}, },
@ -161,33 +155,50 @@ export class Droptarget extends CompositeDisposable {
private toggleClasses( private toggleClasses(
quadrant: Quadrant | null, quadrant: Quadrant | null,
isSmallX: boolean, width: number,
isSmallY: boolean height: number
) { ) {
if (!this.overlay) { if (!this.overlay) {
return; return;
} }
const isSmallX = width < 100;
const isSmallY = height < 100;
const isLeft = quadrant === 'left'; const isLeft = quadrant === 'left';
const isRight = quadrant === 'right'; const isRight = quadrant === 'right';
const isTop = quadrant === 'top'; const isTop = quadrant === 'top';
const isBottom = quadrant === 'bottom'; const isBottom = quadrant === 'bottom';
const size = const rightClass = !isSmallX && isRight;
typeof this.options.overlayModel?.cover === 'number' const leftClass = !isSmallX && isLeft;
? clamp(this.options.overlayModel?.cover, 0, 1) const topClass = !isSmallY && isTop;
: 0.5; 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 translate = (1 - size) / 2;
const scale = size; const scale = size;
let transform = ''; let transform = '';
const rightClass = !isSmallX && isRight;
const leftClass = !isSmallX && isLeft;
const topClass = !isSmallY && isTop;
const bottomClass = !isSmallY && isBottom;
if (rightClass) { if (rightClass) {
transform = `translateX(${100 * translate}%) scaleX(${scale})`; transform = `translateX(${100 * translate}%) scaleX(${scale})`;
} else if (leftClass) { } else if (leftClass) {
@ -202,11 +213,6 @@ export class Droptarget extends CompositeDisposable {
this.overlay.style.transform = transform; this.overlay.style.transform = transform;
// toggleClass(this.overlay, 'right', !isSmallX && isRight);
// toggleClass(this.overlay, 'left', !isSmallX && isLeft);
// toggleClass(this.overlay, 'top', !isSmallY && isTop);
// toggleClass(this.overlay, 'bottom', !isSmallY && isBottom);
toggleClass(this.overlay, 'small-right', isSmallX && isRight); toggleClass(this.overlay, 'small-right', isSmallX && isRight);
toggleClass(this.overlay, 'small-left', isSmallX && isLeft); toggleClass(this.overlay, 'small-left', isSmallX && isLeft);
toggleClass(this.overlay, 'small-top', isSmallY && isTop); toggleClass(this.overlay, 'small-top', isSmallY && isTop);
@ -240,20 +246,23 @@ export class Droptarget extends CompositeDisposable {
width: number, width: number,
height: number height: number
): Quadrant | null | undefined { ): Quadrant | null | undefined {
if ( const isPercentage =
!this.options.overlayModel?.units || this.options.overlayModel?.activationSize === undefined ||
this.options.overlayModel?.units === 'percentage' this.options.overlayModel?.activationSize?.type === 'percentage';
) {
const value =
typeof this.options.overlayModel?.activationSize?.value === 'number'
? this.options.overlayModel?.activationSize?.value
: 20;
if (isPercentage) {
return calculateQuadrant_Percentage( return calculateQuadrant_Percentage(
overlayType, overlayType,
x, x,
y, y,
width, width,
height, height,
typeof this.options.overlayModel?.directionalThreshold === value
'number'
? this.options.overlayModel?.directionalThreshold
: 20
); );
} }
@ -263,14 +272,11 @@ export class Droptarget extends CompositeDisposable {
y, y,
width, width,
height, height,
typeof this.options.overlayModel?.directionalThreshold === 'number' value
? this.options.overlayModel?.directionalThreshold
: 20
); );
} }
private removeDropTarget() { private removeDropTarget() {
console.log('remove');
if (this.target) { if (this.target) {
this._state = undefined; this._state = undefined;
this.element.removeChild(this.target); this.element.removeChild(this.target);
@ -305,36 +311,6 @@ function calculateQuadrant_Percentage(
return 'bottom'; return 'bottom';
} }
// switch (overlayType) {
// case 'all':
// case 'nocenter':
// if (xp < threshold) {
// return 'left';
// }
// if (xp > 100 - threshold) {
// return 'right';
// }
// if (yp < threshold) {
// return 'top';
// }
// if (yp > 100 - threshold) {
// return 'bottom';
// }
// break;
// case 'vertical':
// if (yp < 50) {
// return 'top';
// }
// return 'bottom';
// case 'horizontal':
// if (xp < 50) {
// return 'left';
// }
// return 'right';
// }
if (!overlayType.includes('center')) { if (!overlayType.includes('center')) {
return undefined; return undefined;
} }
@ -363,35 +339,6 @@ function calculateQuadrant_Pixels(
return 'bottom'; return 'bottom';
} }
// switch (overlayType) {
// case 'all':
// case 'nocenter':
// if (x < threshold) {
// return 'left';
// }
// if (x > width - threshold) {
// return 'right';
// }
// if (y < threshold) {
// return 'top';
// }
// if (y > height - threshold) {
// return 'bottom';
// }
// break;
// case 'vertical':
// if (x < width / 2) {
// return 'top';
// }
// return 'bottom';
// case 'horizontal':
// if (y < height / 2) {
// return 'left';
// }
// return 'right';
// }
if (!overlayType.includes('center')) { if (!overlayType.includes('center')) {
return undefined; return undefined;
} }

View File

@ -232,14 +232,11 @@ export class DockviewComponent
return true return true
}, },
acceptedTargetZones: ['top', 'bottom', 'left', 'right'], acceptedTargetZones: ['top', 'bottom', 'left', 'right'],
overlayModel:{ overlayModel:{
units: 'pixels', activationSize: { type: 'pixels', value: 10 },
type: 'line', size: { type:'pixels', value: 20 }
directionalThreshold: 5,
cover: 0.1
}
} }
) })
this.addDisposables( this.addDisposables(
dropTarget, dropTarget,
@ -254,13 +251,17 @@ export class DockviewComponent
case Position.Top: case Position.Top:
case Position.Bottom: case Position.Bottom:
if(this.gridview.orientation === Orientation.HORIZONTAL) { if(this.gridview.orientation === Orientation.HORIZONTAL) {
this.gridview.flipOrientation(); // we need to add to a vertical splitview but the current root is a horizontal splitview.
// insert a vertical splitview at the root level and add the existing view as a child
this.gridview.insertOrthogonalSplitviewAtRoot();
} }
break; break;
case Position.Left: case Position.Left:
case Position.Right: case Position.Right:
if(this.gridview.orientation === Orientation.VERTICAL) { if(this.gridview.orientation === Orientation.VERTICAL) {
this.gridview.flipOrientation(); // we need to add to a horizontal splitview but the current root is a vertical splitview.
// insert a horiziontal splitview at the root level and add the existing view as a child
this.gridview.insertOrthogonalSplitviewAtRoot();
} }
break; break;
default: default:
@ -270,11 +271,13 @@ export class DockviewComponent
switch(event.position) { switch(event.position) {
case Position.Top: case Position.Top:
case Position.Left: case Position.Left:
this.createGroupAtLocation([0]); const verticalGroup = this.createGroupAtLocation([0]); // insert into first position
this.moveGroupOrPanel(verticalGroup, data.groupId, data.panelId || undefined, Position.Center);
break; break;
case Position.Bottom: case Position.Bottom:
case Position.Right: case Position.Right:
this.createGroupAtLocation([this.gridview.length]); const horizontalGroup = this.createGroupAtLocation([this.gridview.length]); // insert into last position
this.moveGroupOrPanel(horizontalGroup, data.groupId, data.panelId || undefined, Position.Center);
} }
} }
)); ));

View File

@ -418,7 +418,7 @@ 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 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 * If the root is orientated as a HORIZONTAL node then nest the existing root within a new VERITCAL root node
*/ */
public flipOrientation(): void { public insertOrthogonalSplitviewAtRoot(): void {
if (!this._root) { if (!this._root) {
return; return;
} }

View File

@ -71,7 +71,9 @@ export abstract class DraggablePaneviewPanel extends PaneviewPanel {
this.target = new Droptarget(this.element, { this.target = new Droptarget(this.element, {
acceptedTargetZones: ['top', 'bottom'], acceptedTargetZones: ['top', 'bottom'],
threshold: 50, overlayModel: {
activationSize: { type: 'percentage', value: 50 },
},
canDisplayOverlay: (event) => { canDisplayOverlay: (event) => {
const data = getPaneData(); const data = getPaneData();