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; }