Merge pull request #7 from mathuo/refactor-dnd

work in progress
This commit is contained in:
mathuo 2021-07-14 20:32:23 +01:00 committed by GitHub
commit bce106217a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1159 additions and 628 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -26,17 +26,16 @@ describe('droptarget', () => {
let position: Position | undefined = undefined; let position: Position | undefined = undefined;
droptarget = new Droptarget(element, { droptarget = new Droptarget(element, {
isDisabled: () => false, canDisplayOverlay: () => true,
isDirectional: false, validOverlays: 'none',
id: 'test-dnd',
enableExternalDragEvents: true,
}); });
droptarget.onDidChange((event) => { droptarget.onDrop((event) => {
position = event.position; position = event.position;
}); });
fireEvent.dragEnter(element); fireEvent.dragEnter(element);
fireEvent.dragOver(element);
const target = element.querySelector( const target = element.querySelector(
'.drop-target-dropzone' '.drop-target-dropzone'
@ -49,17 +48,16 @@ describe('droptarget', () => {
let position: Position | undefined = undefined; let position: Position | undefined = undefined;
droptarget = new Droptarget(element, { droptarget = new Droptarget(element, {
isDisabled: () => false, canDisplayOverlay: () => true,
isDirectional: true, validOverlays: 'all',
id: 'test-dnd',
enableExternalDragEvents: true,
}); });
droptarget.onDidChange((event) => { droptarget.onDrop((event) => {
position = event.position; position = event.position;
}); });
fireEvent.dragEnter(element); fireEvent.dragEnter(element);
fireEvent.dragOver(element);
const target = element.querySelector( const target = element.querySelector(
'.drop-target-dropzone' '.drop-target-dropzone'
@ -80,15 +78,14 @@ describe('droptarget', () => {
test('default', () => { test('default', () => {
droptarget = new Droptarget(element, { droptarget = new Droptarget(element, {
isDisabled: () => false, canDisplayOverlay: () => true,
isDirectional: true, validOverlays: 'all',
id: 'test-dnd',
enableExternalDragEvents: true,
}); });
expect(droptarget.state).toBeUndefined(); expect(droptarget.state).toBeUndefined();
fireEvent.dragEnter(element); fireEvent.dragEnter(element);
fireEvent.dragOver(element);
let viewQuery = element.querySelectorAll( let viewQuery = element.querySelectorAll(
'.drop-target > .drop-target-dropzone > .drop-target-selection' '.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 { ReactPanelDeserialzier } from '../../react/deserializer';
import { Position } from '../../dnd/droptarget'; import { Position } from '../../dnd/droptarget';
import { GroupviewPanel } from '../../groupview/groupviewPanel'; import { GroupviewPanel } from '../../groupview/groupviewPanel';
import { IGroupPanel } from '../../groupview/groupPanel';
class PanelContentPartTest implements IContentRenderer { class PanelContentPartTest implements IContentRenderer {
element: HTMLElement = document.createElement('div'); element: HTMLElement = document.createElement('div');
@ -357,6 +358,7 @@ describe('dockviewComponent', () => {
data: { data: {
views: ['panel1'], views: ['panel1'],
id: 'group-1', id: 'group-1',
activeView: 'panel1',
}, },
size: 500, size: 500,
}, },

View File

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

View File

@ -1,6 +1,5 @@
import { import {
IDockviewComponent, IDockviewComponent,
LayoutDropEvent,
SerializedDockview, SerializedDockview,
} from '../dockview/dockviewComponent'; } from '../dockview/dockviewComponent';
import { import {
@ -339,19 +338,19 @@ export class DockviewApi {
return this.component.addPanel(options); return this.component.addPanel(options);
} }
addDndHandle(type: string, cb: (event: LayoutDropEvent) => PanelOptions) { // addDndHandle(type: string, cb: (event: LayoutDropEvent) => PanelOptions) {
return this.component.addDndHandle(type, cb); // return this.component.addDndHandle(type, cb);
} // }
createDragTarget( // createDragTarget(
target: { // target: {
element: HTMLElement; // element: HTMLElement;
content: string; // content: string;
}, // },
options: (() => PanelOptions) | PanelOptions // options: (() => PanelOptions) | PanelOptions
) { // ) {
return this.component.createDragTarget(target, options); // return this.component.createDragTarget(target, options);
} // }
addEmptyGroup(options?: AddGroupOptions) { addEmptyGroup(options?: AddGroupOptions) {
return this.component.addEmptyGroup(options); 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 { PanelOptions } from '../dockview/options';
import { tryParseJSON } from '../json'; import { tryParseJSON } from '../json';
import { PanelTransfer, PaneTransfer } from './droptarget';
export const DATA_KEY = 'splitview/transfer'; export const DATA_KEY = 'splitview/transfer';
@ -12,7 +13,7 @@ export const isPanelTransferEvent = (event: DragEvent) => {
}; };
export enum DragType { export enum DragType {
ITEM = 'group_drag', DOCKVIEW_TAB = 'dockview_tab',
EXTERNAL = 'external_group_drag', EXTERNAL = 'external_group_drag',
} }
@ -30,7 +31,7 @@ export type DataObject = DragItem | ExternalDragItem;
* dragging a tab component * dragging a tab component
*/ */
export const isTabDragEvent = (data: any): data is DragItem => { 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 { > .drop-target-selection {
position: relative; position: relative;
pointer-events: none; pointer-events: none;
box-sizing: border-box;
height: 100%; height: 100%;
width: 100%; width: 100%;
background-color: var(--dv-drag-over-background-color); background-color: var(--dv-drag-over-background-color);
@ -33,6 +34,19 @@
&.bottom { &.bottom {
height: 50%; 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 { toggleClass } from '../dom';
import { Emitter, Event } from '../events'; 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 { export enum Position {
Top = 'Top', Top = 'Top',
@ -15,146 +46,176 @@ export interface DroptargetEvent {
event: DragEvent; 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 target: HTMLElement | undefined;
private overlay: HTMLElement | undefined; private overlay: HTMLElement | undefined;
private _state: Position | undefined; private _state: Position | undefined;
private readonly _onDidChange = new Emitter<DroptargetEvent>(); private readonly _onDrop = new Emitter<DroptargetEvent>();
readonly onDidChange: Event<DroptargetEvent> = this._onDidChange.event; readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
get state() { get state() {
return this._state; return this._state;
} }
set validOverlays(value: DropTargetDirections) {
this.options.validOverlays = value;
}
set canDisplayOverlay(value: CanDisplayOverlay) {
this.options.canDisplayOverlay = value;
}
constructor( constructor(
private element: HTMLElement, private readonly element: HTMLElement,
private options: { private readonly options: {
isDisabled: () => boolean; canDisplayOverlay: CanDisplayOverlay;
isDirectional: boolean; validOverlays: DropTargetDirections;
id: string;
enableExternalDragEvents?: boolean;
} }
) { ) {
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() { public dispose() {
this._onDidChange.dispose(); this._onDrop.dispose();
this.removeDropTarget(); 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() { private removeDropTarget() {
if (this.target) { if (this.target) {
this._state = undefined; 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.element.removeChild(this.target);
this.target = undefined; this.target = undefined;
this.element.classList.remove('drop-target'); 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 { .tab {
flex-shrink: 0; flex-shrink: 0;
&.dragged { // &.dragged {
transform: translate3d( // transform: translate3d(
0px, // 0px,
0px, // 0px,
0px // 0px
); /* forces tab to be drawn on a separate layer (see https://github.com/microsoft/vscode/issues/18733) */ // ); /* forces tab to be drawn on a separate layer (see https://github.com/microsoft/vscode/issues/18733) */
} // }
&.dragging { &.dragging {
.tab-action { .tab-action {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -58,11 +58,6 @@
outline-offset: -1px; outline-offset: -1px;
outline-color: var(--dv-paneview-active-outline-color); 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 { .pane-body {

View File

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

View File

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

View File

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

View File

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

View File

@ -106,8 +106,12 @@ export class Splitview {
private _proportions: number[] | undefined = undefined; private _proportions: number[] | undefined = undefined;
private proportionalLayout: boolean; private proportionalLayout: boolean;
private _onDidSashEnd = new Emitter<void>(); private readonly _onDidSashEnd = new Emitter<void>();
public onDidSashEnd = this._onDidSashEnd.event; 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() { get size() {
return this._size; return this._size;
@ -548,6 +552,8 @@ export class Splitview {
) { ) {
this.distributeViewSizes(); this.distributeViewSizes();
} }
this._onDidAddView.fire(view);
} }
distributeViewSizes(): void { distributeViewSizes(): void {
@ -602,6 +608,8 @@ export class Splitview {
this.distributeViewSizes(); this.distributeViewSizes();
} }
this._onDidRemoveView.fire(viewItem.view);
return viewItem.view; return viewItem.view;
} }
@ -1033,6 +1041,10 @@ export class Splitview {
} }
public dispose() { public dispose() {
this._onDidSashEnd.dispose();
this._onDidAddView.dispose();
this._onDidRemoveView.dispose();
this.element.remove(); this.element.remove();
for (let i = 0; i < this.element.children.length; i++) { for (let i = 0; i < this.element.children.length; i++) {
if (this.element.children.item(i) === this.element) { 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-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-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-background-color: rgba(83, 89, 93, 0.5);
--dv-drag-over-border-color: white;
--dv-tabs-container-scrollbar-color: #888; --dv-tabs-container-scrollbar-color: #888;
} }