Merge pull request #322 from mathuo/299-access-to-datatransfer-object-on-drag-events

feat: expose dragstart event on custom handlers
This commit is contained in:
mathuo 2023-09-04 19:43:36 +01:00 committed by GitHub
commit 0b39e84f86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 425 additions and 108 deletions

View File

@ -2,6 +2,7 @@ import { fireEvent } from '@testing-library/dom';
import { GroupDragHandler } from '../../dnd/groupDragHandler'; import { GroupDragHandler } from '../../dnd/groupDragHandler';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer'; import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer';
import { DockviewComponent } from '../../dockview/dockviewComponent';
describe('groupDragHandler', () => { describe('groupDragHandler', () => {
test('that the dnd transfer object is setup and torndown', () => { test('that the dnd transfer object is setup and torndown', () => {
@ -16,7 +17,11 @@ describe('groupDragHandler', () => {
}); });
const group = new groupMock(); 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')); fireEvent.dragStart(element, new Event('dragstart'));
@ -54,7 +59,11 @@ describe('groupDragHandler', () => {
}); });
const group = new groupMock(); 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 }); const event = new KeyboardEvent('dragstart', { shiftKey: false });
@ -82,7 +91,11 @@ describe('groupDragHandler', () => {
}); });
const group = new groupMock(); 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 }); const event = new KeyboardEvent('dragstart', { shiftKey: false });

View File

@ -1,16 +1,24 @@
import { fireEvent } from '@testing-library/dom'; import { fireEvent } from '@testing-library/dom';
import { LocalSelectionTransfer, PanelTransfer } from '../../../dnd/dataTransfer'; import {
LocalSelectionTransfer,
PanelTransfer,
} from '../../../dnd/dataTransfer';
import { DockviewComponent } from '../../../dockview/dockviewComponent'; import { DockviewComponent } from '../../../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../../../dockview/dockviewGroupPanel'; import { DockviewGroupPanel } from '../../../dockview/dockviewGroupPanel';
import { DockviewGroupPanelModel } from '../../../dockview/dockviewGroupPanelModel'; import { DockviewGroupPanelModel } from '../../../dockview/dockviewGroupPanelModel';
import { Tab } from '../../../dockview/components/tab/tab'; import { Tab } from '../../../dockview/components/tab/tab';
import { IDockviewPanel } from '../../../dockview/dockviewPanel';
describe('tab', () => { describe('tab', () => {
test('that empty tab has inactive-tab class', () => { test('that empty tab has inactive-tab class', () => {
const accessorMock = jest.fn(); const accessorMock = jest.fn();
const groupMock = 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'); expect(cut.element.className).toBe('tab inactive-tab');
}); });
@ -19,7 +27,11 @@ describe('tab', () => {
const accessorMock = jest.fn(); const accessorMock = jest.fn();
const groupMock = 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); cut.setActive(true);
expect(cut.element.className).toBe('tab active-tab'); expect(cut.element.className).toBe('tab active-tab');
@ -54,7 +66,11 @@ describe('tab', () => {
const accessor = new accessorMock() as DockviewComponent; const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel; 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( jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
@ -99,7 +115,11 @@ describe('tab', () => {
const accessor = new accessorMock() as DockviewComponent; const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel; 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( jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
@ -149,7 +169,11 @@ describe('tab', () => {
const accessor = new accessorMock() as DockviewComponent; const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel; 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( jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
@ -199,7 +223,11 @@ describe('tab', () => {
const accessor = new accessorMock() as DockviewComponent; const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel; 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( jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100
@ -255,7 +283,11 @@ describe('tab', () => {
const accessor = new accessorMock() as DockviewComponent; const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel; 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( jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation(
() => 100 () => 100

View File

@ -13,6 +13,10 @@ import { IDockviewPanel } from '../../dockview/dockviewPanel';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { fireEvent } from '@testing-library/dom'; import { fireEvent } from '@testing-library/dom';
import { getPanelData } from '../../dnd/dataTransfer'; import { getPanelData } from '../../dnd/dataTransfer';
import {
GroupDragEvent,
TabDragEvent,
} from '../../dockview/components/titlebar/tabsContainer';
class PanelContentPartTest implements IContentRenderer { class PanelContentPartTest implements IContentRenderer {
element: HTMLElement = document.createElement('div'); element: HTMLElement = document.createElement('div');
@ -3979,4 +3983,84 @@ describe('dockviewComponent', () => {
}); });
expect(showDndOverlay).toBeCalledTimes(5); 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);
});
}); });

View File

@ -38,6 +38,10 @@ import {
import { Emitter, Event } from '../events'; import { Emitter, Event } from '../events';
import { IDockviewPanel } from '../dockview/dockviewPanel'; import { IDockviewPanel } from '../dockview/dockviewPanel';
import { PaneviewDropEvent } from '../paneview/draggablePaneviewPanel'; import { PaneviewDropEvent } from '../paneview/draggablePaneviewPanel';
import {
GroupDragEvent,
TabDragEvent,
} from '../dockview/components/titlebar/tabsContainer';
export interface CommonApi<T = any> { export interface CommonApi<T = any> {
readonly height: number; readonly height: number;
@ -118,7 +122,9 @@ export class SplitviewApi implements CommonApi<SerializedSplitview> {
return this.component.layout(width, height); return this.component.layout(width, height);
} }
addPanel<T extends object = Parameters>(options: AddSplitviewComponentOptions<T>): ISplitviewPanel { addPanel<T extends object = Parameters>(
options: AddSplitviewComponentOptions<T>
): ISplitviewPanel {
return this.component.addPanel(options); return this.component.addPanel(options);
} }
@ -213,7 +219,9 @@ export class PaneviewApi implements CommonApi<SerializedPaneview> {
this.component.layout(width, height); this.component.layout(width, height);
} }
addPanel<T extends object = Parameters>(options: AddPaneviewComponentOptions<T>): IPaneviewPanel { addPanel<T extends object = Parameters>(
options: AddPaneviewComponentOptions<T>
): IPaneviewPanel {
return this.component.addPanel(options); return this.component.addPanel(options);
} }
@ -297,7 +305,9 @@ export class GridviewApi implements CommonApi<SerializedGridviewComponent> {
this.component.layout(width, height, force); this.component.layout(width, height, force);
} }
addPanel<T extends object = Parameters>(options: AddComponentOptions<T>): IGridviewPanel { addPanel<T extends object = Parameters>(
options: AddComponentOptions<T>
): IGridviewPanel {
return this.component.addPanel(options); return this.component.addPanel(options);
} }
@ -402,6 +412,14 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.onDidDrop; return this.component.onDidDrop;
} }
get onWillDragGroup(): Event<GroupDragEvent> {
return this.component.onWillDragGroup;
}
get onWillDragPanel(): Event<TabDragEvent> {
return this.component.onWillDragPanel;
}
get panels(): IDockviewPanel[] { get panels(): IDockviewPanel[] {
return this.component.panels; return this.component.panels;
} }
@ -432,7 +450,9 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
this.component.layout(width, height, force); this.component.layout(width, height, force);
} }
addPanel<T extends object = Parameters>(options: AddPanelOptions<T>): IDockviewPanel { addPanel<T extends object = Parameters>(
options: AddPanelOptions<T>
): IDockviewPanel {
return this.component.addPanel(options); return this.component.addPanel(options);
} }

View File

@ -10,7 +10,7 @@ export abstract class DragHandler extends CompositeDisposable {
private readonly dataDisposable = new MutableDisposable(); private readonly dataDisposable = new MutableDisposable();
private readonly pointerEventsDisposable = new MutableDisposable(); private readonly pointerEventsDisposable = new MutableDisposable();
private readonly _onDragStart = new Emitter<void>(); private readonly _onDragStart = new Emitter<DragEvent>();
readonly onDragStart = this._onDragStart.event; readonly onDragStart = this._onDragStart.event;
constructor(protected readonly el: HTMLElement) { constructor(protected readonly el: HTMLElement) {
@ -25,7 +25,7 @@ export abstract class DragHandler extends CompositeDisposable {
this.configure(); this.configure();
} }
abstract getData(dataTransfer?: DataTransfer | null): IDisposable; abstract getData(event: DragEvent): IDisposable;
protected isCancelled(_event: DragEvent): boolean { protected isCancelled(_event: DragEvent): boolean {
return false; return false;
@ -35,7 +35,7 @@ export abstract class DragHandler extends CompositeDisposable {
this.addDisposables( this.addDisposables(
this._onDragStart, this._onDragStart,
addDisposableListener(this.el, 'dragstart', (event) => { addDisposableListener(this.el, 'dragstart', (event) => {
if (this.isCancelled(event)) { if (event.defaultPrevented || this.isCancelled(event)) {
event.preventDefault(); event.preventDefault();
return; return;
} }
@ -60,11 +60,15 @@ export abstract class DragHandler extends CompositeDisposable {
this.el.classList.add('dv-dragged'); this.el.classList.add('dv-dragged');
setTimeout(() => this.el.classList.remove('dv-dragged'), 0); 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) { if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.effectAllowed = 'move';
const hasData = event.dataTransfer.items.length > 0;
if (!hasData) {
/** /**
* Although this is not used by dockview many third party dnd libraries will check * Although this is not used by dockview many third party dnd libraries will check
* dataTransfer.types to determine valid drag events. * dataTransfer.types to determine valid drag events.
@ -79,6 +83,7 @@ export abstract class DragHandler extends CompositeDisposable {
'__dockview_internal_drag_event__' '__dockview_internal_drag_event__'
); );
} }
}
}), }),
addDisposableListener(this.el, 'dragend', () => { addDisposableListener(this.el, 'dragend', () => {
this.pointerEventsDisposable.dispose(); this.pointerEventsDisposable.dispose();

View File

@ -1,3 +1,4 @@
import { DockviewComponent } from '../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel'; import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { quasiPreventDefault } from '../dom'; import { quasiPreventDefault } from '../dom';
import { addDisposableListener } from '../events'; import { addDisposableListener } from '../events';
@ -12,7 +13,7 @@ export class GroupDragHandler extends DragHandler {
constructor( constructor(
element: HTMLElement, element: HTMLElement,
private readonly accessorId: string, private readonly accessor: DockviewComponent,
private readonly group: DockviewGroupPanel private readonly group: DockviewGroupPanel
) { ) {
super(element); super(element);
@ -43,9 +44,11 @@ export class GroupDragHandler extends DragHandler {
return false; return false;
} }
getData(dataTransfer: DataTransfer | null): IDisposable { getData(dragEvent: DragEvent): IDisposable {
const dataTransfer = dragEvent.dataTransfer;
this.panelTransfer.setData( this.panelTransfer.setData(
[new PanelTransfer(this.accessorId, this.group.id, null)], [new PanelTransfer(this.accessor.id, this.group.id, null)],
PanelTransfer.prototype PanelTransfer.prototype
); );

View File

@ -11,9 +11,37 @@ import { DockviewDropTargets, ITabRenderer } from '../../types';
import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { DroptargetEvent, Droptarget } from '../../../dnd/droptarget'; import { DroptargetEvent, Droptarget } from '../../../dnd/droptarget';
import { DragHandler } from '../../../dnd/abstractDragHandler'; import { DragHandler } from '../../../dnd/abstractDragHandler';
import { IDockviewPanel } from '../../dockviewPanel';
class TabDragHandler extends DragHandler {
private readonly panelTransfer =
LocalSelectionTransfer.getInstance<PanelTransfer>();
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 { export interface ITab extends IDisposable {
readonly panelId: string; readonly panel: IDockviewPanel;
readonly element: HTMLElement; readonly element: HTMLElement;
setContent: (element: ITabRenderer) => void; setContent: (element: ITabRenderer) => void;
onChanged: Event<MouseEvent>; onChanged: Event<MouseEvent>;
@ -24,7 +52,7 @@ export interface ITab extends IDisposable {
export class Tab extends CompositeDisposable implements ITab { export class Tab extends CompositeDisposable implements ITab {
private readonly _element: HTMLElement; private readonly _element: HTMLElement;
private readonly droptarget: Droptarget; private readonly droptarget: Droptarget;
private content?: ITabRenderer; private content: ITabRenderer | undefined = undefined;
private readonly _onChanged = new Emitter<MouseEvent>(); private readonly _onChanged = new Emitter<MouseEvent>();
readonly onChanged: Event<MouseEvent> = this._onChanged.event; readonly onChanged: Event<MouseEvent> = this._onChanged.event;
@ -32,12 +60,15 @@ export class Tab extends CompositeDisposable implements ITab {
private readonly _onDropped = new Emitter<DroptargetEvent>(); private readonly _onDropped = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDropped.event; readonly onDrop: Event<DroptargetEvent> = this._onDropped.event;
private readonly _onDragStart = new Emitter<DragEvent>();
readonly onDragStart = this._onDragStart.event;
public get element(): HTMLElement { public get element(): HTMLElement {
return this._element; return this._element;
} }
constructor( constructor(
public readonly panelId: string, public readonly panel: IDockviewPanel,
private readonly accessor: DockviewComponent, private readonly accessor: DockviewComponent,
private readonly group: DockviewGroupPanel private readonly group: DockviewGroupPanel
) { ) {
@ -50,38 +81,11 @@ export class Tab extends CompositeDisposable implements ITab {
toggleClass(this.element, 'inactive-tab', true); toggleClass(this.element, 'inactive-tab', true);
this.addDisposables( const dragHandler = new TabDragHandler(
this._onChanged, this._element,
this._onDropped, this.accessor,
new (class Handler extends DragHandler { this.group,
private readonly panelTransfer = this.panel
LocalSelectionTransfer.getInstance<PanelTransfer>();
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);
})
); );
this.droptarget = new Droptarget(this._element, { this.droptarget = new Droptarget(this._element, {
@ -102,7 +106,7 @@ export class Tab extends CompositeDisposable implements ITab {
return false; return false;
} }
return this.panelId !== data.panelId; return this.panel.id !== data.panelId;
} }
return this.group.model.canDisplayOverlay( return this.group.model.canDisplayOverlay(
@ -114,6 +118,20 @@ export class Tab extends CompositeDisposable implements ITab {
}); });
this.addDisposables( 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.droptarget.onDrop((event) => {
this._onDropped.fire(event); this._onDropped.fire(event);
}), }),

View File

@ -16,13 +16,26 @@ export interface TabDropIndexEvent {
readonly index: number; 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 { export interface ITabsContainer extends IDisposable {
readonly element: HTMLElement; readonly element: HTMLElement;
readonly panels: string[]; readonly panels: string[];
readonly size: number; readonly size: number;
hidden: boolean;
delete: (id: string) => void; delete: (id: string) => void;
indexOf: (id: string) => number; indexOf: (id: string) => number;
onDrop: Event<TabDropIndexEvent>; onDrop: Event<TabDropIndexEvent>;
onTabDragStart: Event<TabDragEvent>;
onGroupDragStart: Event<GroupDragEvent>;
setActive: (isGroupActive: boolean) => void; setActive: (isGroupActive: boolean) => void;
setActivePanel: (panel: IDockviewPanel) => void; setActivePanel: (panel: IDockviewPanel) => void;
isActive: (tab: ITab) => boolean; isActive: (tab: ITab) => boolean;
@ -30,7 +43,6 @@ export interface ITabsContainer extends IDisposable {
openPanel: (panel: IDockviewPanel, index?: number) => void; openPanel: (panel: IDockviewPanel, index?: number) => void;
setRightActionsElement(element: HTMLElement | undefined): void; setRightActionsElement(element: HTMLElement | undefined): void;
setLeftActionsElement(element: HTMLElement | undefined): void; setLeftActionsElement(element: HTMLElement | undefined): void;
hidden: boolean;
show(): void; show(): void;
hide(): void; hide(): void;
} }
@ -55,8 +67,15 @@ export class TabsContainer
private readonly _onDrop = new Emitter<TabDropIndexEvent>(); private readonly _onDrop = new Emitter<TabDropIndexEvent>();
readonly onDrop: Event<TabDropIndexEvent> = this._onDrop.event; readonly onDrop: Event<TabDropIndexEvent> = this._onDrop.event;
private readonly _onTabDragStart = new Emitter<TabDragEvent>();
readonly onTabDragStart: Event<TabDragEvent> = this._onTabDragStart.event;
private readonly _onGroupDragStart = new Emitter<GroupDragEvent>();
readonly onGroupDragStart: Event<GroupDragEvent> =
this._onGroupDragStart.event;
get panels(): string[] { get panels(): string[] {
return this.tabs.map((_) => _.value.panelId); return this.tabs.map((_) => _.value.panel.id);
} }
get size(): number { get size(): number {
@ -122,7 +141,7 @@ export class TabsContainer
} }
public indexOf(id: string): number { 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( constructor(
@ -131,7 +150,11 @@ export class TabsContainer
) { ) {
super(); super();
this.addDisposables(this._onDrop); this.addDisposables(
this._onDrop,
this._onTabDragStart,
this._onGroupDragStart
);
this._element = document.createElement('div'); this._element = document.createElement('div');
this._element.className = 'tabs-and-actions-container'; this._element.className = 'tabs-and-actions-container';
@ -181,6 +204,12 @@ export class TabsContainer
this.addDisposables( this.addDisposables(
this.voidContainer, this.voidContainer,
this.voidContainer.onDragStart((event) => {
this._onGroupDragStart.fire({
nativeEvent: event,
group: this.group,
});
}),
this.voidContainer.onDrop((event) => { this.voidContainer.onDrop((event) => {
this._onDrop.fire({ this._onDrop.fire({
event: event.nativeEvent, event: event.nativeEvent,
@ -260,7 +289,7 @@ export class TabsContainer
} }
public delete(id: string): void { 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]; const tabToRemove = this.tabs.splice(index, 1)[0];
@ -273,7 +302,7 @@ export class TabsContainer
public setActivePanel(panel: IDockviewPanel): void { public setActivePanel(panel: IDockviewPanel): void {
this.tabs.forEach((tab) => { this.tabs.forEach((tab) => {
const isActivePanel = panel.id === tab.value.panelId; const isActivePanel = panel.id === tab.value.panel.id;
tab.value.setActive(isActivePanel); tab.value.setActive(isActivePanel);
}); });
} }
@ -282,17 +311,20 @@ export class TabsContainer
panel: IDockviewPanel, panel: IDockviewPanel,
index: number = this.tabs.length index: number = this.tabs.length
): void { ): void {
if (this.tabs.find((tab) => tab.value.panelId === panel.id)) { if (this.tabs.find((tab) => tab.value.panel.id === panel.id)) {
return; return;
} }
const tabToAdd = new Tab(panel.id, this.accessor, this.group); const tab = new Tab(panel, this.accessor, this.group);
if (!panel.view?.tab) { if (!panel.view?.tab) {
throw new Error('invalid header component'); throw new Error('invalid header component');
} }
tabToAdd.setContent(panel.view.tab); tab.setContent(panel.view.tab);
const disposable = CompositeDisposable.from( const disposable = new CompositeDisposable(
tabToAdd.onChanged((event) => { tab.onDragStart((event) => {
this._onTabDragStart.fire({ nativeEvent: event, panel });
}),
tab.onChanged((event) => {
const isFloatingGroupsEnabled = const isFloatingGroupsEnabled =
!this.accessor.options.disableFloatingGroups; !this.accessor.options.disableFloatingGroups;
@ -306,10 +338,9 @@ export class TabsContainer
) { ) {
event.preventDefault(); event.preventDefault();
const panel = this.accessor.getGroupPanel(tabToAdd.panelId); const panel = this.accessor.getGroupPanel(tab.panel.id);
const { top, left } = const { top, left } = tab.element.getBoundingClientRect();
tabToAdd.element.getBoundingClientRect();
const { top: rootTop, left: rootLeft } = const { top: rootTop, left: rootLeft } =
this.accessor.element.getBoundingClientRect(); this.accessor.element.getBoundingClientRect();
@ -338,15 +369,15 @@ export class TabsContainer
skipFocus: alreadyFocused, skipFocus: alreadyFocused,
}); });
}), }),
tabToAdd.onDrop((event) => { tab.onDrop((event) => {
this._onDrop.fire({ this._onDrop.fire({
event: event.nativeEvent, event: event.nativeEvent,
index: this.tabs.findIndex((x) => x.value === tabToAdd), index: this.tabs.findIndex((x) => x.value === tab),
}); });
}) })
); );
const value: IValueDisposable<ITab> = { value: tabToAdd, disposable }; const value: IValueDisposable<ITab> = { value: tab, disposable };
this.addTab(value, index); this.addTab(value, index);
} }

View File

@ -15,6 +15,9 @@ export class VoidContainer extends CompositeDisposable {
private readonly _onDrop = new Emitter<DroptargetEvent>(); private readonly _onDrop = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDrop.event; readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
private readonly _onDragStart = new Emitter<DragEvent>();
readonly onDragStart = this._onDragStart.event;
get element(): HTMLElement { get element(): HTMLElement {
return this._element; return this._element;
} }
@ -33,12 +36,13 @@ export class VoidContainer extends CompositeDisposable {
this.addDisposables( this.addDisposables(
this._onDrop, this._onDrop,
this._onDragStart,
addDisposableListener(this._element, 'click', () => { addDisposableListener(this._element, 'click', () => {
this.accessor.doSetGroupActive(this.group); 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, { this.voidDropTarget = new Droptarget(this._element, {
acceptedTargetZones: ['center'], acceptedTargetZones: ['center'],
@ -68,6 +72,9 @@ export class VoidContainer extends CompositeDisposable {
this.addDisposables( this.addDisposables(
handler, handler,
handler.onDragStart((event) => {
this._onDragStart.fire(event);
}),
this.voidDropTarget.onDrop((event) => { this.voidDropTarget.onDrop((event) => {
this._onDrop.fire(event); this._onDrop.fire(event);
}), }),

View File

@ -51,6 +51,10 @@ import {
DockviewFloatingGroupPanel, DockviewFloatingGroupPanel,
IDockviewFloatingGroupPanel, IDockviewFloatingGroupPanel,
} from './dockviewFloatingGroupPanel'; } from './dockviewFloatingGroupPanel';
import {
GroupDragEvent,
TabDragEvent,
} from './components/titlebar/tabsContainer';
export interface PanelReference { export interface PanelReference {
update: (event: { params: { [key: string]: any } }) => void; update: (event: { params: { [key: string]: any } }) => void;
@ -133,6 +137,8 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
readonly onDidAddPanel: Event<IDockviewPanel>; readonly onDidAddPanel: Event<IDockviewPanel>;
readonly onDidLayoutFromJSON: Event<void>; readonly onDidLayoutFromJSON: Event<void>;
readonly onDidActivePanelChange: Event<IDockviewPanel | undefined>; readonly onDidActivePanelChange: Event<IDockviewPanel | undefined>;
readonly onWillDragPanel: Event<TabDragEvent>;
readonly onWillDragGroup: Event<GroupDragEvent>;
addFloatingGroup( addFloatingGroup(
item: IDockviewPanel | DockviewGroupPanel, item: IDockviewPanel | DockviewGroupPanel,
coord?: { x: number; y: number } coord?: { x: number; y: number }
@ -149,6 +155,13 @@ export class DockviewComponent
private _options: Exclude<DockviewComponentOptions, 'orientation'>; private _options: Exclude<DockviewComponentOptions, 'orientation'>;
private watermark: IWatermarkRenderer | null = null; private watermark: IWatermarkRenderer | null = null;
private readonly _onWillDragPanel = new Emitter<TabDragEvent>();
readonly onWillDragPanel: Event<TabDragEvent> = this._onWillDragPanel.event;
private readonly _onWillDragGroup = new Emitter<GroupDragEvent>();
readonly onWillDragGroup: Event<GroupDragEvent> =
this._onWillDragGroup.event;
private readonly _onDidDrop = new Emitter<DockviewDropEvent>(); private readonly _onDidDrop = new Emitter<DockviewDropEvent>();
readonly onDidDrop: Event<DockviewDropEvent> = this._onDidDrop.event; readonly onDidDrop: Event<DockviewDropEvent> = this._onDidDrop.event;
@ -207,6 +220,12 @@ export class DockviewComponent
toggleClass(this.gridview.element, 'dv-dockview', true); toggleClass(this.gridview.element, 'dv-dockview', true);
this.addDisposables( this.addDisposables(
this._onWillDragPanel,
this._onWillDragGroup,
this._onDidActivePanelChange,
this._onDidAddPanel,
this._onDidRemovePanel,
this._onDidLayoutFromJSON,
this._onDidDrop, this._onDidDrop,
Event.any( Event.any(
this.onDidAddGroup, this.onDidAddGroup,
@ -1171,6 +1190,12 @@ export class DockviewComponent
if (!this._groups.has(view.id)) { if (!this._groups.has(view.id)) {
const disposable = new CompositeDisposable( const disposable = new CompositeDisposable(
view.model.onTabDragStart((event) => {
this._onWillDragPanel.fire(event);
}),
view.model.onGroupDragStart((event) => {
this._onWillDragGroup.fire(event);
}),
view.model.onMove((event) => { view.model.onMove((event) => {
const { groupId, itemId, target, index } = event; const { groupId, itemId, target, index } = event;
this.moveGroupOrPanel(view, groupId, itemId, target, index); this.moveGroupOrPanel(view, groupId, itemId, target, index);
@ -1246,13 +1271,4 @@ export class DockviewComponent
group.value.model.containsPanel(panel) group.value.model.containsPanel(panel)
)?.value; )?.value;
} }
public dispose(): void {
this._onDidActivePanelChange.dispose();
this._onDidAddPanel.dispose();
this._onDidRemovePanel.dispose();
this._onDidLayoutFromJSON.dispose();
super.dispose();
}
} }

View File

@ -12,7 +12,9 @@ import {
IContentContainer, IContentContainer,
} from './components/panel/content'; } from './components/panel/content';
import { import {
GroupDragEvent,
ITabsContainer, ITabsContainer,
TabDragEvent,
TabsContainer, TabsContainer,
} from './components/titlebar/tabsContainer'; } from './components/titlebar/tabsContainer';
import { DockviewDropTargets, IWatermarkRenderer } from './types'; import { DockviewDropTargets, IWatermarkRenderer } from './types';
@ -160,6 +162,13 @@ export class DockviewGroupPanelModel
private readonly _onDidDrop = new Emitter<GroupviewDropEvent>(); private readonly _onDidDrop = new Emitter<GroupviewDropEvent>();
readonly onDidDrop: Event<GroupviewDropEvent> = this._onDidDrop.event; readonly onDidDrop: Event<GroupviewDropEvent> = this._onDidDrop.event;
private readonly _onTabDragStart = new Emitter<TabDragEvent>();
readonly onTabDragStart: Event<TabDragEvent> = this._onTabDragStart.event;
private readonly _onGroupDragStart = new Emitter<GroupDragEvent>();
readonly onGroupDragStart: Event<GroupDragEvent> =
this._onGroupDragStart.event;
private readonly _onDidAddPanel = new Emitter<GroupviewChangeEvent>(); private readonly _onDidAddPanel = new Emitter<GroupviewChangeEvent>();
readonly onDidAddPanel: Event<GroupviewChangeEvent> = readonly onDidAddPanel: Event<GroupviewChangeEvent> =
this._onDidAddPanel.event; this._onDidAddPanel.event;
@ -315,6 +324,14 @@ export class DockviewGroupPanelModel
this.locked = options.locked || false; this.locked = options.locked || false;
this.addDisposables( 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.tabsContainer.onDrop((event) => {
this.handleDropEvent(event.event, 'center', event.index); this.handleDropEvent(event.event, 'center', event.index);
}), }),

View File

@ -23,10 +23,6 @@ export class CompositeDisposable {
return this._isDisposed; return this._isDisposed;
} }
public static from(...args: IDisposable[]): CompositeDisposable {
return new CompositeDisposable(...args);
}
constructor(...args: IDisposable[]) { constructor(...args: IDisposable[]) {
this._disposables = args; this._disposables = args;
} }

View File

@ -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.
<MultiFrameworkContainer sandboxId="dnd-dockview" react={DndDockview} /> <MultiFrameworkContainer sandboxId="dnd-dockview" react={DndDockview} />
### Third Party Dnd Libraries ### Third Party Dnd Libraries

View File

@ -1,4 +1,5 @@
import { import {
DockviewApi,
DockviewDndOverlayEvent, DockviewDndOverlayEvent,
DockviewDropEvent, DockviewDropEvent,
DockviewReact, DockviewReact,
@ -37,13 +38,19 @@ const DraggableElement = () => (
}} }}
draggable={true} draggable={true}
> >
Drag me Drag me into the dock
</span> </span>
); );
const DndDockview = (props: { renderVisibleOnly: boolean; theme?: string }) => { const DndDockview = (props: { renderVisibleOnly: boolean; theme?: string }) => {
const onReady = (event: DockviewReadyEvent) => { const [api, setApi] = React.useState<DockviewApi>();
event.api.addPanel({
React.useEffect(() => {
if (!api) {
return;
}
api.addPanel({
id: 'panel_1', id: 'panel_1',
component: 'default', component: 'default',
params: { params: {
@ -51,7 +58,7 @@ const DndDockview = (props: { renderVisibleOnly: boolean; theme?: string }) => {
}, },
}); });
event.api.addPanel({ api.addPanel({
id: 'panel_2', id: 'panel_2',
component: 'default', component: 'default',
params: { params: {
@ -59,7 +66,7 @@ const DndDockview = (props: { renderVisibleOnly: boolean; theme?: string }) => {
}, },
}); });
event.api.addPanel({ api.addPanel({
id: 'panel_3', id: 'panel_3',
component: 'default', component: 'default',
params: { params: {
@ -67,7 +74,7 @@ const DndDockview = (props: { renderVisibleOnly: boolean; theme?: string }) => {
}, },
}); });
event.api.addPanel({ api.addPanel({
id: 'panel_4', id: 'panel_4',
component: 'default', component: 'default',
params: { params: {
@ -75,6 +82,45 @@ const DndDockview = (props: { renderVisibleOnly: boolean; theme?: string }) => {
}, },
position: { referencePanel: 'panel_1', direction: 'right' }, 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) => { const onDidDrop = (event: DockviewDropEvent) => {
@ -92,6 +138,20 @@ const DndDockview = (props: { renderVisibleOnly: boolean; theme?: string }) => {
return true; 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 ( return (
<div <div
style={{ style={{
@ -102,6 +162,17 @@ const DndDockview = (props: { renderVisibleOnly: boolean; theme?: string }) => {
> >
<div style={{ margin: '2px 0px' }}> <div style={{ margin: '2px 0px' }}>
<DraggableElement /> <DraggableElement />
<div
style={{
padding: '0px 4px',
backgroundColor: 'black',
borderRadius: '2px',
color: 'white',
}}
onDrop={onDrop}
>
Drop a tab or group here to inspect the attached metadata
</div>
</div> </div>
<DockviewReact <DockviewReact
components={components} components={components}