work in progress

This commit is contained in:
mathuo 2021-06-29 22:12:05 +01:00
parent c94987d88c
commit adf09a1a25
31 changed files with 1159 additions and 628 deletions

View File

@ -3,6 +3,7 @@ import {
CompositeDisposable,
GridviewApi,
IGridviewPanelProps,
DockviewDropTarget,
} from 'dockview';
import './activitybar.scss';
import { useLayoutRegistry } from './registry';
@ -48,13 +49,18 @@ export const Activitybar = (props: IGridviewPanelProps) => {
return (
<div className="activity-bar" onClick={onOpenSidebar}>
<div className="activity-bar-item">
<ActivitybarImage
url={
'https://fonts.gstatic.com/s/i/materialicons/search/v7/24px.svg'
}
/>
</div>
<DockviewDropTarget
validOverlays={'vertical'}
canDisplayOverlay={true}
>
<div className="activity-bar-item">
<ActivitybarImage
url={
'https://fonts.gstatic.com/s/i/materialicons/search/v7/24px.svg'
}
/>
</div>
</DockviewDropTarget>
</div>
);
};

View File

@ -8,20 +8,20 @@ export const ControlCenter = () => {
const dragRef = React.useRef<HTMLDivElement>();
React.useEffect(() => {
const api = registry.get<DockviewApi>('dockview');
const target = api.createDragTarget(
{ element: dragRef.current, content: 'drag me' },
() => ({
id: 'yellow',
component: 'test_component',
})
);
// React.useEffect(() => {
// const api = registry.get<DockviewApi>('dockview');
// const target = api.createDragTarget(
// { element: dragRef.current, content: 'drag me' },
// () => ({
// id: 'yellow',
// component: 'test_component',
// })
// );
return () => {
target.dispose();
};
}, []);
// return () => {
// target.dispose();
// };
// }, []);
const onDragStart = (event: React.DragEvent) => {
event.dataTransfer.setData('text/plain', 'Panel2');

View File

@ -304,26 +304,26 @@ export const TestGrid = (props: IGridviewPanelProps) => {
_api.current = event.api;
registry.register('dockview', api);
api.addDndHandle('text/plain', (ev) => {
const { event } = ev;
// api.addDndHandle('text/plain', (ev) => {
// const { event } = ev;
return {
id: 'yellow',
component: 'test_component',
};
});
// return {
// id: 'yellow',
// component: 'test_component',
// };
// });
api.addDndHandle('Files', (ev) => {
const { event } = ev;
// api.addDndHandle('Files', (ev) => {
// const { event } = ev;
ev.event.event.preventDefault();
// ev.event.event.preventDefault();
return {
id: Date.now().toString(),
title: event.event.dataTransfer.files[0].name,
component: 'test_component',
};
});
// return {
// id: Date.now().toString(),
// title: event.event.dataTransfer.files[0].name,
// component: 'test_component',
// };
// });
const state = localStorage.getItem('dockview');
if (state) {

View File

@ -6,6 +6,7 @@ import {
IPaneviewPanelProps,
CompositeDisposable,
PaneviewApi,
PaneviewDropEvent,
} from 'dockview';
import { ControlCenter } from './controlCenter';
import { toggleClass } from '../dom';
@ -183,6 +184,10 @@ export const Sidebar = (props: IGridviewPanelProps) => {
};
}, []);
const onDidDrop = React.useCallback((event: PaneviewDropEvent) => {
console.log('drop', event);
}, []);
return (
<div
style={{
@ -194,6 +199,7 @@ export const Sidebar = (props: IGridviewPanelProps) => {
headerComponents={headerComponents}
components={components}
onReady={onReady}
onDidDrop={onDidDrop}
/>
</div>
);

View File

@ -26,17 +26,16 @@ describe('droptarget', () => {
let position: Position | undefined = undefined;
droptarget = new Droptarget(element, {
isDisabled: () => false,
isDirectional: false,
id: 'test-dnd',
enableExternalDragEvents: true,
canDisplayOverlay: () => true,
validOverlays: 'none',
});
droptarget.onDidChange((event) => {
droptarget.onDrop((event) => {
position = event.position;
});
fireEvent.dragEnter(element);
fireEvent.dragOver(element);
const target = element.querySelector(
'.drop-target-dropzone'
@ -49,17 +48,16 @@ describe('droptarget', () => {
let position: Position | undefined = undefined;
droptarget = new Droptarget(element, {
isDisabled: () => false,
isDirectional: true,
id: 'test-dnd',
enableExternalDragEvents: true,
canDisplayOverlay: () => true,
validOverlays: 'all',
});
droptarget.onDidChange((event) => {
droptarget.onDrop((event) => {
position = event.position;
});
fireEvent.dragEnter(element);
fireEvent.dragOver(element);
const target = element.querySelector(
'.drop-target-dropzone'
@ -80,15 +78,14 @@ describe('droptarget', () => {
test('default', () => {
droptarget = new Droptarget(element, {
isDisabled: () => false,
isDirectional: true,
id: 'test-dnd',
enableExternalDragEvents: true,
canDisplayOverlay: () => true,
validOverlays: 'all',
});
expect(droptarget.state).toBeUndefined();
fireEvent.dragEnter(element);
fireEvent.dragOver(element);
let viewQuery = element.querySelectorAll(
'.drop-target > .drop-target-dropzone > .drop-target-selection'

View File

@ -9,6 +9,7 @@ import { Orientation } from '../../splitview/core/splitview';
import { ReactPanelDeserialzier } from '../../react/deserializer';
import { Position } from '../../dnd/droptarget';
import { GroupviewPanel } from '../../groupview/groupviewPanel';
import { IGroupPanel } from '../../groupview/groupPanel';
class PanelContentPartTest implements IContentRenderer {
element: HTMLElement = document.createElement('div');
@ -357,6 +358,7 @@ describe('dockviewComponent', () => {
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 500,
},

View File

@ -16,7 +16,7 @@ import { fireEvent } from '@testing-library/dom';
import { LocalSelectionTransfer } from '../../dnd/dataTransfer';
import { Position } from '../../dnd/droptarget';
import { GroupviewPanel } from '../../groupview/groupviewPanel';
import { GroupOptions, GroupDropEvent } from '../../groupview/groupview';
import { GroupOptions } from '../../groupview/groupview';
import { DockviewPanelApi } from '../../api/groupPanelApi';
import {
DefaultGroupPanelView,
@ -294,44 +294,44 @@ describe('groupview', () => {
expect(viewQuery).toBeTruthy();
});
test('dnd', () => {
const panel1 = new TestPanel('panel1', jest.fn() as any);
const panel2 = new TestPanel('panel2', jest.fn() as any);
// test('dnd', () => {
// const panel1 = new TestPanel('panel1', jest.fn() as any);
// const panel2 = new TestPanel('panel2', jest.fn() as any);
groupview.model.openPanel(panel1);
groupview.model.openPanel(panel2);
// groupview.model.openPanel(panel1);
// groupview.model.openPanel(panel2);
const events: GroupDropEvent[] = [];
// const events: GroupDropEvent[] = [];
groupview.model.onDrop((event) => {
events.push(event);
});
// groupview.model.onDrop((event) => {
// events.push(event);
// });
const viewQuery = groupview.element.querySelectorAll(
'.groupview > .tabs-and-actions-container > .tabs-container > .tab'
);
expect(viewQuery.length).toBe(2);
// const viewQuery = groupview.element.querySelectorAll(
// '.groupview > .tabs-and-actions-container > .tabs-container > .tab'
// );
// expect(viewQuery.length).toBe(2);
LocalSelectionTransfer.getInstance().setData([], 'dockview-1');
// LocalSelectionTransfer.getInstance().setData([], 'dockview-1');
fireEvent.dragEnter(viewQuery[0]);
// fireEvent.dragEnter(viewQuery[0]);
let dropTarget = viewQuery[0].querySelector('.drop-target-dropzone');
fireEvent.dragOver(dropTarget);
fireEvent.drop(dropTarget);
// let dropTarget = viewQuery[0].querySelector('.drop-target-dropzone');
// fireEvent.dragOver(dropTarget);
// fireEvent.drop(dropTarget);
expect(events.length).toBe(1);
expect(events[0].target).toBe(Position.Center);
expect(events[0].index).toBe(0);
// expect(events.length).toBe(1);
// expect(events[0].target).toBe(Position.Center);
// expect(events[0].index).toBe(0);
fireEvent.dragEnter(viewQuery[1]);
// fireEvent.dragEnter(viewQuery[1]);
dropTarget = viewQuery[1].querySelector('.drop-target-dropzone');
fireEvent.dragOver(dropTarget);
fireEvent.drop(dropTarget);
// dropTarget = viewQuery[1].querySelector('.drop-target-dropzone');
// fireEvent.dragOver(dropTarget);
// fireEvent.drop(dropTarget);
expect(events.length).toBe(2);
expect(events[1].target).toBe(Position.Center);
expect(events[1].index).toBe(1);
});
// expect(events.length).toBe(2);
// expect(events[1].target).toBe(Position.Center);
// expect(events[1].index).toBe(1);
// });
});

View File

@ -1,6 +1,5 @@
import {
IDockviewComponent,
LayoutDropEvent,
SerializedDockview,
} from '../dockview/dockviewComponent';
import {
@ -339,19 +338,19 @@ export class DockviewApi {
return this.component.addPanel(options);
}
addDndHandle(type: string, cb: (event: LayoutDropEvent) => PanelOptions) {
return this.component.addDndHandle(type, cb);
}
// addDndHandle(type: string, cb: (event: LayoutDropEvent) => PanelOptions) {
// return this.component.addDndHandle(type, cb);
// }
createDragTarget(
target: {
element: HTMLElement;
content: string;
},
options: (() => PanelOptions) | PanelOptions
) {
return this.component.createDragTarget(target, options);
}
// createDragTarget(
// target: {
// element: HTMLElement;
// content: string;
// },
// options: (() => PanelOptions) | PanelOptions
// ) {
// return this.component.createDragTarget(target, options);
// }
addEmptyGroup(options?: AddGroupOptions) {
return this.component.addEmptyGroup(options);

View File

@ -0,0 +1,100 @@
import { getElementsByTagName } from '../dom';
import { addDisposableListener, Emitter } from '../events';
import { focusedElement } from '../focusedElement';
import { CompositeDisposable, IDisposable } from '../lifecycle';
import { DATA_KEY, LocalSelectionTransfer } from './dataTransfer';
export abstract class DragHandler extends CompositeDisposable {
private iframes: HTMLElement[] = [];
private readonly _onDragStart = new Emitter<void>();
readonly onDragStart = this._onDragStart.event;
// private activeDrag: { id: string } | undefined;
// get isDragging() {
// return !!this.activeDrag;
// }
private disposable: IDisposable | undefined;
constructor(private readonly el: HTMLElement) {
super();
this.configure();
}
abstract getData(): IDisposable;
private configure() {
this.addDisposables(
addDisposableListener(this.el, 'dragstart', (event) => {
this.iframes = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
for (const iframe of this.iframes) {
iframe.style.pointerEvents = 'none';
}
this.el.classList.add('dragged');
setTimeout(() => this.el.classList.remove('dragged'), 0);
// this.activeDrag = this.getData();
this.disposable?.dispose();
this.disposable = this.getData();
// if (event.dataTransfer) {
// event.dataTransfer.setData(DATA_KEY, stringifiedData);
// event.dataTransfer.effectAllowed = 'move';
// }
}),
addDisposableListener(this.el, 'dragend', (ev) => {
for (const iframe of this.iframes) {
iframe.style.pointerEvents = 'auto';
}
this.iframes = [];
this.disposable?.dispose();
this.disposable = undefined;
// drop events fire before dragend so we can remove this safely
// LocalSelectionTransfer.getInstance().clearData(this.activeDrag);
// this.activeDrag = undefined;
}),
addDisposableListener(this.el, 'mousedown', (event) => {
if (event.defaultPrevented) {
return;
}
/**
* TODO: alternative to stopPropagation
*
* I need to stop the event propagation here since otherwise it'll be intercepted by event handlers
* on the tabs-container. I cannot use event.preventDefault() since I need the on DragStart event to occur
*/
event.stopPropagation();
/**
* //TODO mousedown focusing with draggable element (is there a better approach?)
*
* this mousedown event wants to focus the tab itself but if we call preventDefault()
* this would also prevent the dragStart event from firing. To get around this we propagate
* the onChanged event during the next tick of the event-loop, allowing the tab element to become
* focused on this tick and ensuring the dragstart event is not interrupted
*/
const oldFocus = focusedElement.element as HTMLElement;
setTimeout(() => {
oldFocus.focus();
// this._onChanged.fire({ kind: MouseEventKind.CLICK, event });
}, 0);
}),
addDisposableListener(this.el, 'contextmenu', (event) => {
// this._onChanged.fire({
// kind: MouseEventKind.CONTEXT_MENU,
// event,
// });
})
);
}
}

View File

@ -1,5 +1,6 @@
import { PanelOptions } from '../dockview/options';
import { tryParseJSON } from '../json';
import { PanelTransfer, PaneTransfer } from './droptarget';
export const DATA_KEY = 'splitview/transfer';
@ -12,7 +13,7 @@ export const isPanelTransferEvent = (event: DragEvent) => {
};
export enum DragType {
ITEM = 'group_drag',
DOCKVIEW_TAB = 'dockview_tab',
EXTERNAL = 'external_group_drag',
}
@ -30,7 +31,7 @@ export type DataObject = DragItem | ExternalDragItem;
* dragging a tab component
*/
export const isTabDragEvent = (data: any): data is DragItem => {
return data.type === DragType.ITEM;
return data.type === DragType.DOCKVIEW_TAB;
};
/**
@ -102,3 +103,25 @@ export class LocalSelectionTransfer<T> {
}
}
}
export function getPanelData(): PanelTransfer | undefined {
const panelTransfer = LocalSelectionTransfer.getInstance<PanelTransfer>();
const isPanelEvent = panelTransfer.hasData(PanelTransfer.prototype);
if (!isPanelEvent) {
return undefined;
}
return panelTransfer.getData(PanelTransfer.prototype)![0];
}
export function getPaneData(): PaneTransfer | undefined {
const paneTransfer = LocalSelectionTransfer.getInstance<PaneTransfer>();
const isPanelEvent = paneTransfer.hasData(PaneTransfer.prototype);
if (!isPanelEvent) {
return undefined;
}
return paneTransfer.getData(PaneTransfer.prototype)![0];
}

View File

@ -0,0 +1,181 @@
import { addDisposableListener, Emitter } from '../events';
import { CompositeDisposable, IDisposable } from '../lifecycle';
import { LocalSelectionTransfer } from './dataTransfer';
export interface IDragAndDropObserverCallbacks {
onDragEnter: (e: DragEvent) => void;
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;
constructor(
private element: HTMLElement,
private callbacks: IDragAndDropObserverCallbacks
) {
super();
this.registerListeners();
}
private registerListeners(): void {
this.addDisposables(
addDisposableListener(this.element, 'dragenter', (e: DragEvent) => {
this.counter++;
this.callbacks.onDragEnter(e);
})
);
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)
if (this.callbacks.onDragOver) {
this.callbacks.onDragOver(e);
}
})
);
this.addDisposables(
addDisposableListener(this.element, 'dragleave', (e: DragEvent) => {
this.counter--;
if (this.counter === 0) {
this.callbacks.onDragLeave(e);
}
})
);
this.addDisposables(
addDisposableListener(this.element, 'dragend', (e: DragEvent) => {
this.counter = 0;
this.callbacks.onDragEnd(e);
})
);
this.addDisposables(
addDisposableListener(this.element, 'drop', (e: DragEvent) => {
this.counter = 0;
this.callbacks.onDrop(e);
})
);
}
}
export interface IDraggedCompositeData {
eventData: DragEvent;
dragAndDropData: any;
}
export interface ICompositeDragAndDropObserverCallbacks {
onDragEnter?: (e: IDraggedCompositeData) => void;
onDragLeave?: (e: IDraggedCompositeData) => void;
onDrop?: (e: IDraggedCompositeData) => void;
onDragOver?: (e: IDraggedCompositeData) => void;
onDragStart?: (e: IDraggedCompositeData) => void;
onDragEnd?: (e: IDraggedCompositeData) => void;
}
class DockviewIdentifier<T = {}> {
constructor(private readonly data: T) {
//
}
}
export class DragAndDrop extends CompositeDisposable {
private _onDragStart = new Emitter<any>();
private _onDragEnd = new Emitter<any>();
private static _instance: DragAndDrop | undefined;
static get INSTANCE(): DragAndDrop {
if (!DragAndDrop._instance) {
DragAndDrop._instance = new DragAndDrop();
}
return DragAndDrop._instance;
}
private transferData =
LocalSelectionTransfer.getInstance<DockviewIdentifier>();
private constructor() {
super();
this.addDisposables(this._onDragStart, this._onDragEnd);
}
registerTarget(
element: HTMLElement,
callbacks: ICompositeDragAndDropObserverCallbacks
): IDisposable {
const disposables = new CompositeDisposable();
disposables.addDisposables(
new DragAndDropObserver(element, {
onDragEnd: (e) => {
// no-op
},
onDragEnter: (e) => {
e.preventDefault();
},
onDragLeave: (e) => {
//
},
onDrop: (e) => {
//
},
onDragOver: (e) => {
//
},
})
);
return disposables;
}
registerDraggable(
element: HTMLElement,
draggedItemProvider: () => { type: string; id: string },
callbacks: ICompositeDragAndDropObserverCallbacks
): IDisposable {
element.draggable = true;
const disposables = new CompositeDisposable();
disposables.addDisposables(
addDisposableListener(element, 'dragstart', (e) => {
this._onDragStart.fire({ event: e });
})
);
disposables.addDisposables(
new DragAndDropObserver(element, {
onDragEnd: (e) => {
// no-op
},
onDragEnter: (e) => {
//
},
onDragLeave: (e) => {
//
},
onDrop: (e) => {
//
},
onDragOver: (e) => {
//
},
})
);
return disposables;
}
}

View File

@ -12,6 +12,7 @@
> .drop-target-selection {
position: relative;
pointer-events: none;
box-sizing: border-box;
height: 100%;
width: 100%;
background-color: var(--dv-drag-over-background-color);
@ -33,6 +34,19 @@
&.bottom {
height: 50%;
}
&.small-top {
border-top: 1px solid var(--dv-drag-over-border-color);
}
&.small-bottom {
border-bottom: 1px solid var(--dv-drag-over-border-color);
}
&.small-left {
border-left: 1px solid var(--dv-drag-over-border-color);
}
&.small-right {
border-right: 1px solid var(--dv-drag-over-border-color);
}
}
}
}

View File

@ -1,6 +1,37 @@
import { toggleClass } from '../dom';
import { Emitter, Event } from '../events';
import { LocalSelectionTransfer } from './dataTransfer';
import { CompositeDisposable } from '../lifecycle';
import { DragAndDropObserver } from './dnd';
export interface DroptargetEvent {
position: Position;
event: DragEvent;
}
class TransferObject {
constructor() {
//
}
}
export class PanelTransfer extends TransferObject {
constructor(
public readonly viewId: string,
public readonly groupId: string,
public readonly panelId: string
) {
super();
}
}
export class PaneTransfer extends TransferObject {
constructor(
public readonly viewId: string,
public readonly paneId: string
) {
super();
}
}
export enum Position {
Top = 'Top',
@ -15,146 +46,176 @@ export interface DroptargetEvent {
event: DragEvent;
}
export class Droptarget {
export type DropTargetDirections = 'vertical' | 'horizontal' | 'all' | 'none';
function isBooleanValue(
canDisplayOverlay: CanDisplayOverlay
): canDisplayOverlay is boolean {
return typeof canDisplayOverlay === 'boolean';
}
export type CanDisplayOverlay = boolean | ((dragEvent: DragEvent) => boolean);
export class Droptarget extends CompositeDisposable {
private target: HTMLElement | undefined;
private overlay: HTMLElement | undefined;
private _state: Position | undefined;
private readonly _onDidChange = new Emitter<DroptargetEvent>();
readonly onDidChange: Event<DroptargetEvent> = this._onDidChange.event;
private readonly _onDrop = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
get state() {
return this._state;
}
set validOverlays(value: DropTargetDirections) {
this.options.validOverlays = value;
}
set canDisplayOverlay(value: CanDisplayOverlay) {
this.options.canDisplayOverlay = value;
}
constructor(
private element: HTMLElement,
private options: {
isDisabled: () => boolean;
isDirectional: boolean;
id: string;
enableExternalDragEvents?: boolean;
private readonly element: HTMLElement,
private readonly options: {
canDisplayOverlay: CanDisplayOverlay;
validOverlays: DropTargetDirections;
}
) {
this.element.addEventListener('dragenter', this.onDragEnter);
super();
this.addDisposables(
new DragAndDropObserver(this.element, {
onDragEnter: (e) => undefined,
onDragOver: (e) => {
if (isBooleanValue(this.options.canDisplayOverlay)) {
if (!this.options.canDisplayOverlay) {
return;
}
} else if (!this.options.canDisplayOverlay(e)) {
return;
}
if (!this.target) {
console.debug('[droptarget] created');
this.target = document.createElement('div');
this.target.className = 'drop-target-dropzone';
this.overlay = document.createElement('div');
this.overlay.className = 'drop-target-selection';
this._state = Position.Center;
this.target.appendChild(this.overlay);
this.element.classList.add('drop-target');
this.element.append(this.target);
}
if (this.options.validOverlays === 'none') {
return;
}
if (!this.target || !this.overlay) {
return;
}
const width = this.target.clientWidth;
const height = this.target.clientHeight;
if (width === 0 || height === 0) {
return; // avoid div!0
}
const x = e.offsetX;
const y = e.offsetY;
const xp = (100 * x) / width;
const yp = (100 * y) / height;
let isRight = false;
let isLeft = false;
let isTop = false;
let isBottom = false;
switch (this.options.validOverlays) {
case 'all':
isRight = xp > 80;
isLeft = xp < 20;
isTop = !isRight && !isLeft && yp < 20;
isBottom = !isRight && !isLeft && yp > 80;
break;
case 'vertical':
isTop = yp < 50;
isBottom = yp >= 50;
break;
case 'horizontal':
isLeft = xp < 50;
isRight = xp >= 50;
break;
}
const isSmallX = width < 100;
const isSmallY = height < 100;
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);
toggleClass(this.overlay, 'small-top', isSmallY && isTop);
toggleClass(
this.overlay,
'small-bottom',
isSmallY && isBottom
);
if (isRight) {
this._state = Position.Right;
} else if (isLeft) {
this._state = Position.Left;
} else if (isTop) {
this._state = Position.Top;
} else if (isBottom) {
this._state = Position.Bottom;
} else {
this._state = Position.Center;
}
},
onDragLeave: (e) => {
this.removeDropTarget();
},
onDragEnd: (e) => {
this.removeDropTarget();
},
onDrop: (e) => {
e.preventDefault();
e.stopPropagation();
const state = this._state;
console.debug('[dragtarget] drop');
this.removeDropTarget();
if (state) {
this._onDrop.fire({ position: state, event: e });
}
},
})
);
}
public dispose() {
this._onDidChange.dispose();
this._onDrop.dispose();
this.removeDropTarget();
this.element.removeEventListener('dragenter', this.onDragEnter);
}
private onDragEnter = (event: DragEvent) => {
if (
!this.options.enableExternalDragEvents &&
!LocalSelectionTransfer.getInstance().hasData(this.options.id)
) {
console.debug('[droptarget] invalid event');
return;
}
if (this.options.isDisabled()) {
return;
}
event.preventDefault();
if (!this.target) {
console.debug('[droptarget] created');
this.target = document.createElement('div');
this.target.className = 'drop-target-dropzone';
this.overlay = document.createElement('div');
this.overlay.className = 'drop-target-selection';
//
this._state = Position.Center;
this.target.addEventListener('dragover', this.onDragOver);
this.target.addEventListener('dragleave', this.onDragLeave);
this.target.addEventListener('drop', this.onDrop);
this.target.appendChild(this.overlay);
this.element.classList.add('drop-target');
this.element.append(this.target);
}
};
private onDrop = (event: DragEvent) => {
if (
!this.options.enableExternalDragEvents &&
!LocalSelectionTransfer.getInstance().hasData(this.options.id)
) {
console.debug('[dragtarget] invalid');
return;
}
const state = this._state;
console.debug('[dragtarget] drop');
this.removeDropTarget();
if (event.defaultPrevented) {
console.debug('[dragtarget] defaultPrevented');
} else if (state) {
this._onDidChange.fire({ position: state, event });
}
};
private onDragOver = (event: DragEvent) => {
event.preventDefault();
if (!this.options.isDirectional) {
return;
}
if (!this.target || !this.overlay) {
return;
}
const width = this.target.clientWidth;
const height = this.target.clientHeight;
if (width === 0 || height === 0) {
return; // avoid div!0
}
const x = event.offsetX;
const y = event.offsetY;
const xp = (100 * x) / width;
const yp = (100 * y) / height;
const isRight = xp > 80;
const isLeft = xp < 20;
const isTop = !isRight && !isLeft && yp < 20;
const isBottom = !isRight && !isLeft && yp > 80;
toggleClass(this.overlay, 'right', isRight);
toggleClass(this.overlay, 'left', isLeft);
toggleClass(this.overlay, 'top', isTop);
toggleClass(this.overlay, 'bottom', isBottom);
if (isRight) {
this._state = Position.Right;
} else if (isLeft) {
this._state = Position.Left;
} else if (isTop) {
this._state = Position.Top;
} else if (isBottom) {
this._state = Position.Bottom;
} else {
this._state = Position.Center;
}
};
private onDragLeave = (event: DragEvent) => {
console.debug('[droptarget] leave');
this.removeDropTarget();
};
private removeDropTarget() {
if (this.target) {
this._state = undefined;
this.target.removeEventListener('dragover', this.onDragOver);
this.target.removeEventListener('dragleave', this.onDragLeave);
this.target.removeEventListener('drop', this.onDrop);
this.element.removeChild(this.target);
this.target = undefined;
this.element.classList.remove('drop-target');

View File

@ -1,13 +1,21 @@
.dragged {
transform: translate3d(
0px,
0px,
0px
); /* forces tab to be drawn on a separate layer (see https://github.com/microsoft/vscode/issues/18733) */
}
.tab {
flex-shrink: 0;
&.dragged {
transform: translate3d(
0px,
0px,
0px
); /* forces tab to be drawn on a separate layer (see https://github.com/microsoft/vscode/issues/18733) */
}
// &.dragged {
// transform: translate3d(
// 0px,
// 0px,
// 0px
// ); /* forces tab to be drawn on a separate layer (see https://github.com/microsoft/vscode/issues/18733) */
// }
&.dragging {
.tab-action {

View File

@ -7,13 +7,8 @@ import { Position } from '../dnd/droptarget';
import { tail, sequenceEquals } from '../array';
import { GroupviewPanelState, IGroupPanel } from '../groupview/groupPanel';
import { DockviewGroupPanel } from './dockviewGroupPanel';
import {
CompositeDisposable,
IDisposable,
IValueDisposable,
MutableDisposable,
} from '../lifecycle';
import { Event, Emitter, addDisposableListener } from '../events';
import { CompositeDisposable, IValueDisposable } from '../lifecycle';
import { Event, Emitter } from '../events';
import { Watermark } from './components/watermark/watermark';
import { timeoutAsPromise } from '../async';
import {
@ -28,16 +23,10 @@ import { createComponent } from '../panel/componentFactory';
import {
AddGroupOptions,
AddPanelOptions,
PanelOptions,
DockviewOptions as DockviewComponentOptions,
MovementOptions,
TabContextMenuEvent,
} from './options';
import {
DATA_KEY,
DragType,
LocalSelectionTransfer,
} from '../dnd/dataTransfer';
import {
BaseGrid,
IBaseGrid,
@ -50,7 +39,6 @@ import { Orientation } from '../splitview/core/splitview';
import { DefaultTab } from './components/tab/defaultTab';
import {
GroupChangeKind,
GroupDropEvent,
GroupOptions,
GroupPanelViewState,
} from '../groupview/groupview';
@ -113,17 +101,17 @@ export interface IDockviewComponent extends IBaseGrid<GroupviewPanel> {
onTabContextMenu: Event<TabContextMenuEvent>;
moveToNext(options?: MovementOptions): void;
moveToPrevious(options?: MovementOptions): void;
createDragTarget(
target: {
element: HTMLElement;
content: string;
},
options: (() => PanelOptions) | PanelOptions
): IDisposable;
addDndHandle(
type: string,
cb: (event: LayoutDropEvent) => PanelOptions
): void;
// createDragTarget(
// target: {
// element: HTMLElement;
// content: string;
// },
// options: (() => PanelOptions) | PanelOptions
// ): IDisposable;
// addDndHandle(
// type: string,
// cb: (event: LayoutDropEvent) => PanelOptions
// ): void;
setActivePanel(panel: IGroupPanel): void;
focus(): void;
toJSON(): SerializedDockview;
@ -131,10 +119,6 @@ export interface IDockviewComponent extends IBaseGrid<GroupviewPanel> {
onDidLayoutChange: Event<void>;
}
export interface LayoutDropEvent {
event: GroupDropEvent;
}
export class DockviewComponent
extends BaseGrid<GroupviewPanel>
implements IDockviewComponent
@ -153,13 +137,13 @@ export class DockviewComponent
readonly onTabContextMenu: Event<TabContextMenuEvent> =
this._onTabContextMenu.event;
// everything else
private drag = new MutableDisposable();
// private drag = new MutableDisposable();
private _deserializer: IPanelDeserializer | undefined;
private panelState: State = {};
private registry = new Map<
string,
(event: LayoutDropEvent) => PanelOptions
>();
// private registry = new Map<
// string,
// (event: LayoutDropEvent) => PanelOptions
// >();
private _api: DockviewApi;
private _options: DockviewComponentOptions;
@ -274,12 +258,12 @@ export class DockviewComponent
this.layout(this.gridview.width, this.gridview.height, true);
}
addDndHandle(
type: string,
cb: (event: LayoutDropEvent) => PanelOptions
): void {
this.registry.set(type, cb);
}
// addDndHandle(
// type: string,
// cb: (event: LayoutDropEvent) => PanelOptions
// ): void {
// this.registry.set(type, cb);
// }
focus(): void {
this.activeGroup?.focus();
@ -289,57 +273,57 @@ export class DockviewComponent
return this.panels.get(id)?.value;
}
createDragTarget(
target: {
element: HTMLElement;
content: string;
},
options: (() => PanelOptions) | PanelOptions
): IDisposable {
return new CompositeDisposable(
addDisposableListener(target.element, 'dragstart', (event) => {
if (!event.dataTransfer) {
throw new Error('unsupported');
}
// createDragTarget(
// target: {
// element: HTMLElement;
// content: string;
// },
// options: (() => PanelOptions) | PanelOptions
// ): IDisposable {
// return new CompositeDisposable(
// addDisposableListener(target.element, 'dragstart', (event) => {
// if (!event.dataTransfer) {
// throw new Error('unsupported');
// }
const panelOptions =
typeof options === 'function' ? options() : options;
// const panelOptions =
// typeof options === 'function' ? options() : options;
const panel = this.panels.get(panelOptions.id)?.value;
if (panel) {
this.drag.value = panel.group!.model.startActiveDrag(panel);
}
// const panel = this.panels.get(panelOptions.id)?.value;
// if (panel) {
// this.drag.value = panel.group!.model.startActiveDrag(panel);
// }
const data = JSON.stringify({
type: DragType.EXTERNAL,
...panelOptions,
});
// const data = JSON.stringify({
// type: DragType.EXTERNAL,
// ...panelOptions,
// });
LocalSelectionTransfer.getInstance().setData([data], this.id);
// LocalSelectionTransfer.getInstance().setData([data], this.id);
event.dataTransfer.effectAllowed = 'move';
// event.dataTransfer.effectAllowed = 'move';
const dragImage = document.createElement('div');
dragImage.textContent = target.content;
dragImage.classList.add('custom-dragging');
// const dragImage = document.createElement('div');
// dragImage.textContent = target.content;
// dragImage.classList.add('custom-dragging');
document.body.appendChild(dragImage);
event.dataTransfer.setDragImage(
dragImage,
event.offsetX,
event.offsetY
);
setTimeout(() => document.body.removeChild(dragImage), 0);
// document.body.appendChild(dragImage);
// event.dataTransfer.setDragImage(
// dragImage,
// event.offsetX,
// event.offsetY
// );
// setTimeout(() => document.body.removeChild(dragImage), 0);
event.dataTransfer.setData(DATA_KEY, data);
}),
addDisposableListener(this.element, 'dragend', (ev) => {
// drop events fire before dragend so we can remove this safely
LocalSelectionTransfer.getInstance().clearData(this.id);
this.drag.dispose();
})
);
}
// event.dataTransfer.setData(DATA_KEY, data);
// }),
// addDisposableListener(this.element, 'dragend', (ev) => {
// // drop events fire before dragend so we can remove this safely
// LocalSelectionTransfer.getInstance().clearData(this.id);
// this.drag.dispose();
// })
// );
// }
setActivePanel(panel: IGroupPanel): void {
if (!panel.group) {
@ -759,47 +743,6 @@ export class DockviewComponent
}),
view.model.onDidGroupChange((event) => {
this._onGridEvent.fire(event);
}),
view.model.onDrop((event) => {
const dragEvent = event.event;
const dataTransfer = dragEvent.dataTransfer;
if (!dataTransfer) {
return;
}
if (dataTransfer.types.length === 0) {
return;
}
const cb = this.registry.get(dataTransfer.types[0]);
if (!cb) {
return;
}
const panelOptions = cb({ event });
let panel = this.getGroupPanel(panelOptions.id);
if (!panel) {
panel = this._addPanel(panelOptions);
}
const groupId = panel.group?.id;
if (!groupId) {
throw new Error(
`Panel ${panel.id} has no associated group`
);
}
this.moveGroupOrPanel(
view,
groupId,
panel.id,
event.target,
event.index
);
})
);

View File

@ -555,9 +555,8 @@ export class Gridview implements IDisposable {
let newSiblingSize: number | Sizing = 0;
const newSiblingCachedVisibleSize = grandParent.getChildCachedVisibleSize(
parentIndex
);
const newSiblingCachedVisibleSize =
grandParent.getChildCachedVisibleSize(parentIndex);
if (typeof newSiblingCachedVisibleSize === 'number') {
newSiblingSize = Sizing.Invisible(newSiblingCachedVisibleSize);
}

View File

@ -1,12 +1,8 @@
import { DockviewApi } from '../api/component.api';
import { timeoutAsPromise } from '../async';
import {
extractData,
isCustomDragEvent,
isPanelTransferEvent,
isTabDragEvent,
} from '../dnd/dataTransfer';
import { Droptarget, DroptargetEvent, Position } from '../dnd/droptarget';
import { getPanelData } from '../dnd/dataTransfer';
import { Position } from '../dnd/droptarget';
import { Droptarget } from '../dnd/droptarget';
import {
DockviewComponent,
IDockviewComponent,
@ -15,7 +11,7 @@ import { isAncestor, toggleClass } from '../dom';
import { addDisposableListener, Emitter, Event } from '../events';
import { IGridPanelView } from '../gridview/baseComponentGridview';
import { IViewSize } from '../gridview/gridview';
import { CompositeDisposable, Disposable, IDisposable } from '../lifecycle';
import { CompositeDisposable, IDisposable } from '../lifecycle';
import { PanelInitParameters, PanelUpdateEvent } from '../panel/types';
import { IGroupPanel } from './groupPanel';
import { ContentContainer, IContentContainer } from './panel/content';
@ -46,6 +42,20 @@ export enum GroupChangeKind {
LAYOUT_CONFIG_UPDATED = 'LAYOUT_CONFIG_UPDATED',
}
export interface DndService {
canDisplayOverlay(
group: IGroupview,
event: DragEvent,
target: DockviewDropTargets
): boolean;
onDrop(
group: IGroupview,
event: DragEvent,
position: Position,
index?: number
): void;
}
export interface IGroupItem {
id: string;
header: { element: HTMLElement };
@ -77,6 +87,12 @@ export interface GroupPanelViewState {
id: string;
}
export enum DockviewDropTargets {
Tab,
Panel,
TabContainer,
}
export interface IGroupview extends IDisposable, IGridPanelView {
readonly isActive: boolean;
readonly size: number;
@ -99,7 +115,7 @@ export interface IGroupview extends IDisposable, IGridPanelView {
onDidGroupChange: Event<{ kind: GroupChangeKind }>;
onMove: Event<GroupMoveEvent>;
//
startActiveDrag(panel: IGroupPanel): IDisposable;
// startActiveDrag(panel: IGroupPanel): IDisposable;
//
moveToNext(options?: { panel?: IGroupPanel; suppressRoll?: boolean }): void;
moveToPrevious(options?: {
@ -108,12 +124,7 @@ export interface IGroupview extends IDisposable, IGridPanelView {
}): void;
isContentFocused(): boolean;
updateActions(): void;
}
export interface GroupDropEvent {
event: DragEvent;
target: Position;
index?: number;
canDisplayOverlay(event: DragEvent, target: DockviewDropTargets): boolean;
}
export class Groupview extends CompositeDisposable implements IGroupview {
@ -138,9 +149,6 @@ export class Groupview extends CompositeDisposable implements IGroupview {
private readonly _onMove = new Emitter<GroupMoveEvent>();
readonly onMove: Event<GroupMoveEvent> = this._onMove.event;
private readonly _onDrop = new Emitter<GroupDropEvent>();
readonly onDrop: Event<GroupDropEvent> = this._onDrop.event;
private readonly _onDidGroupChange = new Emitter<GroupChangeEvent>();
readonly onDidGroupChange: Event<{ kind: GroupChangeKind }> =
this._onDidGroupChange.event;
@ -205,24 +213,26 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.container.classList.add('groupview');
this.addDisposables(this._onMove, this._onDidGroupChange, this._onDrop);
this.addDisposables(this._onMove, this._onDidGroupChange);
this.tabsContainer = new TabsContainer(this.accessor, this.parent, {
tabHeight: options.tabHeight,
});
this.contentContainer = new ContentContainer();
this.dropTarget = new Droptarget(this.contentContainer.element, {
isDirectional: true,
id: this.accessor.id,
isDisabled: () => {
// disable the drop target if we only have one tab, and that is also the tab we are moving
return (
this._panels.length === 1 &&
this.tabsContainer.hasActiveDragEvent
);
validOverlays: 'all',
canDisplayOverlay: (event) => {
const data = getPanelData();
if (data) {
const groupHasOnePanelAndIsActiveDragElement =
this._panels.length === 1 && data.groupId === this.id;
return !groupHasOnePanelAndIsActiveDragElement;
}
return this.canDisplayOverlay(event, DockviewDropTargets.Panel);
},
enableExternalDragEvents:
this.accessor.options.enableExternalDragEvents,
});
container.append(
@ -233,28 +243,22 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.addDisposables(
this._onMove,
this._onDidGroupChange,
this.tabsContainer.onDropEvent((event) =>
this.handleDropEvent(event.event, event.index)
),
this.tabsContainer.onDrop((event) => {
this.handleDropEvent(event.event, Position.Center, event.index);
}),
this.contentContainer.onDidFocus(() => {
this.accessor.doSetGroupActive(this.parent, true);
}),
this.contentContainer.onDidBlur(() => {
// this._activePanel?.api._ondid
}),
this.dropTarget.onDidChange((event) => {
// if we've center dropped on ourself then ignore
if (
event.position === Position.Center &&
this.tabsContainer.hasActiveDragEvent
) {
return;
}
this.handleDropEvent(event);
this.dropTarget.onDrop((event) => {
this.handleDropEvent(event.event, event.position);
})
);
}
initialize() {
if (this.options?.panels) {
this.options.panels.forEach((panel) => {
this.doAddPanel(panel);
@ -264,9 +268,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
if (this.options?.activePanel) {
this.openPanel(this.options.activePanel);
}
}
initialize() {
// must be run after the constructor otherwise this.parent may not be
// correctly initialized
this.setActive(this.isActive, true, true);
@ -295,19 +297,19 @@ export class Groupview extends CompositeDisposable implements IGroupview {
};
}
public startActiveDrag(panel: IGroupPanel): IDisposable {
const index = this.tabsContainer.indexOf(panel.id);
if (index > -1) {
const tab = this.tabsContainer.at(index);
tab.startDragEvent();
return {
dispose: () => {
tab.stopDragEvent();
},
};
}
return Disposable.NONE;
}
// public startActiveDrag(panel: IGroupPanel): IDisposable {
// const index = this.tabsContainer.indexOf(panel.id);
// if (index > -1) {
// const tab = this.tabsContainer.at(index);
// tab.startDragEvent();
// return {
// dispose: () => {
// tab.stopDragEvent();
// },
// };
// }
// return Disposable.NONE;
// }
public moveToNext(options?: {
panel?: IGroupPanel;
@ -385,7 +387,10 @@ export class Groupview extends CompositeDisposable implements IGroupview {
panel: IGroupPanel,
options: { index?: number; skipFocus?: boolean } = {}
) {
if (typeof options.index !== 'number') {
if (
typeof options.index !== 'number' ||
options.index > this.panels.length
) {
options.index = this.panels.length;
}
if (this._activePanel === panel) {
@ -668,33 +673,34 @@ export class Groupview extends CompositeDisposable implements IGroupview {
}
}
private handleDropEvent(event: DroptargetEvent, index?: number) {
if (isPanelTransferEvent(event.event)) {
this.handlePanelDropEvent(event.event, event.position, index);
return;
}
this._onDrop.fire({
event: event.event,
target: event.position,
index,
});
console.debug('[customDropEvent]');
canDisplayOverlay(
dragOverEvent: DragEvent,
target: DockviewDropTargets
): boolean {
// custom overlay handler
return false;
}
private handlePanelDropEvent(
private handleDropEvent(
event: DragEvent,
target: Position,
position: Position,
index?: number
) {
const dataObject = extractData(event);
const data = getPanelData();
if (isTabDragEvent(dataObject)) {
const { groupId, itemId } = dataObject;
if (data) {
const fromSameGroup =
this.tabsContainer.indexOf(data.panelId) !== -1;
if (fromSameGroup && this.tabsContainer.size === 1) {
console.debug('[tabs] ignore event');
return;
}
const { groupId, panelId } = data;
const isSameGroup = this.id === groupId;
if (isSameGroup && !target) {
const oldIndex = this.tabsContainer.indexOf(itemId);
if (isSameGroup && !position) {
const oldIndex = this.tabsContainer.indexOf(panelId);
if (oldIndex === index) {
console.debug(
'[tabs] drop indicates no change in position'
@ -704,30 +710,13 @@ export class Groupview extends CompositeDisposable implements IGroupview {
}
this._onMove.fire({
target,
groupId: dataObject.groupId,
itemId: dataObject.itemId,
index,
});
}
if (isCustomDragEvent(dataObject)) {
let panel = this.accessor.getGroupPanel(dataObject.id);
if (!panel) {
panel = this.accessor.addPanel(dataObject);
}
if (!panel.group) {
throw new Error(`panel ${panel.id} has no associated group`);
}
this._onMove.fire({
target,
groupId: panel.group.id,
itemId: panel.id,
target: position,
groupId: data.groupId,
itemId: data.panelId,
index,
});
} else {
// custom drop handler
}
}

View File

@ -1,17 +1,14 @@
import { addDisposableListener, Emitter, Event } from '../events';
import { Droptarget, DroptargetEvent } from '../dnd/droptarget';
import { CompositeDisposable } from '../lifecycle';
import {
DATA_KEY,
DragType,
LocalSelectionTransfer,
} from '../dnd/dataTransfer';
import { getPanelData, LocalSelectionTransfer } from '../dnd/dataTransfer';
import { getElementsByTagName, toggleClass } from '../dom';
import { IDockviewComponent } from '../dockview/dockviewComponent';
import { ITabRenderer } from './types';
import { focusedElement } from '../focusedElement';
import { IGroupPanel } from './groupPanel';
import { GroupviewPanel } from './groupviewPanel';
import { DroptargetEvent, Droptarget, PanelTransfer } from '../dnd/droptarget';
import { DockviewDropTargets } from './groupview';
export enum MouseEventKind {
CLICK = 'CLICK',
@ -26,22 +23,16 @@ export interface LayoutMouseEvent {
}
export interface ITab {
id: string;
panelId: string;
element: HTMLElement;
hasActiveDragEvent: boolean;
setContent: (element: ITabRenderer) => void;
onChanged: Event<LayoutMouseEvent>;
onDropped: Event<DroptargetEvent>;
onDrop: Event<DroptargetEvent>;
setActive(isActive: boolean): void;
startDragEvent(): void;
stopDragEvent(): void;
}
export class Tab extends CompositeDisposable implements ITab {
private _element: HTMLElement;
private dragInPlayDetails: { id?: string; isDragging: boolean } = {
isDragging: false,
};
private droptarget: Droptarget;
private content?: ITabRenderer;
@ -49,28 +40,19 @@ export class Tab extends CompositeDisposable implements ITab {
readonly onChanged: Event<LayoutMouseEvent> = this._onChanged.event;
private readonly _onDropped = new Emitter<DroptargetEvent>();
readonly onDropped: Event<DroptargetEvent> = this._onDropped.event;
readonly onDrop: Event<DroptargetEvent> = this._onDropped.event;
private readonly panelTransfer =
LocalSelectionTransfer.getInstance<PanelTransfer>();
public get element() {
return this._element;
}
public get hasActiveDragEvent() {
return this.dragInPlayDetails?.isDragging;
}
public startDragEvent() {
this.dragInPlayDetails = { isDragging: true, id: this.accessor.id };
}
public stopDragEvent() {
this.dragInPlayDetails = { isDragging: false, id: undefined };
}
private iframes: HTMLElement[] = [];
constructor(
public id: string,
public panelId: string,
private readonly accessor: IDockviewComponent,
private group: GroupviewPanel
) {
@ -85,11 +67,6 @@ export class Tab extends CompositeDisposable implements ITab {
this.addDisposables(
addDisposableListener(this._element, 'dragstart', (event) => {
this.dragInPlayDetails = {
isDragging: true,
id: this.accessor.id,
};
this.iframes = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
@ -102,18 +79,18 @@ export class Tab extends CompositeDisposable implements ITab {
this.element.classList.add('dragged');
setTimeout(() => this.element.classList.remove('dragged'), 0);
const data = JSON.stringify({
type: DragType.ITEM,
itemId: this.id,
groupId: this.group.id,
});
LocalSelectionTransfer.getInstance().setData(
[data],
this.dragInPlayDetails.id
this.panelTransfer.setData(
[
new PanelTransfer(
this.accessor.id,
this.group.id,
this.panelId
),
],
PanelTransfer.prototype
);
if (event.dataTransfer) {
event.dataTransfer.setData(DATA_KEY, data);
event.dataTransfer.effectAllowed = 'move';
}
}),
@ -123,14 +100,7 @@ export class Tab extends CompositeDisposable implements ITab {
}
this.iframes = [];
// drop events fire before dragend so we can remove this safely
LocalSelectionTransfer.getInstance().clearData(
this.dragInPlayDetails.id
);
this.dragInPlayDetails = {
isDragging: false,
id: undefined,
};
this.panelTransfer.clearData(PanelTransfer.prototype);
}),
addDisposableListener(this._element, 'mousedown', (event) => {
if (event.defaultPrevented) {
@ -168,16 +138,22 @@ export class Tab extends CompositeDisposable implements ITab {
);
this.droptarget = new Droptarget(this._element, {
isDirectional: false,
isDisabled: () => this.dragInPlayDetails.isDragging,
id: this.accessor.id,
enableExternalDragEvents: this.accessor.options
.enableExternalDragEvents,
validOverlays: 'none',
canDisplayOverlay: (event) => {
const data = getPanelData();
if (data) {
return this.panelId !== data.panelId;
}
return this.group.model.canDisplayOverlay(
event,
DockviewDropTargets.Tab
);
},
});
this.addDisposables(
this.droptarget.onDidChange((event) => {
event.event.preventDefault();
this.droptarget.onDrop((event) => {
this._onDropped.fire(event);
})
);

View File

@ -10,8 +10,12 @@
display: none;
}
.tabs-container {
.void-container {
display: flex;
flex-grow: 1;
}
.tabs-container {
display: flex;
overflow-x: overlay;
overflow-y: hidden;
@ -32,10 +36,6 @@
background: var(--dv-tabs-container-scrollbar-color);
}
&.drag-over-target {
background-color: var(--dv-drag-over-background-color);
}
.tab {
-webkit-user-drag: element;
outline: none;

View File

@ -5,28 +5,28 @@ import {
} from '../../lifecycle';
import { addDisposableListener, Emitter, Event } from '../../events';
import { ITab, MouseEventKind, Tab } from '../tab';
import { removeClasses, addClasses } from '../../dom';
import { DroptargetEvent, Position } from '../../dnd/droptarget';
import { last } from '../../array';
import { IGroupPanel } from '../groupPanel';
import { IDockviewComponent } from '../../dockview/dockviewComponent';
import { LocalSelectionTransfer } from '../../dnd/dataTransfer';
import { getPanelData } from '../../dnd/dataTransfer';
import { GroupviewPanel } from '../groupviewPanel';
import { Droptarget } from '../../dnd/droptarget';
import { DockviewDropTargets } from '../groupview';
export interface TabDropEvent {
readonly event: DroptargetEvent;
readonly index?: number;
export interface TabDropIndexEvent {
event: DragEvent;
readonly index: number;
}
export interface ITabsContainer extends IDisposable {
readonly element: HTMLElement;
readonly panels: string[];
readonly hasActiveDragEvent: boolean;
readonly size: number;
height: number | undefined;
delete: (id: string) => void;
indexOf: (id: string) => number;
at: (index: number) => ITab;
onDropEvent: Event<TabDropEvent>;
onDrop: Event<TabDropIndexEvent>;
setActive: (isGroupActive: boolean) => void;
setActivePanel: (panel: IGroupPanel) => void;
isActive: (tab: ITab) => boolean;
@ -43,8 +43,11 @@ export class TabsContainer
{
private readonly _element: HTMLElement;
private readonly tabContainer: HTMLElement;
private readonly voidContainer: HTMLElement;
private readonly actionContainer: HTMLElement;
private readonly voidDropTarget: Droptarget;
private tabs: IValueDisposable<ITab>[] = [];
private selectedIndex = -1;
private active = false;
@ -53,11 +56,15 @@ export class TabsContainer
private _height: number | undefined;
private readonly _onDropped = new Emitter<TabDropEvent>();
readonly onDropEvent: Event<TabDropEvent> = this._onDropped.event;
private readonly _onDrop = new Emitter<TabDropIndexEvent>();
readonly onDrop: Event<TabDropIndexEvent> = this._onDrop.event;
get panels() {
return this.tabs.map((_) => _.value.id);
return this.tabs.map((_) => _.value.panelId);
}
get size() {
return this.tabs.length;
}
get height(): number | undefined {
@ -67,20 +74,15 @@ export class TabsContainer
set height(value: number | undefined) {
this._height = value;
if (typeof value !== 'number') {
// removeClasses(this.element, 'separator-border');
this.element.style.removeProperty(
'--dv-tabs-and-actions-container-height'
);
} else {
// addClasses(this.element, 'separator-border');
// if (styles?.separatorBorder) {
this.element.style.setProperty(
'--dv-tabs-and-actions-container-height',
`${value}px`
);
// }
}
// this._element.style.height = `${this.height}px`;
}
show() {
@ -116,16 +118,12 @@ export class TabsContainer
);
}
public get hasActiveDragEvent() {
return !!this.tabs.find((tab) => tab.value.hasActiveDragEvent);
}
public at(index: number) {
return this.tabs[index]?.value;
}
public indexOf(id: string): number {
return this.tabs.findIndex((tab) => tab.value.id === id);
return this.tabs.findIndex((tab) => tab.value.panelId === id);
}
constructor(
@ -135,7 +133,7 @@ export class TabsContainer
) {
super();
this.addDisposables(this._onDropped);
this.addDisposables(this._onDrop);
this._element = document.createElement('div');
this._element.className = 'tabs-and-actions-container';
@ -148,10 +146,38 @@ export class TabsContainer
this.tabContainer = document.createElement('div');
this.tabContainer.className = 'tabs-container';
this.voidContainer = document.createElement('div');
this.voidContainer.className = 'void-container';
this._element.appendChild(this.tabContainer);
this._element.appendChild(this.voidContainer);
this._element.appendChild(this.actionContainer);
this.voidDropTarget = new Droptarget(this.voidContainer, {
validOverlays: 'none',
canDisplayOverlay: (event) => {
const data = getPanelData();
if (data) {
// don't show the overlay if the tab being dragged is the last panel of this group
return last(this.tabs)?.value.panelId !== data.panelId;
}
return group.model.canDisplayOverlay(
event,
DockviewDropTargets.Panel
);
},
});
this.addDisposables(
this.voidDropTarget.onDrop((event) => {
this._onDrop.fire({
event: event.event,
index: this.tabs.length,
});
}),
this.voidDropTarget,
addDisposableListener(this.tabContainer, 'mousedown', (event) => {
if (event.defaultPrevented) {
return;
@ -162,62 +188,6 @@ export class TabsContainer
if (isLeftClick) {
this.accessor.doSetGroupActive(this.group);
}
}),
addDisposableListener(this.tabContainer, 'dragenter', (event) => {
if (
!LocalSelectionTransfer.getInstance().hasData(
this.accessor.id
)
) {
console.debug('[tabs] invalid drop event');
return;
}
if (!last(this.tabs)?.value.hasActiveDragEvent) {
addClasses(this.tabContainer, 'drag-over-target');
}
}),
addDisposableListener(this.tabContainer, 'dragover', (event) => {
event.preventDefault();
}),
addDisposableListener(this.tabContainer, 'dragleave', (event) => {
removeClasses(this.tabContainer, 'drag-over-target');
}),
addDisposableListener(this.tabContainer, 'drop', (event) => {
if (
!LocalSelectionTransfer.getInstance().hasData(
this.accessor.id
)
) {
console.debug('[tabs] invalid drop event');
return;
}
if (event.defaultPrevented) {
console.debug('[tab] drop event defaultprevented');
return;
}
removeClasses(this.tabContainer, 'drag-over-target');
const activetab = this.tabs.find(
(tab) => tab.value.hasActiveDragEvent
);
const ignore = !!(
activetab &&
event
.composedPath()
.find((x) => activetab.value.element === x)
);
if (ignore) {
console.debug('[tabs] ignore event');
return;
}
this._onDropped.fire({
event: { event, position: Position.Center },
index: this.tabs.length - (activetab ? 1 : 0),
});
})
);
}
@ -251,7 +221,7 @@ export class TabsContainer
}
public delete(id: string) {
const index = this.tabs.findIndex((tab) => tab.value.id === id);
const index = this.tabs.findIndex((tab) => tab.value.panelId === id);
const tabToRemove = this.tabs.splice(index, 1)[0];
@ -263,13 +233,13 @@ export class TabsContainer
public setActivePanel(panel: IGroupPanel) {
this.tabs.forEach((tab) => {
const isActivePanel = panel.id === tab.value.id;
const isActivePanel = panel.id === tab.value.panelId;
tab.value.setActive(isActivePanel);
});
}
public openPanel(panel: IGroupPanel, index: number = this.tabs.length) {
if (this.tabs.find((tab) => tab.value.id === panel.id)) {
if (this.tabs.find((tab) => tab.value.panelId === panel.id)) {
return;
}
const tabToAdd = new Tab(panel.id, this.accessor, this.group);
@ -299,9 +269,9 @@ export class TabsContainer
break;
}
}),
tabToAdd.onDropped((event) => {
this._onDropped.fire({
event,
tabToAdd.onDrop((event) => {
this._onDrop.fire({
event: event.event,
index: this.tabs.findIndex((x) => x.value === tabToAdd),
});
})

View File

@ -0,0 +1,126 @@
import { DragHandler } from '../dnd/abstractDragHandler';
import { getPaneData, LocalSelectionTransfer } from '../dnd/dataTransfer';
import {
Droptarget,
DroptargetEvent,
PaneTransfer,
Position,
} from '../dnd/droptarget';
import { Emitter, Event } from '../events';
import { IDisposable } from '../lifecycle';
import { Orientation } from '../splitview/core/splitview';
import { PanePanelInitParameter, PaneviewPanel } from './paneviewPanel';
interface ViewContainer {
readonly title: string;
readonly icon: string;
}
interface ViewContainerModel {
readonly title: string;
readonly icon: string;
readonly onDidAdd: Event<void>;
readonly onDidRemove: Event<void>;
}
interface IViewContainerService {
getViewContainerById(id: string): ViewContainer;
getViewContainerModel(container: ViewContainer): ViewContainerModel;
}
export abstract class DraggablePaneviewPanel extends PaneviewPanel {
private handler: DragHandler | undefined;
private target: Droptarget | undefined;
private readonly _onDidDrop = new Emitter<DroptargetEvent>();
readonly onDidDrop = this._onDidDrop.event;
constructor(
id: string,
component: string,
headerComponent: string | undefined,
orientation: Orientation,
isExpanded: boolean,
disableDnd: boolean
) {
super(id, component, headerComponent, orientation, isExpanded);
if (!disableDnd) {
this.initDragFeatures();
}
}
private initDragFeatures() {
const id = this.id;
this.header!.draggable = true;
this.header!.tabIndex = 0;
this.handler = new (class PaneDragHandler extends DragHandler {
getData(): IDisposable {
LocalSelectionTransfer.getInstance().setData(
[new PaneTransfer('paneview', id)],
PaneTransfer.prototype
);
return {
dispose: () => {
LocalSelectionTransfer.getInstance().clearData(
PaneTransfer.prototype
);
},
};
}
})(this.header!);
this.target = new Droptarget(this.element, {
validOverlays: 'vertical',
canDisplayOverlay: (event: DragEvent) => {
const data = getPaneData();
if (!data) {
return true;
}
return data.paneId !== this.id;
},
});
this.addDisposables(
this._onDidDrop,
this.handler,
this.target,
this.target.onDrop((event) => {
const data = getPaneData();
if (!data) {
this._onDidDrop.fire(event);
return;
}
const containerApi = (this.params! as PanePanelInitParameter)
.containerApi;
const id = data.paneId;
const existingPanel = containerApi.getPanel(id);
if (!existingPanel) {
this._onDidDrop.fire(event);
return;
}
const fromIndex = containerApi
.getPanels()
.indexOf(existingPanel);
let toIndex = containerApi.getPanels().indexOf(this);
if (
event.position === Position.Right ||
event.position === Position.Bottom
) {
toIndex = Math.max(0, toIndex + 1);
}
containerApi.movePanel(fromIndex, toIndex);
})
);
}
}

View File

@ -22,4 +22,5 @@ export interface PaneviewComponentOptions {
header: FrameworkFactory<IPaneHeaderPart>;
body: FrameworkFactory<IPaneBodyPart>;
};
disableDnd?: boolean;
}

View File

@ -58,11 +58,6 @@
outline-offset: -1px;
outline-color: var(--dv-paneview-active-outline-color);
}
// outline-width: 1px;
// outline-style: solid;
// outline-offset: -1px;
// opacity: 1 !important;
// outline-color: dodgerblue;
}
}
.pane-body {

View File

@ -24,6 +24,13 @@ export class Paneview extends CompositeDisposable implements IDisposable {
private readonly _onDidChange = new Emitter<void>();
readonly onDidChange: Event<void> = this._onDidChange.event;
get onDidAddView() {
return <Event<PaneviewPanel>>this.splitview.onDidAddView;
}
get onDidRemoveView() {
return <Event<PaneviewPanel>>this.splitview.onDidRemoveView;
}
get minimumSize() {
return this.splitview.minimumSize;
}
@ -66,10 +73,12 @@ export class Paneview extends CompositeDisposable implements IDisposable {
// if we've added views from the descriptor we need to
// add the panes to our Pane array and setup animation
this.getPanes().forEach((pane, index) => {
const disposable = pane.onDidChangeExpansionState(() => {
this.setupAnimation();
this._onDidChange.fire(undefined);
});
const disposable = new CompositeDisposable(
pane.onDidChangeExpansionState(() => {
this.setupAnimation();
this._onDidChange.fire(undefined);
})
);
const paneItem: PaneItem = {
pane,
@ -112,6 +121,7 @@ export class Paneview extends CompositeDisposable implements IDisposable {
};
this.paneItems.splice(index, 0, paneItem);
pane.orthogonalSize = this.splitview.orthogonalSize;
this.splitview.addView(pane, size, index, skipLayout);
}
@ -131,9 +141,21 @@ export class Paneview extends CompositeDisposable implements IDisposable {
return paneItem;
}
private skipAnimation = false;
public moveView(from: number, to: number) {
if (from === to) {
return;
}
const view = this.removePane(from);
this.addPane(view.pane, to);
this.skipAnimation = true;
try {
this.addPane(view.pane, view.pane.size, to, false);
} finally {
this.skipAnimation = false;
}
}
public layout(size: number, orthogonalSize: number): void {
@ -145,6 +167,10 @@ export class Paneview extends CompositeDisposable implements IDisposable {
}
private setupAnimation() {
if (this.skipAnimation) {
return;
}
if (this.animationTimer) {
clearTimeout(this.animationTimer);
this.animationTimer = undefined;

View File

@ -22,6 +22,8 @@ import {
PanePanelInitParameter,
IPaneviewPanel,
} from './paneviewPanel';
import { DraggablePaneviewPanel } from './draggablePaneviewPanel';
import { DroptargetEvent } from '../dnd/droptarget';
export interface SerializedPaneviewPanel {
snap?: boolean;
@ -74,7 +76,7 @@ class DefaultHeader extends CompositeDisposable implements IPaneHeaderPart {
}
}
export class PaneFramework extends PaneviewPanel {
export class PaneFramework extends DraggablePaneviewPanel {
constructor(
private readonly options: {
id: string;
@ -84,6 +86,7 @@ export class PaneFramework extends PaneviewPanel {
header: IPaneHeaderPart;
orientation: Orientation;
isExpanded: boolean;
disableDnd: boolean;
}
) {
super(
@ -91,7 +94,8 @@ export class PaneFramework extends PaneviewPanel {
options.component,
options.headerComponent,
options.orientation,
options.isExpanded
options.isExpanded,
options.disableDnd
);
}
@ -142,13 +146,25 @@ export interface IPaneviewComponent extends IDisposable {
export class PaneviewComponent
extends CompositeDisposable
implements IPaneviewComponent {
implements IPaneviewComponent
{
private _disposable = new MutableDisposable();
private _paneview!: Paneview;
private readonly _onDidLayoutChange = new Emitter<void>();
readonly onDidLayoutChange: Event<void> = this._onDidLayoutChange.event;
private readonly _onDidDrop = new Emitter<DroptargetEvent>();
readonly onDidDrop: Event<DroptargetEvent> = this._onDidDrop.event;
get onDidAddView() {
return this._paneview.onDidAddView;
}
get onDidRemoveView() {
return this._paneview.onDidRemoveView;
}
set paneview(value: Paneview) {
this._paneview = value;
@ -216,8 +232,8 @@ export class PaneviewComponent
this.options.frameworkComponents || {},
this.options.frameworkWrapper
? {
createComponent: this.options.frameworkWrapper.body
.createComponent,
createComponent:
this.options.frameworkWrapper.body.createComponent,
}
: undefined
);
@ -232,8 +248,9 @@ export class PaneviewComponent
this.options.headerframeworkComponents,
this.options.frameworkWrapper
? {
createComponent: this.options.frameworkWrapper.header
.createComponent,
createComponent:
this.options.frameworkWrapper.header
.createComponent,
}
: undefined
);
@ -249,6 +266,11 @@ export class PaneviewComponent
body,
orientation: Orientation.VERTICAL,
isExpanded: !!options.isExpanded,
disableDnd: !!this.options.disableDnd,
});
view.onDidDrop((event) => {
this._onDidDrop.fire(event);
});
const size: Sizing | number =
@ -309,10 +331,8 @@ export class PaneviewComponent
if (!this.element.parentElement) {
return;
}
const {
width,
height,
} = this.element.parentElement.getBoundingClientRect();
const { width, height } =
this.element.parentElement.getBoundingClientRect();
this.layout(width, height);
}
@ -367,8 +387,9 @@ export class PaneviewComponent
this.options.frameworkComponents || {},
this.options.frameworkWrapper
? {
createComponent: this.options.frameworkWrapper
.body.createComponent,
createComponent:
this.options.frameworkWrapper.body
.createComponent,
}
: undefined
);
@ -383,9 +404,9 @@ export class PaneviewComponent
this.options.headerframeworkComponents || {},
this.options.frameworkWrapper
? {
createComponent: this.options
.frameworkWrapper.header
.createComponent,
createComponent:
this.options.frameworkWrapper.header
.createComponent,
}
: undefined
);
@ -401,6 +422,11 @@ export class PaneviewComponent
body,
orientation: Orientation.VERTICAL,
isExpanded: !!view.expanded,
disableDnd: !!this.options.disableDnd,
});
panel.onDidDrop((event) => {
this._onDidDrop.fire(event);
});
queue.push(() => {

View File

@ -61,7 +61,8 @@ export interface IPaneviewPanel
export abstract class PaneviewPanel
extends BasePanelView<PaneviewPanelApiImpl>
implements IPaneview, IPaneviewPanel {
implements IPaneview, IPaneviewPanel
{
private _onDidChangeExpansionState: Emitter<boolean> = new Emitter<boolean>(
{ replay: true }
);
@ -71,6 +72,7 @@ export abstract class PaneviewPanel
private headerSize = 22;
private _orthogonalSize = 0;
private _size = 0;
private _minimumBodySize = 0;
private _maximumBodySize: number = Number.POSITIVE_INFINITY;
private _isExpanded = false;
@ -80,7 +82,6 @@ export abstract class PaneviewPanel
private headerPart?: IPaneBodyPart;
private expandedSize = 0;
private animationTimer: any | undefined;
private _orientation: Orientation;
set orientation(value: Orientation) {
@ -107,6 +108,10 @@ export abstract class PaneviewPanel
return headerSize + maximumBodySize;
}
get size() {
return this._size;
}
get orthogonalSize() {
return this._orthogonalSize;
}
@ -176,7 +181,7 @@ export abstract class PaneviewPanel
})
);
this.render();
this.renderOnce();
}
setVisible(isVisible: boolean) {
@ -216,6 +221,8 @@ export abstract class PaneviewPanel
}
layout(size: number, orthogonalSize: number) {
this._size = size;
this._orthogonalSize = orthogonalSize;
const [width, height] =
this.orientation === Orientation.HORIZONTAL
? [size, orthogonalSize]
@ -259,7 +266,7 @@ export abstract class PaneviewPanel
};
}
private render() {
private renderOnce() {
this.header = document.createElement('div');
this.header.tabIndex = -1;

View File

@ -0,0 +1,44 @@
import * as React from 'react';
import {
CanDisplayOverlay,
Droptarget,
DropTargetDirections,
} from '../dnd/droptarget';
export interface IDragTragetProps {
canDisplayOverlay: CanDisplayOverlay;
validOverlays: DropTargetDirections;
children: React.ReactNode;
}
export const DockviewDropTarget = React.forwardRef(
(props: IDragTragetProps, ref: React.ForwardedRef<HTMLDivElement>) => {
const domRef = React.useRef<HTMLDivElement>(null);
const dropTargetRef = React.useRef<Droptarget>();
React.useImperativeHandle(ref, () => domRef.current!, []);
React.useEffect(() => {
dropTargetRef.current = new Droptarget(domRef.current!, {
canDisplayOverlay: props.canDisplayOverlay,
validOverlays: props.validOverlays,
});
return () => {
dropTargetRef.current?.dispose();
};
}, []);
React.useEffect(() => {
dropTargetRef.current!.validOverlays = props.validOverlays;
}, [props.validOverlays]);
React.useEffect(() => {
dropTargetRef.current!.canDisplayOverlay = props.canDisplayOverlay;
}, [props.canDisplayOverlay]);
return <div ref={domRef}>{props.children}</div>;
}
);
DockviewDropTarget.displayName = 'DockviewDropTarget';

View File

@ -8,3 +8,4 @@ export * from '../gridview/gridviewPanel';
export * from './paneview/paneview';
export * from './types';
export * from './react';
export * from './dropTarget';

View File

@ -9,6 +9,7 @@ import { PaneviewApi } from '../../api/component.api';
import { PanePanelSection } from './view';
import { PanelCollection, PanelParameters } from '../types';
import { watchElementResize } from '../../dom';
import { DroptargetEvent } from '../../dnd/droptarget';
export interface PaneviewReadyEvent {
api: PaneviewApi;
@ -21,12 +22,19 @@ export interface IPaneviewPanelProps<T extends {} = Record<string, any>>
title: string;
}
export interface PaneviewDropEvent {
api: PaneviewApi;
event: DroptargetEvent;
}
export interface IPaneviewReactProps {
onReady?: (event: PaneviewReadyEvent) => void;
components?: PanelCollection<IPaneviewPanelProps>;
headerComponents?: PanelCollection<IPaneviewPanelProps>;
className?: string;
disableAutoResizing?: boolean;
disableDnd?: boolean;
onDidDrop?(event: PaneviewDropEvent): void;
}
export const PaneviewReact = React.forwardRef(
@ -68,6 +76,7 @@ export const PaneviewReact = React.forwardRef(
frameworkComponents: props.components,
components: {},
headerComponents: {},
disableDnd: props.disableDnd,
headerframeworkComponents: props.headerComponents,
frameworkWrapper: {
header: {
@ -79,16 +88,25 @@ export const PaneviewReact = React.forwardRef(
},
});
const api = new PaneviewApi(paneview);
const disposable = paneview.onDidDrop((event) => {
if (props.onDidDrop) {
props.onDidDrop({ event, api });
}
});
const { clientWidth, clientHeight } = domRef.current!;
paneview.layout(clientWidth, clientHeight);
if (props.onReady) {
props.onReady({ api: new PaneviewApi(paneview) });
props.onReady({ api });
}
paneviewRef.current = paneview;
return () => {
disposable.dispose();
paneview.dispose();
};
}, []);

View File

@ -106,8 +106,12 @@ export class Splitview {
private _proportions: number[] | undefined = undefined;
private proportionalLayout: boolean;
private _onDidSashEnd = new Emitter<void>();
public onDidSashEnd = this._onDidSashEnd.event;
private readonly _onDidSashEnd = new Emitter<void>();
readonly onDidSashEnd = this._onDidSashEnd.event;
private readonly _onDidAddView = new Emitter<IView>();
readonly onDidAddView = this._onDidAddView.event;
private readonly _onDidRemoveView = new Emitter<IView>();
readonly onDidRemoveView = this._onDidAddView.event;
get size() {
return this._size;
@ -548,6 +552,8 @@ export class Splitview {
) {
this.distributeViewSizes();
}
this._onDidAddView.fire(view);
}
distributeViewSizes(): void {
@ -602,6 +608,8 @@ export class Splitview {
this.distributeViewSizes();
}
this._onDidRemoveView.fire(viewItem.view);
return viewItem.view;
}
@ -1033,6 +1041,10 @@ export class Splitview {
}
public dispose() {
this._onDidSashEnd.dispose();
this._onDidAddView.dispose();
this._onDidRemoveView.dispose();
this.element.remove();
for (let i = 0; i < this.element.children.length; i++) {
if (this.element.children.item(i) === this.element) {

View File

@ -7,6 +7,7 @@
--dv-tab-close-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>');
--dv-tab-dirty-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"/></svg>');
--dv-drag-over-background-color: rgba(83, 89, 93, 0.5);
--dv-drag-over-border-color: white;
--dv-tabs-container-scrollbar-color: #888;
}