Merge pull request #171 from mathuo/170-allow-dnd-of-entire-dockview-groups

feat: dnd dockview groups
This commit is contained in:
mathuo 2023-02-12 20:54:15 +07:00 committed by GitHub
commit b8a5d30f81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 239 additions and 63 deletions

View File

@ -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();
});

View File

@ -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: [],
};
});

View File

@ -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';

View File

@ -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();
}

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

View 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 {
//
}
}

View File

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

View File

@ -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
);

View File

@ -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

View File

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

View File

@ -13,6 +13,7 @@
.void-container {
display: flex;
flex-grow: 1;
cursor: grab;
}
.tabs-container {

View File

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

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