mirror of
https://github.com/mathuo/dockview
synced 2025-09-10 19:36:33 +00:00
feat: dnd dockview groups
This commit is contained in:
parent
5e4a9d5855
commit
5e4d2cb506
@ -76,6 +76,7 @@ describe('tabsContainer', () => {
|
||||
return {
|
||||
id: 'testgroupid',
|
||||
model: groupView,
|
||||
panels: [],
|
||||
};
|
||||
});
|
||||
|
||||
@ -120,7 +121,7 @@ describe('tabsContainer', () => {
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('that dropping the last tab should render no drop target', () => {
|
||||
test('that dropping over the empty space should render a drop target', () => {
|
||||
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
|
||||
return {
|
||||
id: 'testcomponentid',
|
||||
@ -138,6 +139,7 @@ describe('tabsContainer', () => {
|
||||
return {
|
||||
id: 'testgroupid',
|
||||
model: groupView,
|
||||
panels: [],
|
||||
};
|
||||
});
|
||||
|
||||
@ -176,7 +178,7 @@ describe('tabsContainer', () => {
|
||||
|
||||
expect(
|
||||
cut.element.getElementsByClassName('drop-target-dropzone').length
|
||||
).toBe(0);
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('that dropping the first tab should render a drop target', () => {
|
||||
@ -197,6 +199,7 @@ describe('tabsContainer', () => {
|
||||
return {
|
||||
id: 'testgroupid',
|
||||
model: groupView,
|
||||
panels: [],
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -14,12 +14,12 @@ export abstract class DragHandler extends CompositeDisposable {
|
||||
|
||||
private iframes: HTMLElement[] = [];
|
||||
|
||||
constructor(private readonly el: HTMLElement) {
|
||||
constructor(protected readonly el: HTMLElement) {
|
||||
super();
|
||||
this.configure();
|
||||
}
|
||||
|
||||
abstract getData(): IDisposable;
|
||||
abstract getData(dataTransfer?: DataTransfer | null): IDisposable;
|
||||
|
||||
private configure() {
|
||||
this.addDisposables(
|
||||
@ -37,7 +37,7 @@ export abstract class DragHandler extends CompositeDisposable {
|
||||
this.el.classList.add('dragged');
|
||||
setTimeout(() => this.el.classList.remove('dragged'), 0);
|
||||
|
||||
this.disposable.value = this.getData();
|
||||
this.disposable.value = this.getData(event.dataTransfer);
|
||||
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
|
@ -14,6 +14,15 @@ export class PanelTransfer extends TransferObject {
|
||||
}
|
||||
}
|
||||
|
||||
export class GroupTransfer extends TransferObject {
|
||||
constructor(
|
||||
public readonly viewId: string,
|
||||
public readonly groupId: string
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class PaneTransfer extends TransferObject {
|
||||
constructor(
|
||||
public readonly viewId: string,
|
||||
@ -78,6 +87,17 @@ export function getPanelData(): PanelTransfer | undefined {
|
||||
return panelTransfer.getData(PanelTransfer.prototype)![0];
|
||||
}
|
||||
|
||||
export function getGroupData(): GroupTransfer | undefined {
|
||||
const panelTransfer = LocalSelectionTransfer.getInstance<GroupTransfer>();
|
||||
const isPanelEvent = panelTransfer.hasData(GroupTransfer.prototype);
|
||||
|
||||
if (!isPanelEvent) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return panelTransfer.getData(GroupTransfer.prototype)![0];
|
||||
}
|
||||
|
||||
export function getPaneData(): PaneTransfer | undefined {
|
||||
const paneTransfer = LocalSelectionTransfer.getInstance<PaneTransfer>();
|
||||
const isPanelEvent = paneTransfer.hasData(PaneTransfer.prototype);
|
||||
|
16
packages/dockview/src/dnd/ghost.ts
Normal file
16
packages/dockview/src/dnd/ghost.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { addClasses } from '../dom';
|
||||
|
||||
export function addGhostImage(
|
||||
dataTransfer: DataTransfer,
|
||||
ghostElement: HTMLElement
|
||||
) {
|
||||
// class dockview provides to force ghost image to be drawn on a different layer and prevent weird rendering issues
|
||||
addClasses(ghostElement, 'dragged');
|
||||
|
||||
document.body.appendChild(ghostElement);
|
||||
dataTransfer.setDragImage(ghostElement, 0, 0);
|
||||
|
||||
setTimeout(() => {
|
||||
ghostElement.remove();
|
||||
}, 0);
|
||||
}
|
@ -31,7 +31,7 @@ import {
|
||||
toTarget,
|
||||
} from '../gridview/baseComponentGridview';
|
||||
import { DockviewApi } from '../api/component.api';
|
||||
import { Orientation } from '../splitview/core/splitview';
|
||||
import { Orientation, Sizing } from '../splitview/core/splitview';
|
||||
import { DefaultTab } from './components/tab/defaultTab';
|
||||
import {
|
||||
GroupOptions,
|
||||
@ -565,7 +565,7 @@ export class DockviewComponent
|
||||
moveGroupOrPanel(
|
||||
referenceGroup: GroupPanel,
|
||||
groupId: string,
|
||||
itemId: string,
|
||||
itemId: string | undefined,
|
||||
target: Position,
|
||||
index?: number
|
||||
): void {
|
||||
@ -573,6 +573,43 @@ export class DockviewComponent
|
||||
? this._groups.get(groupId)?.value
|
||||
: undefined;
|
||||
|
||||
|
||||
if(itemId === undefined) {
|
||||
|
||||
|
||||
if(sourceGroup) {
|
||||
if (!target || target === Position.Center) {
|
||||
const activePanel = sourceGroup.activePanel;
|
||||
const panels = [...sourceGroup.panels].map(p => sourceGroup.model.removePanel(p.id));
|
||||
|
||||
if (sourceGroup?.model.size === 0) {
|
||||
this.doRemoveGroup(sourceGroup);
|
||||
}
|
||||
|
||||
for(const panel of panels) {
|
||||
referenceGroup.model.openPanel(panel,{skipSetPanelActive:panel !== activePanel});
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
this.gridview.removeView(getGridLocation(sourceGroup.element));
|
||||
|
||||
const referenceLocation = getGridLocation(referenceGroup.element);
|
||||
const dropLocation = getRelativeLocation(
|
||||
this.gridview.orientation,
|
||||
referenceLocation,
|
||||
target
|
||||
);
|
||||
|
||||
|
||||
|
||||
this.gridview.addView(sourceGroup, Sizing.Distribute, dropLocation);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target || target === Position.Center) {
|
||||
const groupItem: IDockviewPanel | undefined =
|
||||
sourceGroup?.model.removePanel(itemId) ||
|
||||
@ -595,6 +632,7 @@ export class DockviewComponent
|
||||
target
|
||||
);
|
||||
|
||||
|
||||
if (sourceGroup && sourceGroup.size < 2) {
|
||||
const [targetParentLocation, to] = tail(targetLocation);
|
||||
const sourceLocation = getGridLocation(sourceGroup.element);
|
||||
@ -640,7 +678,7 @@ export class DockviewComponent
|
||||
target
|
||||
);
|
||||
|
||||
const group = this.createGroupAtLocation( dropLocation);
|
||||
const group = this.createGroupAtLocation(dropLocation);
|
||||
group.model.openPanel(groupItem);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DockviewApi } from '../api/component.api';
|
||||
import { getPanelData, PanelTransfer } from '../dnd/dataTransfer';
|
||||
import { getGroupData, getPanelData, PanelTransfer } from '../dnd/dataTransfer';
|
||||
import { Droptarget, Position } from '../dnd/droptarget';
|
||||
import { DockviewComponent } from '../dockview/dockviewComponent';
|
||||
import { isAncestor, toggleClass } from '../dom';
|
||||
@ -38,7 +38,7 @@ export interface IGroupItem {
|
||||
|
||||
interface GroupMoveEvent {
|
||||
groupId: string;
|
||||
itemId: string;
|
||||
itemId?: string;
|
||||
target: Position;
|
||||
index?: number;
|
||||
}
|
||||
@ -245,6 +245,16 @@ export class Groupview extends CompositeDisposable implements IGroupview {
|
||||
return false;
|
||||
}
|
||||
|
||||
const group = getGroupData();
|
||||
|
||||
if (
|
||||
group &&
|
||||
group.viewId === this.accessor.id &&
|
||||
group.groupId !== this.id
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const data = getPanelData();
|
||||
|
||||
if (data && data.viewId === this.accessor.id) {
|
||||
@ -685,6 +695,18 @@ export class Groupview extends CompositeDisposable implements IGroupview {
|
||||
position: Position,
|
||||
index?: number
|
||||
): void {
|
||||
const groupData = getGroupData();
|
||||
|
||||
if (groupData) {
|
||||
const { groupId } = groupData;
|
||||
this._onMove.fire({
|
||||
target: position,
|
||||
groupId: groupId,
|
||||
index,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const data = getPanelData();
|
||||
|
||||
if (data) {
|
||||
|
@ -13,6 +13,7 @@
|
||||
.void-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
|
@ -5,13 +5,10 @@ import {
|
||||
} from '../../lifecycle';
|
||||
import { addDisposableListener, Emitter, Event } from '../../events';
|
||||
import { ITab, MouseEventKind, Tab } from '../tab';
|
||||
import { last } from '../../array';
|
||||
import { IDockviewPanel } from '../groupPanel';
|
||||
import { DockviewComponent } from '../../dockview/dockviewComponent';
|
||||
import { getPanelData } from '../../dnd/dataTransfer';
|
||||
import { GroupPanel } from '../groupviewPanel';
|
||||
import { Droptarget } from '../../dnd/droptarget';
|
||||
import { DockviewDropTargets } from '../dnd';
|
||||
import { VoidContainer } from './voidContainer';
|
||||
|
||||
export interface TabDropIndexEvent {
|
||||
event: DragEvent;
|
||||
@ -44,10 +41,8 @@ export class TabsContainer
|
||||
{
|
||||
private readonly _element: HTMLElement;
|
||||
private readonly tabContainer: HTMLElement;
|
||||
private readonly voidContainer: HTMLElement;
|
||||
private readonly actionContainer: HTMLElement;
|
||||
|
||||
private readonly voidDropTarget: Droptarget;
|
||||
private readonly voidContainer: VoidContainer;
|
||||
|
||||
private tabs: IValueDisposable<ITab>[] = [];
|
||||
private selectedIndex = -1;
|
||||
@ -138,9 +133,9 @@ export class TabsContainer
|
||||
}
|
||||
|
||||
constructor(
|
||||
private accessor: DockviewComponent,
|
||||
private group: GroupPanel,
|
||||
options: { tabHeight?: number }
|
||||
private readonly accessor: DockviewComponent,
|
||||
private readonly group: GroupPanel,
|
||||
readonly options: { tabHeight?: number }
|
||||
) {
|
||||
super();
|
||||
|
||||
@ -157,38 +152,20 @@ export class TabsContainer
|
||||
this.tabContainer = document.createElement('div');
|
||||
this.tabContainer.className = 'tabs-container';
|
||||
|
||||
this.voidContainer = document.createElement('div');
|
||||
this.voidContainer.className = 'void-container';
|
||||
this.voidContainer = new VoidContainer(this.accessor, this.group);
|
||||
|
||||
this._element.appendChild(this.tabContainer);
|
||||
this._element.appendChild(this.voidContainer);
|
||||
this._element.appendChild(this.voidContainer.element);
|
||||
this._element.appendChild(this.actionContainer);
|
||||
|
||||
this.voidDropTarget = new Droptarget(this.voidContainer, {
|
||||
validOverlays: 'none',
|
||||
canDisplayOverlay: (event) => {
|
||||
const data = getPanelData();
|
||||
|
||||
if (data && this.accessor.id === data.viewId) {
|
||||
// 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.voidContainer,
|
||||
this.voidContainer.onDrop((event) => {
|
||||
this._onDrop.fire({
|
||||
event: event.nativeEvent,
|
||||
index: this.tabs.length,
|
||||
});
|
||||
}),
|
||||
this.voidDropTarget,
|
||||
addDisposableListener(this.tabContainer, 'mousedown', (event) => {
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
|
132
packages/dockview/src/groupview/titlebar/voidContainer.ts
Normal file
132
packages/dockview/src/groupview/titlebar/voidContainer.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { last } from '../../array';
|
||||
import { DragHandler } from '../../dnd/abstractDragHandler';
|
||||
import {
|
||||
getPanelData,
|
||||
GroupTransfer,
|
||||
LocalSelectionTransfer,
|
||||
} from '../../dnd/dataTransfer';
|
||||
import { Droptarget, DroptargetEvent } from '../../dnd/droptarget';
|
||||
import { addGhostImage } from '../../dnd/ghost';
|
||||
import { DockviewComponent } from '../../dockview/dockviewComponent';
|
||||
import { addDisposableListener, Emitter, Event } from '../../events';
|
||||
import { CompositeDisposable, IDisposable } from '../../lifecycle';
|
||||
import { DockviewDropTargets } from '../dnd';
|
||||
import { GroupPanel } from '../groupviewPanel';
|
||||
|
||||
class CustomDragHandler extends DragHandler {
|
||||
private readonly panelTransfer =
|
||||
LocalSelectionTransfer.getInstance<GroupTransfer>();
|
||||
|
||||
constructor(
|
||||
element: HTMLElement,
|
||||
private readonly accessorId: string,
|
||||
private readonly group: GroupPanel
|
||||
) {
|
||||
super(element);
|
||||
}
|
||||
|
||||
getData(dataTransfer: DataTransfer | null): IDisposable {
|
||||
this.panelTransfer.setData(
|
||||
[new GroupTransfer(this.accessorId, this.group.id)],
|
||||
GroupTransfer.prototype
|
||||
);
|
||||
|
||||
const style = window.getComputedStyle(this.el);
|
||||
|
||||
const bgColor = style.getPropertyValue(
|
||||
'--dv-activegroup-visiblepanel-tab-background-color'
|
||||
);
|
||||
const color = style.getPropertyValue(
|
||||
'--dv-activegroup-visiblepanel-tab-color'
|
||||
);
|
||||
|
||||
if (dataTransfer) {
|
||||
const ghostElement = document.createElement('div');
|
||||
|
||||
ghostElement.style.backgroundColor = bgColor;
|
||||
ghostElement.style.color = color;
|
||||
ghostElement.style.padding = '2px 8px';
|
||||
ghostElement.style.height = '24px';
|
||||
ghostElement.style.fontSize = '11px';
|
||||
ghostElement.style.lineHeight = '20px';
|
||||
ghostElement.style.borderRadius = '12px';
|
||||
ghostElement.style.position = 'absolute';
|
||||
ghostElement.textContent = `Multiple Panels (${this.group.size})`;
|
||||
|
||||
addGhostImage(dataTransfer, ghostElement);
|
||||
}
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
this.panelTransfer.clearData(GroupTransfer.prototype);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
export class VoidContainer extends CompositeDisposable {
|
||||
private readonly _element: HTMLElement;
|
||||
private readonly voidDropTarget: Droptarget;
|
||||
|
||||
private readonly _onDrop = new Emitter<DroptargetEvent>();
|
||||
readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
|
||||
|
||||
get element() {
|
||||
return this._element;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly accessor: DockviewComponent,
|
||||
private readonly group: GroupPanel
|
||||
) {
|
||||
super();
|
||||
|
||||
this._element = document.createElement('div');
|
||||
|
||||
this._element.className = 'void-container';
|
||||
this._element.tabIndex = 0;
|
||||
this._element.draggable = true;
|
||||
|
||||
this.addDisposables(
|
||||
this._onDrop,
|
||||
addDisposableListener(this._element, 'click', () => {
|
||||
this.accessor.doSetGroupActive(this.group);
|
||||
})
|
||||
);
|
||||
|
||||
const handler = new CustomDragHandler(
|
||||
this._element,
|
||||
accessor.id,
|
||||
group
|
||||
);
|
||||
|
||||
this.voidDropTarget = new Droptarget(this._element, {
|
||||
validOverlays: 'none',
|
||||
canDisplayOverlay: (event) => {
|
||||
const data = getPanelData();
|
||||
|
||||
if (data && this.accessor.id === data.viewId) {
|
||||
// don't show the overlay if the tab being dragged is the last panel of this group
|
||||
return last(this.group.panels)?.id !== data.panelId;
|
||||
}
|
||||
|
||||
return group.model.canDisplayOverlay(
|
||||
event,
|
||||
DockviewDropTargets.Panel
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
this.addDisposables(
|
||||
handler,
|
||||
this.voidDropTarget.onDrop((event) => {
|
||||
this._onDrop.fire(event);
|
||||
}),
|
||||
this.voidDropTarget
|
||||
);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user