From adf09a1a25ceee0accc43aad2b576ab650e4ccf7 Mon Sep 17 00:00:00 2001
From: mathuo <{ID}+{username}@users.noreply.github.com>
Date: Tue, 29 Jun 2021 22:12:05 +0100
Subject: [PATCH] work in progress
---
.../src/layout-grid/activitybar.tsx | 20 +-
.../src/layout-grid/controlCenter.tsx | 26 +-
.../src/layout-grid/layoutGrid.tsx | 32 +-
.../dockview-demo/src/layout-grid/sidebar.tsx | 6 +
.../src/__tests__/dnd/droptarget.spec.ts | 25 +-
.../dockview/dockviewComponent.spec.ts | 2 +
.../src/__tests__/groupview/groupview.spec.ts | 60 ++--
packages/dockview/src/api/component.api.ts | 25 +-
.../dockview/src/dnd/abstractDragHandler.ts | 100 ++++++
packages/dockview/src/dnd/dataTransfer.ts | 27 +-
packages/dockview/src/dnd/dnd.ts | 181 +++++++++++
packages/dockview/src/dnd/droptarget.scss | 14 +
packages/dockview/src/dnd/droptarget.ts | 301 +++++++++++-------
.../dockview/components/tab/defaultTab.scss | 22 +-
.../src/dockview/dockviewComponent.ts | 191 ++++-------
packages/dockview/src/gridview/gridview.ts | 5 +-
packages/dockview/src/groupview/groupview.ts | 189 ++++++-----
packages/dockview/src/groupview/tab.ts | 90 ++----
.../src/groupview/titlebar/tabsContainer.scss | 10 +-
.../src/groupview/titlebar/tabsContainer.ts | 138 ++++----
.../src/paneview/draggablePaneviewPanel.ts | 126 ++++++++
packages/dockview/src/paneview/options.ts | 1 +
packages/dockview/src/paneview/paneview.scss | 5 -
packages/dockview/src/paneview/paneview.ts | 36 ++-
.../src/paneview/paneviewComponent.ts | 58 +++-
.../dockview/src/paneview/paneviewPanel.ts | 15 +-
packages/dockview/src/react/dropTarget.tsx | 44 +++
packages/dockview/src/react/index.ts | 1 +
.../dockview/src/react/paneview/paneview.tsx | 20 +-
.../dockview/src/splitview/core/splitview.ts | 16 +-
packages/dockview/src/theme.scss | 1 +
31 files changed, 1159 insertions(+), 628 deletions(-)
create mode 100644 packages/dockview/src/dnd/abstractDragHandler.ts
create mode 100644 packages/dockview/src/dnd/dnd.ts
create mode 100644 packages/dockview/src/paneview/draggablePaneviewPanel.ts
create mode 100644 packages/dockview/src/react/dropTarget.tsx
diff --git a/packages/dockview-demo/src/layout-grid/activitybar.tsx b/packages/dockview-demo/src/layout-grid/activitybar.tsx
index 36bc7e1ed..d3bd310f6 100644
--- a/packages/dockview-demo/src/layout-grid/activitybar.tsx
+++ b/packages/dockview-demo/src/layout-grid/activitybar.tsx
@@ -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 (
);
};
diff --git a/packages/dockview-demo/src/layout-grid/controlCenter.tsx b/packages/dockview-demo/src/layout-grid/controlCenter.tsx
index 4c0494aae..acddacf06 100644
--- a/packages/dockview-demo/src/layout-grid/controlCenter.tsx
+++ b/packages/dockview-demo/src/layout-grid/controlCenter.tsx
@@ -8,20 +8,20 @@ export const ControlCenter = () => {
const dragRef = React.useRef();
- React.useEffect(() => {
- const api = registry.get('dockview');
- const target = api.createDragTarget(
- { element: dragRef.current, content: 'drag me' },
- () => ({
- id: 'yellow',
- component: 'test_component',
- })
- );
+ // React.useEffect(() => {
+ // const api = registry.get('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');
diff --git a/packages/dockview-demo/src/layout-grid/layoutGrid.tsx b/packages/dockview-demo/src/layout-grid/layoutGrid.tsx
index 67427c962..e26deaaf0 100644
--- a/packages/dockview-demo/src/layout-grid/layoutGrid.tsx
+++ b/packages/dockview-demo/src/layout-grid/layoutGrid.tsx
@@ -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) {
diff --git a/packages/dockview-demo/src/layout-grid/sidebar.tsx b/packages/dockview-demo/src/layout-grid/sidebar.tsx
index d4e8f5ed1..6b3b33754 100644
--- a/packages/dockview-demo/src/layout-grid/sidebar.tsx
+++ b/packages/dockview-demo/src/layout-grid/sidebar.tsx
@@ -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 (
{
headerComponents={headerComponents}
components={components}
onReady={onReady}
+ onDidDrop={onDidDrop}
/>
);
diff --git a/packages/dockview/src/__tests__/dnd/droptarget.spec.ts b/packages/dockview/src/__tests__/dnd/droptarget.spec.ts
index 0337f73ad..ad4f30f28 100644
--- a/packages/dockview/src/__tests__/dnd/droptarget.spec.ts
+++ b/packages/dockview/src/__tests__/dnd/droptarget.spec.ts
@@ -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'
diff --git a/packages/dockview/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview/src/__tests__/dockview/dockviewComponent.spec.ts
index 5e00c6863..71544c376 100644
--- a/packages/dockview/src/__tests__/dockview/dockviewComponent.spec.ts
+++ b/packages/dockview/src/__tests__/dockview/dockviewComponent.spec.ts
@@ -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,
},
diff --git a/packages/dockview/src/__tests__/groupview/groupview.spec.ts b/packages/dockview/src/__tests__/groupview/groupview.spec.ts
index 9983d2229..266eaf36c 100644
--- a/packages/dockview/src/__tests__/groupview/groupview.spec.ts
+++ b/packages/dockview/src/__tests__/groupview/groupview.spec.ts
@@ -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);
+ // });
});
diff --git a/packages/dockview/src/api/component.api.ts b/packages/dockview/src/api/component.api.ts
index fb4caa1be..5b7e1d6b0 100644
--- a/packages/dockview/src/api/component.api.ts
+++ b/packages/dockview/src/api/component.api.ts
@@ -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);
diff --git a/packages/dockview/src/dnd/abstractDragHandler.ts b/packages/dockview/src/dnd/abstractDragHandler.ts
new file mode 100644
index 000000000..b6fc1d288
--- /dev/null
+++ b/packages/dockview/src/dnd/abstractDragHandler.ts
@@ -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();
+ 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,
+ // });
+ })
+ );
+ }
+}
diff --git a/packages/dockview/src/dnd/dataTransfer.ts b/packages/dockview/src/dnd/dataTransfer.ts
index 2558c2f2e..3100473bb 100644
--- a/packages/dockview/src/dnd/dataTransfer.ts
+++ b/packages/dockview/src/dnd/dataTransfer.ts
@@ -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 {
}
}
}
+
+export function getPanelData(): PanelTransfer | undefined {
+ const panelTransfer = LocalSelectionTransfer.getInstance();
+ 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();
+ const isPanelEvent = paneTransfer.hasData(PaneTransfer.prototype);
+
+ if (!isPanelEvent) {
+ return undefined;
+ }
+
+ return paneTransfer.getData(PaneTransfer.prototype)![0];
+}
diff --git a/packages/dockview/src/dnd/dnd.ts b/packages/dockview/src/dnd/dnd.ts
new file mode 100644
index 000000000..b5e0ee9bf
--- /dev/null
+++ b/packages/dockview/src/dnd/dnd.ts
@@ -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 {
+ constructor(private readonly data: T) {
+ //
+ }
+}
+
+export class DragAndDrop extends CompositeDisposable {
+ private _onDragStart = new Emitter();
+ private _onDragEnd = new Emitter();
+ private static _instance: DragAndDrop | undefined;
+ static get INSTANCE(): DragAndDrop {
+ if (!DragAndDrop._instance) {
+ DragAndDrop._instance = new DragAndDrop();
+ }
+ return DragAndDrop._instance;
+ }
+
+ private transferData =
+ LocalSelectionTransfer.getInstance();
+
+ 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;
+ }
+}
diff --git a/packages/dockview/src/dnd/droptarget.scss b/packages/dockview/src/dnd/droptarget.scss
index 4fc743a5f..12e4079fb 100644
--- a/packages/dockview/src/dnd/droptarget.scss
+++ b/packages/dockview/src/dnd/droptarget.scss
@@ -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);
+ }
}
}
}
diff --git a/packages/dockview/src/dnd/droptarget.ts b/packages/dockview/src/dnd/droptarget.ts
index 8362975cd..ab0821647 100644
--- a/packages/dockview/src/dnd/droptarget.ts
+++ b/packages/dockview/src/dnd/droptarget.ts
@@ -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();
- readonly onDidChange: Event = this._onDidChange.event;
+ private readonly _onDrop = new Emitter();
+ readonly onDrop: Event = 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');
diff --git a/packages/dockview/src/dockview/components/tab/defaultTab.scss b/packages/dockview/src/dockview/components/tab/defaultTab.scss
index 7d78a017a..79b70d23a 100644
--- a/packages/dockview/src/dockview/components/tab/defaultTab.scss
+++ b/packages/dockview/src/dockview/components/tab/defaultTab.scss
@@ -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 {
diff --git a/packages/dockview/src/dockview/dockviewComponent.ts b/packages/dockview/src/dockview/dockviewComponent.ts
index 98f34b5ad..a1ff5f4b4 100644
--- a/packages/dockview/src/dockview/dockviewComponent.ts
+++ b/packages/dockview/src/dockview/dockviewComponent.ts
@@ -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 {
onTabContextMenu: Event;
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 {
onDidLayoutChange: Event;
}
-export interface LayoutDropEvent {
- event: GroupDropEvent;
-}
-
export class DockviewComponent
extends BaseGrid
implements IDockviewComponent
@@ -153,13 +137,13 @@ export class DockviewComponent
readonly onTabContextMenu: Event =
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
- );
})
);
diff --git a/packages/dockview/src/gridview/gridview.ts b/packages/dockview/src/gridview/gridview.ts
index f4d7626f9..236650b11 100644
--- a/packages/dockview/src/gridview/gridview.ts
+++ b/packages/dockview/src/gridview/gridview.ts
@@ -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);
}
diff --git a/packages/dockview/src/groupview/groupview.ts b/packages/dockview/src/groupview/groupview.ts
index a6375028d..2b93af8c2 100644
--- a/packages/dockview/src/groupview/groupview.ts
+++ b/packages/dockview/src/groupview/groupview.ts
@@ -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;
//
- 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();
readonly onMove: Event = this._onMove.event;
- private readonly _onDrop = new Emitter();
- readonly onDrop: Event = this._onDrop.event;
-
private readonly _onDidGroupChange = new Emitter();
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
}
}
diff --git a/packages/dockview/src/groupview/tab.ts b/packages/dockview/src/groupview/tab.ts
index efe9734aa..4077287e1 100644
--- a/packages/dockview/src/groupview/tab.ts
+++ b/packages/dockview/src/groupview/tab.ts
@@ -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;
- onDropped: Event;
+ onDrop: Event;
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 = this._onChanged.event;
private readonly _onDropped = new Emitter();
- readonly onDropped: Event = this._onDropped.event;
+ readonly onDrop: Event = this._onDropped.event;
+
+ private readonly panelTransfer =
+ LocalSelectionTransfer.getInstance();
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);
})
);
diff --git a/packages/dockview/src/groupview/titlebar/tabsContainer.scss b/packages/dockview/src/groupview/titlebar/tabsContainer.scss
index e7ed61128..595af871c 100644
--- a/packages/dockview/src/groupview/titlebar/tabsContainer.scss
+++ b/packages/dockview/src/groupview/titlebar/tabsContainer.scss
@@ -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;
diff --git a/packages/dockview/src/groupview/titlebar/tabsContainer.ts b/packages/dockview/src/groupview/titlebar/tabsContainer.ts
index 8dfbb2fc4..a88561105 100644
--- a/packages/dockview/src/groupview/titlebar/tabsContainer.ts
+++ b/packages/dockview/src/groupview/titlebar/tabsContainer.ts
@@ -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;
+ onDrop: Event;
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[] = [];
private selectedIndex = -1;
private active = false;
@@ -53,11 +56,15 @@ export class TabsContainer
private _height: number | undefined;
- private readonly _onDropped = new Emitter();
- readonly onDropEvent: Event = this._onDropped.event;
+ private readonly _onDrop = new Emitter();
+ readonly onDrop: Event = 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),
});
})
diff --git a/packages/dockview/src/paneview/draggablePaneviewPanel.ts b/packages/dockview/src/paneview/draggablePaneviewPanel.ts
new file mode 100644
index 000000000..4aea7d119
--- /dev/null
+++ b/packages/dockview/src/paneview/draggablePaneviewPanel.ts
@@ -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;
+ readonly onDidRemove: Event;
+}
+
+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();
+ 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);
+ })
+ );
+ }
+}
diff --git a/packages/dockview/src/paneview/options.ts b/packages/dockview/src/paneview/options.ts
index 19782db2a..285cc58fa 100644
--- a/packages/dockview/src/paneview/options.ts
+++ b/packages/dockview/src/paneview/options.ts
@@ -22,4 +22,5 @@ export interface PaneviewComponentOptions {
header: FrameworkFactory;
body: FrameworkFactory;
};
+ disableDnd?: boolean;
}
diff --git a/packages/dockview/src/paneview/paneview.scss b/packages/dockview/src/paneview/paneview.scss
index 0037ec984..117835fae 100644
--- a/packages/dockview/src/paneview/paneview.scss
+++ b/packages/dockview/src/paneview/paneview.scss
@@ -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 {
diff --git a/packages/dockview/src/paneview/paneview.ts b/packages/dockview/src/paneview/paneview.ts
index 3fa3763af..9c569c015 100644
--- a/packages/dockview/src/paneview/paneview.ts
+++ b/packages/dockview/src/paneview/paneview.ts
@@ -24,6 +24,13 @@ export class Paneview extends CompositeDisposable implements IDisposable {
private readonly _onDidChange = new Emitter();
readonly onDidChange: Event = this._onDidChange.event;
+ get onDidAddView() {
+ return >this.splitview.onDidAddView;
+ }
+ get onDidRemoveView() {
+ return >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;
diff --git a/packages/dockview/src/paneview/paneviewComponent.ts b/packages/dockview/src/paneview/paneviewComponent.ts
index 9017953e2..53503b30a 100644
--- a/packages/dockview/src/paneview/paneviewComponent.ts
+++ b/packages/dockview/src/paneview/paneviewComponent.ts
@@ -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();
readonly onDidLayoutChange: Event = this._onDidLayoutChange.event;
+ private readonly _onDidDrop = new Emitter();
+ readonly onDidDrop: Event = 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(() => {
diff --git a/packages/dockview/src/paneview/paneviewPanel.ts b/packages/dockview/src/paneview/paneviewPanel.ts
index c94eb431f..555731dd2 100644
--- a/packages/dockview/src/paneview/paneviewPanel.ts
+++ b/packages/dockview/src/paneview/paneviewPanel.ts
@@ -61,7 +61,8 @@ export interface IPaneviewPanel
export abstract class PaneviewPanel
extends BasePanelView
- implements IPaneview, IPaneviewPanel {
+ implements IPaneview, IPaneviewPanel
+{
private _onDidChangeExpansionState: Emitter = new Emitter(
{ 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;
diff --git a/packages/dockview/src/react/dropTarget.tsx b/packages/dockview/src/react/dropTarget.tsx
new file mode 100644
index 000000000..4a95ff4f0
--- /dev/null
+++ b/packages/dockview/src/react/dropTarget.tsx
@@ -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) => {
+ const domRef = React.useRef(null);
+ const dropTargetRef = React.useRef();
+
+ 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 {props.children}
;
+ }
+);
+
+DockviewDropTarget.displayName = 'DockviewDropTarget';
diff --git a/packages/dockview/src/react/index.ts b/packages/dockview/src/react/index.ts
index d706a9222..7ae89882c 100644
--- a/packages/dockview/src/react/index.ts
+++ b/packages/dockview/src/react/index.ts
@@ -8,3 +8,4 @@ export * from '../gridview/gridviewPanel';
export * from './paneview/paneview';
export * from './types';
export * from './react';
+export * from './dropTarget';
diff --git a/packages/dockview/src/react/paneview/paneview.tsx b/packages/dockview/src/react/paneview/paneview.tsx
index 91dd63821..d499b5a27 100644
--- a/packages/dockview/src/react/paneview/paneview.tsx
+++ b/packages/dockview/src/react/paneview/paneview.tsx
@@ -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>
title: string;
}
+export interface PaneviewDropEvent {
+ api: PaneviewApi;
+ event: DroptargetEvent;
+}
+
export interface IPaneviewReactProps {
onReady?: (event: PaneviewReadyEvent) => void;
components?: PanelCollection;
headerComponents?: PanelCollection;
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();
};
}, []);
diff --git a/packages/dockview/src/splitview/core/splitview.ts b/packages/dockview/src/splitview/core/splitview.ts
index 907735499..6a139c1c5 100644
--- a/packages/dockview/src/splitview/core/splitview.ts
+++ b/packages/dockview/src/splitview/core/splitview.ts
@@ -106,8 +106,12 @@ export class Splitview {
private _proportions: number[] | undefined = undefined;
private proportionalLayout: boolean;
- private _onDidSashEnd = new Emitter();
- public onDidSashEnd = this._onDidSashEnd.event;
+ private readonly _onDidSashEnd = new Emitter();
+ readonly onDidSashEnd = this._onDidSashEnd.event;
+ private readonly _onDidAddView = new Emitter();
+ readonly onDidAddView = this._onDidAddView.event;
+ private readonly _onDidRemoveView = new Emitter();
+ 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) {
diff --git a/packages/dockview/src/theme.scss b/packages/dockview/src/theme.scss
index 78c59aabd..0b38ec72d 100644
--- a/packages/dockview/src/theme.scss
+++ b/packages/dockview/src/theme.scss
@@ -7,6 +7,7 @@
--dv-tab-close-icon: url('data:image/svg+xml;utf8,');
--dv-tab-dirty-icon: url('data:image/svg+xml;utf8,');
--dv-drag-over-background-color: rgba(83, 89, 93, 0.5);
+ --dv-drag-over-border-color: white;
--dv-tabs-container-scrollbar-color: #888;
}