feat: dnd to edge of dockview

This commit is contained in:
mathuo 2023-02-01 21:48:11 +07:00
parent 5e4d2cb506
commit f5709549c6
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
13 changed files with 420 additions and 90 deletions

View File

@ -17,6 +17,8 @@ export class DragAndDropObserver extends CompositeDisposable {
// repeadedly.
private counter = 0;
private target: any;
constructor(
private element: HTMLElement,
private callbacks: IDragAndDropObserverCallbacks
@ -28,29 +30,55 @@ export class DragAndDropObserver extends CompositeDisposable {
private registerListeners(): void {
this.addDisposables(
addDisposableListener(this.element, 'dragenter', (e: DragEvent) => {
this.counter++;
addDisposableListener(
this.element,
'dragenter',
(e: DragEvent) => {
this.counter++;
this.callbacks.onDragEnter(e);
})
try {
this.target = e.target;
this.callbacks.onDragEnter(e);
} catch (err) {
console.error(err);
}
},
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) {
try {
this.callbacks.onDragOver(e);
} catch (err) {
console.error(err);
}
}
},
true
)
);
this.addDisposables(
addDisposableListener(this.element, 'dragleave', (e: DragEvent) => {
this.counter--;
console.log('dragleave');
if (this.counter === 0) {
this.callbacks.onDragLeave(e);
// if (this.counter === 0) {
if (this.target === e.target) {
this.target = null;
try {
this.callbacks.onDragLeave(e);
} catch (err) {
console.error(err);
}
}
})
);
@ -58,14 +86,23 @@ export class DragAndDropObserver extends CompositeDisposable {
this.addDisposables(
addDisposableListener(this.element, 'dragend', (e: DragEvent) => {
this.counter = 0;
this.callbacks.onDragEnd(e);
this.target = null;
try {
this.callbacks.onDragEnd(e);
} catch (err) {
console.error(err);
}
})
);
this.addDisposables(
addDisposableListener(this.element, 'drop', (e: DragEvent) => {
this.counter = 0;
this.callbacks.onDrop(e);
try {
this.callbacks.onDrop(e);
} catch (err) {
console.error(err);
}
})
);
}

View File

@ -20,7 +20,7 @@
pointer-events: none;
&.left {
transform: translateX(-25%) scaleX(0.5)
transform: translateX(-35%) scaleX(0.3)
}
&.right {

View File

@ -2,6 +2,7 @@ import { toggleClass } from '../dom';
import { Emitter, Event } from '../events';
import { CompositeDisposable } from '../lifecycle';
import { DragAndDropObserver } from './dnd';
import { clamp } from '../math';
export enum Position {
Top = 'Top',
@ -18,7 +19,12 @@ export interface DroptargetEvent {
nativeEvent: DragEvent;
}
export type DropTargetDirections = 'vertical' | 'horizontal' | 'all' | 'none';
export type DropTargetDirections =
| 'top'
| 'bottom'
| 'left'
| 'right'
| 'center';
function isBooleanValue(
canDisplayOverlay: CanDisplayOverlay
@ -42,19 +48,17 @@ export class Droptarget extends CompositeDisposable {
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: DropTargetDirections[];
overlayModel?: {
units?: 'pixels' | 'percentage';
type?: 'modal' | 'line';
cover?: number;
directionalThreshold?: number;
};
}
) {
super();
@ -71,17 +75,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
this.options.acceptedTargetZones,
x,
y,
width,
height
);
if (quadrant === undefined) {
this.removeDropTarget();
return;
}
if (isBooleanValue(this.options.canDisplayOverlay)) {
if (!this.options.canDisplayOverlay) {
return;
@ -102,7 +114,7 @@ export class Droptarget extends CompositeDisposable {
this.element.append(this.target);
}
if (this.options.validOverlays === 'none') {
if (this.options.acceptedTargetZones.length === 0) {
return;
}
@ -110,8 +122,12 @@ export class Droptarget extends CompositeDisposable {
return;
}
const isSmallX = width < 100;
const isSmallY = height < 100;
const isSmallX =
this.options.overlayModel?.type === 'line' ||
width < 100;
const isSmallY =
this.options.overlayModel?.type === 'line' ||
height < 100;
this.toggleClasses(quadrant, isSmallX, isSmallY);
@ -157,10 +173,39 @@ export class Droptarget extends CompositeDisposable {
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 size =
typeof this.options.overlayModel?.cover === 'number'
? clamp(this.options.overlayModel?.cover, 0, 1)
: 0.5;
const translate = (1 - size) / 2;
const scale = size;
let transform = '';
const rightClass = !isSmallX && isRight;
const leftClass = !isSmallX && isLeft;
const topClass = !isSmallY && isTop;
const bottomClass = !isSmallY && isBottom;
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, '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-left', isSmallX && isLeft);
@ -189,47 +234,167 @@ export class Droptarget extends CompositeDisposable {
}
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';
case 'horizontal':
if (xp < 50) {
return 'left';
}
return 'right';
overlayType: DropTargetDirections[],
x: number,
y: number,
width: number,
height: number
): Quadrant | null | undefined {
if (
!this.options.overlayModel?.units ||
this.options.overlayModel?.units === 'percentage'
) {
return calculateQuadrant_Percentage(
overlayType,
x,
y,
width,
height,
typeof this.options.overlayModel?.directionalThreshold ===
'number'
? this.options.overlayModel?.directionalThreshold
: 20
);
}
return null;
return calculateQuadrant_Pixels(
overlayType,
x,
y,
width,
height,
typeof this.options.overlayModel?.directionalThreshold === 'number'
? this.options.overlayModel?.directionalThreshold
: 20
);
}
private removeDropTarget() {
console.log('remove');
if (this.target) {
this._state = undefined;
this.element.removeChild(this.target);
this.target = undefined;
this.overlay = undefined;
this.element.classList.remove('drop-target');
}
}
}
function calculateQuadrant_Percentage(
overlayType: DropTargetDirections[],
x: number,
y: number,
width: number,
height: number,
threshold: number
): Quadrant | null | undefined {
const xp = (100 * x) / width;
const yp = (100 * y) / height;
if (overlayType.includes('left') && xp < threshold) {
return 'left';
}
if (overlayType.includes('right') && xp > 100 - threshold) {
return 'right';
}
if (overlayType.includes('top') && yp < threshold) {
return 'top';
}
if (overlayType.includes('bottom') && yp > 100 - threshold) {
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')) {
return undefined;
}
return null;
}
function calculateQuadrant_Pixels(
overlayType: DropTargetDirections[],
x: number,
y: number,
width: number,
height: number,
threshold: number
): Quadrant | null | undefined {
if (overlayType.includes('left') && x < threshold) {
return 'left';
}
if (overlayType.includes('right') && x > width - threshold) {
return 'right';
}
if (overlayType.includes('top') && y < threshold) {
return 'top';
}
if (overlayType.includes('right') && y > height - threshold) {
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')) {
return undefined;
}
return null;
}

View File

@ -3,8 +3,9 @@ import {
SerializedGridObject,
getGridLocation,
ISerializedLeafNode,
orthogonal,
} from '../gridview/gridview';
import { Position } from '../dnd/droptarget';
import { Droptarget, Position } from '../dnd/droptarget';
import { tail, sequenceEquals } from '../array';
import { GroupviewPanelState, IDockviewPanel } from '../groupview/groupPanel';
import { DockviewGroupPanel } from './dockviewGroupPanel';
@ -40,6 +41,7 @@ import {
} from '../groupview/groupview';
import { GroupPanel } from '../groupview/groupviewPanel';
import { DefaultGroupPanelView } from './defaultGroupPanelView';
import { getPanelData } from '../dnd/dataTransfer';
const nextGroupId = sequentialNumberGenerator();
@ -224,6 +226,59 @@ export class DockviewComponent
this.options.watermarkComponent = Watermark;
}
const dropTarget = new Droptarget(this.element, {
canDisplayOverlay:() => {
return true
},
acceptedTargetZones: ['top', 'bottom', 'left', 'right'],
overlayModel:{
units: 'pixels',
type: 'line',
directionalThreshold: 5,
cover: 0.1
}
}
)
this.addDisposables(
dropTarget,
dropTarget.onDrop((event) => {
const data = getPanelData();
if(!data) {
return;
}
switch(event.position) {
case Position.Top:
case Position.Bottom:
if(this.gridview.orientation === Orientation.HORIZONTAL) {
this.gridview.flipOrientation();
}
break;
case Position.Left:
case Position.Right:
if(this.gridview.orientation === Orientation.VERTICAL) {
this.gridview.flipOrientation();
}
break;
default:
break
}
switch(event.position) {
case Position.Top:
case Position.Left:
this.createGroupAtLocation([0]);
break;
case Position.Bottom:
case Position.Right:
this.createGroupAtLocation([this.gridview.length]);
}
}
));
this._api = new DockviewApi(this);
}
@ -462,6 +517,11 @@ export class DockviewComponent
}
} else {
const group = this.createGroupAtLocation();
if(options.type === 'singular') {
group.locked = true;
}
panel = this.createPanel(options, group);
group.model.openPanel(panel);
}
@ -575,8 +635,6 @@ export class DockviewComponent
if(itemId === undefined) {
if(sourceGroup) {
if (!target || target === Position.Center) {
const activePanel = sourceGroup.activePanel;

View File

@ -38,7 +38,7 @@ export class DockviewGroupPanel
return this._group;
}
get view() {
get view(): IGroupPanelView | undefined {
return this._view;
}

View File

@ -86,6 +86,7 @@ export interface AddPanelOptions
direction?: Direction;
referencePanel?: string;
};
type?: 'tabular' | 'singular';
}
export interface AddGroupOptions {

View File

@ -276,6 +276,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 +414,35 @@ 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 flipOrientation(): 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
);
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

@ -167,6 +167,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,20 +228,20 @@ 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, {
this.tabsContainer = new TabsContainer(this.accessor, this.groupPanel, {
tabHeight: options.tabHeight,
});
this.contentContainer = new ContentContainer();
this.dropTarget = new Droptarget(this.contentContainer.element, {
validOverlays: 'all',
acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'],
canDisplayOverlay: (event, quadrant) => {
if (this.locked && !quadrant) {
return false;
@ -287,7 +289,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.handleDropEvent(event.event, Position.Center, event.index);
}),
this.contentContainer.onDidFocus(() => {
this.accessor.doSetGroupActive(this.parent, true);
this.accessor.doSetGroupActive(this.groupPanel, true);
}),
this.contentContainer.onDidBlur(() => {
// noop
@ -316,12 +318,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 +443,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 +459,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 +491,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.doClose(panel);
}
} else {
this.accessor.removeGroup(this.parent);
this.accessor.removeGroup(this.groupPanel);
}
}
@ -643,7 +648,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,7 +663,7 @@ 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);
}
});
@ -666,7 +671,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.tabsContainer.hide();
this.container.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();

View File

@ -109,7 +109,7 @@ export class Tab extends CompositeDisposable implements ITab {
);
this.droptarget = new Droptarget(this._element, {
validOverlays: 'none',
acceptedTargetZones: ['center'],
canDisplayOverlay: (event) => {
const data = getPanelData();
if (data && this.accessor.id === data.viewId) {

View File

@ -10,6 +10,22 @@
display: none;
}
&.single-panel {
background-color: red !important;
.tabs-container {
flex-grow: 1;
.tab {
flex-grow: 1;
}
}
.void-container {
flex-grow: 0;
}
}
.void-container {
display: flex;
flex-grow: 1;

View File

@ -9,6 +9,7 @@ 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;
@ -146,6 +147,19 @@ export class TabsContainer
this.height = options.tabHeight;
this.addDisposables(
this.accessor.onDidAddPanel((e) => {
if (e.api.group === this.group) {
toggleClass(this._element, 'single-panel', this.size === 1);
}
}),
this.accessor.onDidRemovePanel((e) => {
if (e.api.group === this.group) {
toggleClass(this._element, 'single-panel', this.size === 1);
}
})
);
this.actionContainer = document.createElement('div');
this.actionContainer.className = 'action-container';

View File

@ -105,7 +105,7 @@ export class VoidContainer extends CompositeDisposable {
);
this.voidDropTarget = new Droptarget(this._element, {
validOverlays: 'none',
acceptedTargetZones: ['center'],
canDisplayOverlay: (event) => {
const data = getPanelData();

View File

@ -70,7 +70,8 @@ export abstract class DraggablePaneviewPanel extends PaneviewPanel {
})(this.header);
this.target = new Droptarget(this.element, {
validOverlays: 'vertical',
acceptedTargetZones: ['top', 'bottom'],
threshold: 50,
canDisplayOverlay: (event) => {
const data = getPaneData();