diff --git a/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts b/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts index b597f2fa5..7ca528766 100644 --- a/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts +++ b/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts @@ -2,6 +2,7 @@ import { fireEvent } from '@testing-library/dom'; import { GroupDragHandler } from '../../dnd/groupDragHandler'; import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer'; +import { DockviewComponent } from '../../dockview/dockviewComponent'; describe('groupDragHandler', () => { test('that the dnd transfer object is setup and torndown', () => { @@ -16,7 +17,11 @@ describe('groupDragHandler', () => { }); const group = new groupMock(); - const cut = new GroupDragHandler(element, 'test_accessor_id', group); + const cut = new GroupDragHandler( + element, + { id: 'test_accessor_id' } as DockviewComponent, + group + ); fireEvent.dragStart(element, new Event('dragstart')); @@ -54,7 +59,11 @@ describe('groupDragHandler', () => { }); const group = new groupMock(); - const cut = new GroupDragHandler(element, 'accessor_id', group); + const cut = new GroupDragHandler( + element, + { id: 'accessor_id' } as DockviewComponent, + group + ); const event = new KeyboardEvent('dragstart', { shiftKey: false }); @@ -82,7 +91,11 @@ describe('groupDragHandler', () => { }); const group = new groupMock(); - const cut = new GroupDragHandler(element, 'accessor_id', group); + const cut = new GroupDragHandler( + element, + { id: 'accessor_id' } as DockviewComponent, + group + ); const event = new KeyboardEvent('dragstart', { shiftKey: false }); diff --git a/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts index 2aa671d00..bd45df9f1 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts @@ -1,16 +1,24 @@ import { fireEvent } from '@testing-library/dom'; -import { LocalSelectionTransfer, PanelTransfer } from '../../../dnd/dataTransfer'; +import { + LocalSelectionTransfer, + PanelTransfer, +} from '../../../dnd/dataTransfer'; import { DockviewComponent } from '../../../dockview/dockviewComponent'; import { DockviewGroupPanel } from '../../../dockview/dockviewGroupPanel'; import { DockviewGroupPanelModel } from '../../../dockview/dockviewGroupPanelModel'; import { Tab } from '../../../dockview/components/tab/tab'; +import { IDockviewPanel } from '../../../dockview/dockviewPanel'; describe('tab', () => { test('that empty tab has inactive-tab class', () => { const accessorMock = jest.fn(); const groupMock = jest.fn(); - const cut = new Tab('panelId', new accessorMock(), new groupMock()); + const cut = new Tab( + { id: 'panelId' } as IDockviewPanel, + new accessorMock(), + new groupMock() + ); expect(cut.element.className).toBe('tab inactive-tab'); }); @@ -19,7 +27,11 @@ describe('tab', () => { const accessorMock = jest.fn(); const groupMock = jest.fn(); - const cut = new Tab('panelId', new accessorMock(), new groupMock()); + const cut = new Tab( + { id: 'panelId' } as IDockviewPanel, + new accessorMock(), + new groupMock() + ); cut.setActive(true); expect(cut.element.className).toBe('tab active-tab'); @@ -54,7 +66,11 @@ describe('tab', () => { const accessor = new accessorMock() as DockviewComponent; const groupPanel = new groupPanelMock() as DockviewGroupPanel; - const cut = new Tab('panelId', accessor, groupPanel); + const cut = new Tab( + { id: 'panelId' } as IDockviewPanel, + accessor, + groupPanel + ); jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( () => 100 @@ -99,7 +115,11 @@ describe('tab', () => { const accessor = new accessorMock() as DockviewComponent; const groupPanel = new groupPanelMock() as DockviewGroupPanel; - const cut = new Tab('panel1', accessor, groupPanel); + const cut = new Tab( + { id: 'panel1' } as IDockviewPanel, + accessor, + groupPanel + ); jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( () => 100 @@ -149,7 +169,11 @@ describe('tab', () => { const accessor = new accessorMock() as DockviewComponent; const groupPanel = new groupPanelMock() as DockviewGroupPanel; - const cut = new Tab('panel1', accessor, groupPanel); + const cut = new Tab( + { id: 'panel1' } as IDockviewPanel, + accessor, + groupPanel + ); jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( () => 100 @@ -199,7 +223,11 @@ describe('tab', () => { const accessor = new accessorMock() as DockviewComponent; const groupPanel = new groupPanelMock() as DockviewGroupPanel; - const cut = new Tab('panel1', accessor, groupPanel); + const cut = new Tab( + { id: 'panel1' } as IDockviewPanel, + accessor, + groupPanel + ); jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( () => 100 @@ -255,7 +283,11 @@ describe('tab', () => { const accessor = new accessorMock() as DockviewComponent; const groupPanel = new groupPanelMock() as DockviewGroupPanel; - const cut = new Tab('panel1', accessor, groupPanel); + const cut = new Tab( + { id: 'panel1' } as IDockviewPanel, + accessor, + groupPanel + ); jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( () => 100 diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 65ead5491..1c99baabc 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -13,6 +13,10 @@ import { IDockviewPanel } from '../../dockview/dockviewPanel'; import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; import { fireEvent } from '@testing-library/dom'; import { getPanelData } from '../../dnd/dataTransfer'; +import { + GroupDragEvent, + TabDragEvent, +} from '../../dockview/components/titlebar/tabsContainer'; class PanelContentPartTest implements IContentRenderer { element: HTMLElement = document.createElement('div'); @@ -3979,4 +3983,84 @@ describe('dockviewComponent', () => { }); expect(showDndOverlay).toBeCalledTimes(5); }); + + test('that dragging a tab triggers onWillDragPanel', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const tabDragEvents: TabDragEvent[] = []; + const groupDragEvents: GroupDragEvent[] = []; + + dockview.onWillDragPanel((event) => { + tabDragEvents.push(event); + }); + dockview.onWillDragGroup((event) => { + groupDragEvents.push(event); + }); + + const el = dockview.element.querySelector('.tab')!; + expect(el).toBeTruthy(); + + fireEvent.dragStart(el); + + expect(tabDragEvents.length).toBe(1); + expect(groupDragEvents.length).toBe(0); + }); + + test('that dragging a group triggers onWillDragGroup', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const tabDragEvents: TabDragEvent[] = []; + const groupDragEvents: GroupDragEvent[] = []; + + dockview.onWillDragPanel((event) => { + tabDragEvents.push(event); + }); + dockview.onWillDragGroup((event) => { + groupDragEvents.push(event); + }); + + const el = dockview.element.querySelector('.void-container')!; + expect(el).toBeTruthy(); + + fireEvent.dragStart(el); + + expect(tabDragEvents.length).toBe(0); + expect(groupDragEvents.length).toBe(1); + }); }); diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index 168ad0b9a..6229625ad 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -38,6 +38,10 @@ import { import { Emitter, Event } from '../events'; import { IDockviewPanel } from '../dockview/dockviewPanel'; import { PaneviewDropEvent } from '../paneview/draggablePaneviewPanel'; +import { + GroupDragEvent, + TabDragEvent, +} from '../dockview/components/titlebar/tabsContainer'; export interface CommonApi { readonly height: number; @@ -118,7 +122,9 @@ export class SplitviewApi implements CommonApi { return this.component.layout(width, height); } - addPanel(options: AddSplitviewComponentOptions): ISplitviewPanel { + addPanel( + options: AddSplitviewComponentOptions + ): ISplitviewPanel { return this.component.addPanel(options); } @@ -213,7 +219,9 @@ export class PaneviewApi implements CommonApi { this.component.layout(width, height); } - addPanel(options: AddPaneviewComponentOptions): IPaneviewPanel { + addPanel( + options: AddPaneviewComponentOptions + ): IPaneviewPanel { return this.component.addPanel(options); } @@ -297,7 +305,9 @@ export class GridviewApi implements CommonApi { this.component.layout(width, height, force); } - addPanel(options: AddComponentOptions): IGridviewPanel { + addPanel( + options: AddComponentOptions + ): IGridviewPanel { return this.component.addPanel(options); } @@ -402,6 +412,14 @@ export class DockviewApi implements CommonApi { return this.component.onDidDrop; } + get onWillDragGroup(): Event { + return this.component.onWillDragGroup; + } + + get onWillDragPanel(): Event { + return this.component.onWillDragPanel; + } + get panels(): IDockviewPanel[] { return this.component.panels; } @@ -432,7 +450,9 @@ export class DockviewApi implements CommonApi { this.component.layout(width, height, force); } - addPanel(options: AddPanelOptions): IDockviewPanel { + addPanel( + options: AddPanelOptions + ): IDockviewPanel { return this.component.addPanel(options); } diff --git a/packages/dockview-core/src/dnd/abstractDragHandler.ts b/packages/dockview-core/src/dnd/abstractDragHandler.ts index 0e452dcf3..306ab236e 100644 --- a/packages/dockview-core/src/dnd/abstractDragHandler.ts +++ b/packages/dockview-core/src/dnd/abstractDragHandler.ts @@ -10,7 +10,7 @@ export abstract class DragHandler extends CompositeDisposable { private readonly dataDisposable = new MutableDisposable(); private readonly pointerEventsDisposable = new MutableDisposable(); - private readonly _onDragStart = new Emitter(); + private readonly _onDragStart = new Emitter(); readonly onDragStart = this._onDragStart.event; constructor(protected readonly el: HTMLElement) { @@ -25,7 +25,7 @@ export abstract class DragHandler extends CompositeDisposable { this.configure(); } - abstract getData(dataTransfer?: DataTransfer | null): IDisposable; + abstract getData(event: DragEvent): IDisposable; protected isCancelled(_event: DragEvent): boolean { return false; @@ -35,7 +35,7 @@ export abstract class DragHandler extends CompositeDisposable { this.addDisposables( this._onDragStart, addDisposableListener(this.el, 'dragstart', (event) => { - if (this.isCancelled(event)) { + if (event.defaultPrevented || this.isCancelled(event)) { event.preventDefault(); return; } @@ -60,24 +60,29 @@ export abstract class DragHandler extends CompositeDisposable { this.el.classList.add('dv-dragged'); setTimeout(() => this.el.classList.remove('dv-dragged'), 0); - this.dataDisposable.value = this.getData(event.dataTransfer); + this.dataDisposable.value = this.getData(event); + this._onDragStart.fire(event); if (event.dataTransfer) { event.dataTransfer.effectAllowed = 'move'; - /** - * Although this is not used by dockview many third party dnd libraries will check - * dataTransfer.types to determine valid drag events. - * - * For example: in react-dnd if dataTransfer.types is not set then the dragStart event will be cancelled - * through .preventDefault(). Since this is applied globally to all drag events this would break dockviews - * dnd logic. You can see the code at - * https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542 - */ - event.dataTransfer.setData( - 'text/plain', - '__dockview_internal_drag_event__' - ); + const hasData = event.dataTransfer.items.length > 0; + + if (!hasData) { + /** + * Although this is not used by dockview many third party dnd libraries will check + * dataTransfer.types to determine valid drag events. + * + * For example: in react-dnd if dataTransfer.types is not set then the dragStart event will be cancelled + * through .preventDefault(). Since this is applied globally to all drag events this would break dockviews + * dnd logic. You can see the code at + * https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542 + */ + event.dataTransfer.setData( + 'text/plain', + '__dockview_internal_drag_event__' + ); + } } }), addDisposableListener(this.el, 'dragend', () => { diff --git a/packages/dockview-core/src/dnd/groupDragHandler.ts b/packages/dockview-core/src/dnd/groupDragHandler.ts index 0a6f55008..fd6d42f77 100644 --- a/packages/dockview-core/src/dnd/groupDragHandler.ts +++ b/packages/dockview-core/src/dnd/groupDragHandler.ts @@ -1,3 +1,4 @@ +import { DockviewComponent } from '../dockview/dockviewComponent'; import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel'; import { quasiPreventDefault } from '../dom'; import { addDisposableListener } from '../events'; @@ -12,7 +13,7 @@ export class GroupDragHandler extends DragHandler { constructor( element: HTMLElement, - private readonly accessorId: string, + private readonly accessor: DockviewComponent, private readonly group: DockviewGroupPanel ) { super(element); @@ -43,9 +44,11 @@ export class GroupDragHandler extends DragHandler { return false; } - getData(dataTransfer: DataTransfer | null): IDisposable { + getData(dragEvent: DragEvent): IDisposable { + const dataTransfer = dragEvent.dataTransfer; + this.panelTransfer.setData( - [new PanelTransfer(this.accessorId, this.group.id, null)], + [new PanelTransfer(this.accessor.id, this.group.id, null)], PanelTransfer.prototype ); diff --git a/packages/dockview-core/src/dockview/components/tab/tab.ts b/packages/dockview-core/src/dockview/components/tab/tab.ts index b8d86cabc..48da83a85 100644 --- a/packages/dockview-core/src/dockview/components/tab/tab.ts +++ b/packages/dockview-core/src/dockview/components/tab/tab.ts @@ -11,9 +11,37 @@ import { DockviewDropTargets, ITabRenderer } from '../../types'; import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { DroptargetEvent, Droptarget } from '../../../dnd/droptarget'; import { DragHandler } from '../../../dnd/abstractDragHandler'; +import { IDockviewPanel } from '../../dockviewPanel'; + +class TabDragHandler extends DragHandler { + private readonly panelTransfer = + LocalSelectionTransfer.getInstance(); + + constructor( + element: HTMLElement, + private readonly accessor: DockviewComponent, + private readonly group: DockviewGroupPanel, + private readonly panel: IDockviewPanel + ) { + super(element); + } + + getData(event: DragEvent): IDisposable { + this.panelTransfer.setData( + [new PanelTransfer(this.accessor.id, this.group.id, this.panel.id)], + PanelTransfer.prototype + ); + + return { + dispose: () => { + this.panelTransfer.clearData(PanelTransfer.prototype); + }, + }; + } +} export interface ITab extends IDisposable { - readonly panelId: string; + readonly panel: IDockviewPanel; readonly element: HTMLElement; setContent: (element: ITabRenderer) => void; onChanged: Event; @@ -24,7 +52,7 @@ export interface ITab extends IDisposable { export class Tab extends CompositeDisposable implements ITab { private readonly _element: HTMLElement; private readonly droptarget: Droptarget; - private content?: ITabRenderer; + private content: ITabRenderer | undefined = undefined; private readonly _onChanged = new Emitter(); readonly onChanged: Event = this._onChanged.event; @@ -32,12 +60,15 @@ export class Tab extends CompositeDisposable implements ITab { private readonly _onDropped = new Emitter(); readonly onDrop: Event = this._onDropped.event; + private readonly _onDragStart = new Emitter(); + readonly onDragStart = this._onDragStart.event; + public get element(): HTMLElement { return this._element; } constructor( - public readonly panelId: string, + public readonly panel: IDockviewPanel, private readonly accessor: DockviewComponent, private readonly group: DockviewGroupPanel ) { @@ -50,38 +81,11 @@ export class Tab extends CompositeDisposable implements ITab { toggleClass(this.element, 'inactive-tab', true); - this.addDisposables( - this._onChanged, - this._onDropped, - new (class Handler extends DragHandler { - private readonly panelTransfer = - LocalSelectionTransfer.getInstance(); - - getData(): IDisposable { - this.panelTransfer.setData( - [new PanelTransfer(accessor.id, group.id, panelId)], - PanelTransfer.prototype - ); - - return { - dispose: () => { - this.panelTransfer.clearData( - PanelTransfer.prototype - ); - }, - }; - } - })(this._element) - ); - - this.addDisposables( - addDisposableListener(this._element, 'mousedown', (event) => { - if (event.defaultPrevented) { - return; - } - - this._onChanged.fire(event); - }) + const dragHandler = new TabDragHandler( + this._element, + this.accessor, + this.group, + this.panel ); this.droptarget = new Droptarget(this._element, { @@ -102,7 +106,7 @@ export class Tab extends CompositeDisposable implements ITab { return false; } - return this.panelId !== data.panelId; + return this.panel.id !== data.panelId; } return this.group.model.canDisplayOverlay( @@ -114,6 +118,20 @@ export class Tab extends CompositeDisposable implements ITab { }); this.addDisposables( + this._onChanged, + this._onDropped, + this._onDragStart, + dragHandler.onDragStart((event) => { + this._onDragStart.fire(event); + }), + dragHandler, + addDisposableListener(this._element, 'mousedown', (event) => { + if (event.defaultPrevented) { + return; + } + + this._onChanged.fire(event); + }), this.droptarget.onDrop((event) => { this._onDropped.fire(event); }), diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index 3801ea314..4a3931cf2 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -16,13 +16,26 @@ export interface TabDropIndexEvent { readonly index: number; } +export interface TabDragEvent { + readonly nativeEvent: DragEvent; + readonly panel: IDockviewPanel; +} + +export interface GroupDragEvent { + readonly nativeEvent: DragEvent; + readonly group: DockviewGroupPanel; +} + export interface ITabsContainer extends IDisposable { readonly element: HTMLElement; readonly panels: string[]; readonly size: number; + hidden: boolean; delete: (id: string) => void; indexOf: (id: string) => number; onDrop: Event; + onTabDragStart: Event; + onGroupDragStart: Event; setActive: (isGroupActive: boolean) => void; setActivePanel: (panel: IDockviewPanel) => void; isActive: (tab: ITab) => boolean; @@ -30,7 +43,6 @@ export interface ITabsContainer extends IDisposable { openPanel: (panel: IDockviewPanel, index?: number) => void; setRightActionsElement(element: HTMLElement | undefined): void; setLeftActionsElement(element: HTMLElement | undefined): void; - hidden: boolean; show(): void; hide(): void; } @@ -55,8 +67,15 @@ export class TabsContainer private readonly _onDrop = new Emitter(); readonly onDrop: Event = this._onDrop.event; + private readonly _onTabDragStart = new Emitter(); + readonly onTabDragStart: Event = this._onTabDragStart.event; + + private readonly _onGroupDragStart = new Emitter(); + readonly onGroupDragStart: Event = + this._onGroupDragStart.event; + get panels(): string[] { - return this.tabs.map((_) => _.value.panelId); + return this.tabs.map((_) => _.value.panel.id); } get size(): number { @@ -122,7 +141,7 @@ export class TabsContainer } public indexOf(id: string): number { - return this.tabs.findIndex((tab) => tab.value.panelId === id); + return this.tabs.findIndex((tab) => tab.value.panel.id === id); } constructor( @@ -131,7 +150,11 @@ export class TabsContainer ) { super(); - this.addDisposables(this._onDrop); + this.addDisposables( + this._onDrop, + this._onTabDragStart, + this._onGroupDragStart + ); this._element = document.createElement('div'); this._element.className = 'tabs-and-actions-container'; @@ -181,6 +204,12 @@ export class TabsContainer this.addDisposables( this.voidContainer, + this.voidContainer.onDragStart((event) => { + this._onGroupDragStart.fire({ + nativeEvent: event, + group: this.group, + }); + }), this.voidContainer.onDrop((event) => { this._onDrop.fire({ event: event.nativeEvent, @@ -260,7 +289,7 @@ export class TabsContainer } public delete(id: string): void { - const index = this.tabs.findIndex((tab) => tab.value.panelId === id); + const index = this.tabs.findIndex((tab) => tab.value.panel.id === id); const tabToRemove = this.tabs.splice(index, 1)[0]; @@ -273,7 +302,7 @@ export class TabsContainer public setActivePanel(panel: IDockviewPanel): void { this.tabs.forEach((tab) => { - const isActivePanel = panel.id === tab.value.panelId; + const isActivePanel = panel.id === tab.value.panel.id; tab.value.setActive(isActivePanel); }); } @@ -282,17 +311,20 @@ export class TabsContainer panel: IDockviewPanel, index: number = this.tabs.length ): void { - if (this.tabs.find((tab) => tab.value.panelId === panel.id)) { + if (this.tabs.find((tab) => tab.value.panel.id === panel.id)) { return; } - const tabToAdd = new Tab(panel.id, this.accessor, this.group); + const tab = new Tab(panel, this.accessor, this.group); if (!panel.view?.tab) { throw new Error('invalid header component'); } - tabToAdd.setContent(panel.view.tab); + tab.setContent(panel.view.tab); - const disposable = CompositeDisposable.from( - tabToAdd.onChanged((event) => { + const disposable = new CompositeDisposable( + tab.onDragStart((event) => { + this._onTabDragStart.fire({ nativeEvent: event, panel }); + }), + tab.onChanged((event) => { const isFloatingGroupsEnabled = !this.accessor.options.disableFloatingGroups; @@ -306,10 +338,9 @@ export class TabsContainer ) { event.preventDefault(); - const panel = this.accessor.getGroupPanel(tabToAdd.panelId); + const panel = this.accessor.getGroupPanel(tab.panel.id); - const { top, left } = - tabToAdd.element.getBoundingClientRect(); + const { top, left } = tab.element.getBoundingClientRect(); const { top: rootTop, left: rootLeft } = this.accessor.element.getBoundingClientRect(); @@ -338,15 +369,15 @@ export class TabsContainer skipFocus: alreadyFocused, }); }), - tabToAdd.onDrop((event) => { + tab.onDrop((event) => { this._onDrop.fire({ event: event.nativeEvent, - index: this.tabs.findIndex((x) => x.value === tabToAdd), + index: this.tabs.findIndex((x) => x.value === tab), }); }) ); - const value: IValueDisposable = { value: tabToAdd, disposable }; + const value: IValueDisposable = { value: tab, disposable }; this.addTab(value, index); } diff --git a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts index 1737c87aa..e1d3cd9c7 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts @@ -15,6 +15,9 @@ export class VoidContainer extends CompositeDisposable { private readonly _onDrop = new Emitter(); readonly onDrop: Event = this._onDrop.event; + private readonly _onDragStart = new Emitter(); + readonly onDragStart = this._onDragStart.event; + get element(): HTMLElement { return this._element; } @@ -33,12 +36,13 @@ export class VoidContainer extends CompositeDisposable { this.addDisposables( this._onDrop, + this._onDragStart, addDisposableListener(this._element, 'click', () => { this.accessor.doSetGroupActive(this.group); }) ); - const handler = new GroupDragHandler(this._element, accessor.id, group); + const handler = new GroupDragHandler(this._element, accessor, group); this.voidDropTarget = new Droptarget(this._element, { acceptedTargetZones: ['center'], @@ -68,6 +72,9 @@ export class VoidContainer extends CompositeDisposable { this.addDisposables( handler, + handler.onDragStart((event) => { + this._onDragStart.fire(event); + }), this.voidDropTarget.onDrop((event) => { this._onDrop.fire(event); }), diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 13486ebd9..e659f6312 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -51,6 +51,10 @@ import { DockviewFloatingGroupPanel, IDockviewFloatingGroupPanel, } from './dockviewFloatingGroupPanel'; +import { + GroupDragEvent, + TabDragEvent, +} from './components/titlebar/tabsContainer'; const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100; @@ -136,6 +140,8 @@ export interface IDockviewComponent extends IBaseGrid { readonly onDidAddPanel: Event; readonly onDidLayoutFromJSON: Event; readonly onDidActivePanelChange: Event; + readonly onWillDragPanel: Event; + readonly onWillDragGroup: Event; addFloatingGroup( item: IDockviewPanel | DockviewGroupPanel, coord?: { x: number; y: number } @@ -152,6 +158,13 @@ export class DockviewComponent private _options: Exclude; private watermark: IWatermarkRenderer | null = null; + private readonly _onWillDragPanel = new Emitter(); + readonly onWillDragPanel: Event = this._onWillDragPanel.event; + + private readonly _onWillDragGroup = new Emitter(); + readonly onWillDragGroup: Event = + this._onWillDragGroup.event; + private readonly _onDidDrop = new Emitter(); readonly onDidDrop: Event = this._onDidDrop.event; @@ -210,6 +223,12 @@ export class DockviewComponent toggleClass(this.gridview.element, 'dv-dockview', true); this.addDisposables( + this._onWillDragPanel, + this._onWillDragGroup, + this._onDidActivePanelChange, + this._onDidAddPanel, + this._onDidRemovePanel, + this._onDidLayoutFromJSON, this._onDidDrop, Event.any( this.onDidAddGroup, @@ -1211,6 +1230,12 @@ export class DockviewComponent if (!this._groups.has(view.id)) { const disposable = new CompositeDisposable( + view.model.onTabDragStart((event) => { + this._onWillDragPanel.fire(event); + }), + view.model.onGroupDragStart((event) => { + this._onWillDragGroup.fire(event); + }), view.model.onMove((event) => { const { groupId, itemId, target, index } = event; this.moveGroupOrPanel(view, groupId, itemId, target, index); @@ -1286,13 +1311,4 @@ export class DockviewComponent group.value.model.containsPanel(panel) )?.value; } - - public dispose(): void { - this._onDidActivePanelChange.dispose(); - this._onDidAddPanel.dispose(); - this._onDidRemovePanel.dispose(); - this._onDidLayoutFromJSON.dispose(); - - super.dispose(); - } } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 5bf54d466..d05aa7308 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -12,7 +12,9 @@ import { IContentContainer, } from './components/panel/content'; import { + GroupDragEvent, ITabsContainer, + TabDragEvent, TabsContainer, } from './components/titlebar/tabsContainer'; import { DockviewDropTargets, IWatermarkRenderer } from './types'; @@ -160,6 +162,13 @@ export class DockviewGroupPanelModel private readonly _onDidDrop = new Emitter(); readonly onDidDrop: Event = this._onDidDrop.event; + private readonly _onTabDragStart = new Emitter(); + readonly onTabDragStart: Event = this._onTabDragStart.event; + + private readonly _onGroupDragStart = new Emitter(); + readonly onGroupDragStart: Event = + this._onGroupDragStart.event; + private readonly _onDidAddPanel = new Emitter(); readonly onDidAddPanel: Event = this._onDidAddPanel.event; @@ -315,6 +324,14 @@ export class DockviewGroupPanelModel this.locked = options.locked || false; this.addDisposables( + this._onTabDragStart, + this._onGroupDragStart, + this.tabsContainer.onTabDragStart((event) => { + this._onTabDragStart.fire(event); + }), + this.tabsContainer.onGroupDragStart((event) => { + this._onGroupDragStart.fire(event); + }), this.tabsContainer.onDrop((event) => { this.handleDropEvent(event.event, 'center', event.index); }), diff --git a/packages/dockview-core/src/lifecycle.ts b/packages/dockview-core/src/lifecycle.ts index 8262b28d1..c2a307adc 100644 --- a/packages/dockview-core/src/lifecycle.ts +++ b/packages/dockview-core/src/lifecycle.ts @@ -23,10 +23,6 @@ export class CompositeDisposable { return this._isDisposed; } - public static from(...args: IDisposable[]): CompositeDisposable { - return new CompositeDisposable(...args); - } - constructor(...args: IDisposable[]) { this._disposables = args; } diff --git a/packages/docs/docs/components/dockview.mdx b/packages/docs/docs/components/dockview.mdx index 923009036..25f67edc5 100644 --- a/packages/docs/docs/components/dockview.mdx +++ b/packages/docs/docs/components/dockview.mdx @@ -365,6 +365,10 @@ return ( ); ``` +### Intercepting Drag Events + +You can intercept drag events to attach your own metadata using the `onWillDragPanel` and `onWillDragGroup` api methods. + ### Third Party Dnd Libraries diff --git a/packages/docs/sandboxes/dnd-dockview/src/app.tsx b/packages/docs/sandboxes/dnd-dockview/src/app.tsx index 7a4f97f1a..b930cd968 100644 --- a/packages/docs/sandboxes/dnd-dockview/src/app.tsx +++ b/packages/docs/sandboxes/dnd-dockview/src/app.tsx @@ -1,4 +1,5 @@ import { + DockviewApi, DockviewDndOverlayEvent, DockviewDropEvent, DockviewReact, @@ -37,13 +38,19 @@ const DraggableElement = () => ( }} draggable={true} > - Drag me + Drag me into the dock ); const DndDockview = (props: { renderVisibleOnly: boolean; theme?: string }) => { - const onReady = (event: DockviewReadyEvent) => { - event.api.addPanel({ + const [api, setApi] = React.useState(); + + React.useEffect(() => { + if (!api) { + return; + } + + api.addPanel({ id: 'panel_1', component: 'default', params: { @@ -51,7 +58,7 @@ const DndDockview = (props: { renderVisibleOnly: boolean; theme?: string }) => { }, }); - event.api.addPanel({ + api.addPanel({ id: 'panel_2', component: 'default', params: { @@ -59,7 +66,7 @@ const DndDockview = (props: { renderVisibleOnly: boolean; theme?: string }) => { }, }); - event.api.addPanel({ + api.addPanel({ id: 'panel_3', component: 'default', params: { @@ -67,7 +74,7 @@ const DndDockview = (props: { renderVisibleOnly: boolean; theme?: string }) => { }, }); - event.api.addPanel({ + api.addPanel({ id: 'panel_4', component: 'default', params: { @@ -75,6 +82,45 @@ const DndDockview = (props: { renderVisibleOnly: boolean; theme?: string }) => { }, position: { referencePanel: 'panel_1', direction: 'right' }, }); + + const panelDragDisposable = api.onWillDragPanel((event) => { + const dataTransfer = event.nativeEvent.dataTransfer; + + if (dataTransfer) { + dataTransfer.setData( + 'text/plain', + 'Some custom panel data transfer data' + ); + dataTransfer.setData( + 'text/json', + '{text: "Some custom panel data transfer data"}' + ); + } + }); + + const groupDragDisposable = api.onWillDragGroup((event) => { + const dataTransfer = event.nativeEvent.dataTransfer; + + if (dataTransfer) { + dataTransfer.setData( + 'text/plain', + 'Some custom group data transfer data' + ); + dataTransfer.setData( + 'text/json', + '{text: "Some custom group data transfer data"}' + ); + } + }); + + return () => { + panelDragDisposable.dispose(); + groupDragDisposable.dispose(); + }; + }, [api]); + + const onReady = (event: DockviewReadyEvent) => { + setApi(event.api); }; const onDidDrop = (event: DockviewDropEvent) => { @@ -92,6 +138,20 @@ const DndDockview = (props: { renderVisibleOnly: boolean; theme?: string }) => { return true; }; + const onDrop = (event: React.DragEvent) => { + const dataTransfer = event.dataTransfer; + + let text = 'The following dataTransfer data was found:\n'; + + for (let i = 0; i < dataTransfer.items.length; i++) { + const item = dataTransfer.items[i]; + const value = dataTransfer.getData(item.type); + text += `type=${item.type},data=${value}\n`; + } + + alert(text); + }; + return (
{ >
+
+ Drop a tab or group here to inspect the attached metadata +