mirror of
https://github.com/mathuo/dockview
synced 2025-09-07 09:56:40 +00:00
Merge pull request #171 from mathuo/170-allow-dnd-of-entire-dockview-groups
feat: dnd dockview groups
This commit is contained in:
commit
b8a5d30f81
@ -3,7 +3,7 @@ import { DragHandler } from '../../dnd/abstractDragHandler';
|
||||
import { IDisposable } from '../../lifecycle';
|
||||
|
||||
describe('abstractDragHandler', () => {
|
||||
test('that className dragged is added to element after dragstart event', () => {
|
||||
test('that className dv-dragged is added to element after dragstart event', () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const element = document.createElement('div');
|
||||
@ -26,13 +26,13 @@ describe('abstractDragHandler', () => {
|
||||
}
|
||||
})(element);
|
||||
|
||||
expect(element.classList.contains('dragged')).toBeFalsy();
|
||||
expect(element.classList.contains('dv-dragged')).toBeFalsy();
|
||||
|
||||
fireEvent.dragStart(element);
|
||||
expect(element.classList.contains('dragged')).toBeTruthy();
|
||||
expect(element.classList.contains('dv-dragged')).toBeTruthy();
|
||||
|
||||
jest.runAllTimers();
|
||||
expect(element.classList.contains('dragged')).toBeFalsy();
|
||||
expect(element.classList.contains('dv-dragged')).toBeFalsy();
|
||||
|
||||
handler.dispose();
|
||||
});
|
||||
|
@ -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(
|
||||
@ -34,10 +34,10 @@ export abstract class DragHandler extends CompositeDisposable {
|
||||
iframe.style.pointerEvents = 'none';
|
||||
}
|
||||
|
||||
this.el.classList.add('dragged');
|
||||
setTimeout(() => this.el.classList.remove('dragged'), 0);
|
||||
this.el.classList.add('dv-dragged');
|
||||
setTimeout(() => this.el.classList.remove('dv-dragged'), 0);
|
||||
|
||||
this.disposable.value = this.getData();
|
||||
this.disposable.value = this.getData(event.dataTransfer);
|
||||
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
|
@ -8,7 +8,7 @@ export class PanelTransfer extends TransferObject {
|
||||
constructor(
|
||||
public readonly viewId: string,
|
||||
public readonly groupId: string,
|
||||
public readonly panelId: string
|
||||
public readonly panelId: string | null
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
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, 'dv-dragged');
|
||||
|
||||
document.body.appendChild(ghostElement);
|
||||
dataTransfer.setDragImage(ghostElement, 0, 0);
|
||||
|
||||
setTimeout(() => {
|
||||
ghostElement.remove();
|
||||
}, 0);
|
||||
}
|
60
packages/dockview/src/dnd/groupDragHandler.ts
Normal file
60
packages/dockview/src/dnd/groupDragHandler.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { GroupPanel } from '../groupview/groupviewPanel';
|
||||
import { IDisposable } from '../lifecycle';
|
||||
import { DragHandler } from './abstractDragHandler';
|
||||
import { LocalSelectionTransfer, PanelTransfer } from './dataTransfer';
|
||||
import { addGhostImage } from './ghost';
|
||||
|
||||
export class GroupDragHandler extends DragHandler {
|
||||
private readonly panelTransfer =
|
||||
LocalSelectionTransfer.getInstance<PanelTransfer>();
|
||||
|
||||
constructor(
|
||||
element: HTMLElement,
|
||||
private readonly accessorId: string,
|
||||
private readonly group: GroupPanel
|
||||
) {
|
||||
super(element);
|
||||
}
|
||||
|
||||
getData(dataTransfer: DataTransfer | null): IDisposable {
|
||||
this.panelTransfer.setData(
|
||||
[new PanelTransfer(this.accessorId, this.group.id, null)],
|
||||
PanelTransfer.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(PanelTransfer.prototype);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
//
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
.dragged {
|
||||
.dv-dragged {
|
||||
transform: translate3d(
|
||||
0px,
|
||||
0px,
|
||||
@ -9,7 +9,7 @@
|
||||
.tab {
|
||||
flex-shrink: 0;
|
||||
|
||||
&.dragging {
|
||||
&.dv-tab-dragging {
|
||||
.tab-action {
|
||||
background-color: var(--dv-activegroup-visiblepanel-tab-color);
|
||||
}
|
||||
|
@ -1,15 +1,3 @@
|
||||
.custom-dragging {
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
font-size: 11px;
|
||||
width: 100px;
|
||||
background-color: dodgerblue;
|
||||
color: ghostwhite;
|
||||
border-radius: 11px;
|
||||
position: absolute;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.groupview {
|
||||
&.active-group {
|
||||
> .tabs-and-actions-container > .tabs-container > .tab {
|
||||
@ -50,7 +38,7 @@
|
||||
* therefore we also set some stylings for the dragging event
|
||||
**/
|
||||
.tab {
|
||||
&.dragging {
|
||||
&.dv-tab-dragging {
|
||||
background-color: var(
|
||||
--dv-activegroup-visiblepanel-tab-background-color
|
||||
);
|
||||
|
@ -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,14 @@ export class DockviewComponent
|
||||
? this._groups.get(groupId)?.value
|
||||
: undefined;
|
||||
|
||||
|
||||
if(itemId === undefined) {
|
||||
if(sourceGroup) {
|
||||
this.moveGroup(sourceGroup, referenceGroup, target);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target || target === Position.Center) {
|
||||
const groupItem: IDockviewPanel | undefined =
|
||||
sourceGroup?.model.removePanel(itemId) ||
|
||||
@ -595,6 +603,7 @@ export class DockviewComponent
|
||||
target
|
||||
);
|
||||
|
||||
|
||||
if (sourceGroup && sourceGroup.size < 2) {
|
||||
const [targetParentLocation, to] = tail(targetLocation);
|
||||
const sourceLocation = getGridLocation(sourceGroup.element);
|
||||
@ -640,12 +649,44 @@ export class DockviewComponent
|
||||
target
|
||||
);
|
||||
|
||||
const group = this.createGroupAtLocation( dropLocation);
|
||||
const group = this.createGroupAtLocation(dropLocation);
|
||||
group.model.openPanel(groupItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private moveGroup(sourceGroup: GroupPanel, referenceGroup: GroupPanel, target: Position): void {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override doSetGroupActive(
|
||||
group: GroupPanel | undefined,
|
||||
skipFocus?: boolean
|
||||
|
@ -38,7 +38,7 @@ export interface IGroupItem {
|
||||
|
||||
interface GroupMoveEvent {
|
||||
groupId: string;
|
||||
itemId: string;
|
||||
itemId?: string;
|
||||
target: Position;
|
||||
index?: number;
|
||||
}
|
||||
@ -247,6 +247,16 @@ export class Groupview extends CompositeDisposable implements IGroupview {
|
||||
|
||||
const data = getPanelData();
|
||||
|
||||
if (
|
||||
data &&
|
||||
data.panelId === null &&
|
||||
data.viewId === this.accessor.id &&
|
||||
data.groupId !== this.id
|
||||
) {
|
||||
// prevent dropping on self for group dnd
|
||||
return true;
|
||||
}
|
||||
|
||||
if (data && data.viewId === this.accessor.id) {
|
||||
const groupHasOnePanelAndIsActiveDragElement =
|
||||
this._panels.length === 1 && data.groupId === this.id;
|
||||
@ -686,6 +696,18 @@ export class Groupview extends CompositeDisposable implements IGroupview {
|
||||
const data = getPanelData();
|
||||
|
||||
if (data) {
|
||||
if (data.panelId === null) {
|
||||
// this is a group move dnd event
|
||||
const { groupId } = data;
|
||||
|
||||
this._onMove.fire({
|
||||
target: position,
|
||||
groupId: groupId,
|
||||
index,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const fromSameGroup =
|
||||
this.tabsContainer.indexOf(data.panelId) !== -1;
|
||||
|
||||
|
@ -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;
|
||||
|
68
packages/dockview/src/groupview/titlebar/voidContainer.ts
Normal file
68
packages/dockview/src/groupview/titlebar/voidContainer.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { last } from '../../array';
|
||||
import { getPanelData } from '../../dnd/dataTransfer';
|
||||
import { Droptarget, DroptargetEvent } from '../../dnd/droptarget';
|
||||
import { GroupDragHandler } from '../../dnd/groupDragHandler';
|
||||
import { DockviewComponent } from '../../dockview/dockviewComponent';
|
||||
import { addDisposableListener, Emitter, Event } from '../../events';
|
||||
import { CompositeDisposable } from '../../lifecycle';
|
||||
import { DockviewDropTargets } from '../dnd';
|
||||
import { GroupPanel } from '../groupviewPanel';
|
||||
|
||||
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 GroupDragHandler(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