mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 04:26:23 +08:00
205cd7a86d
Closes: BS-2946
454 lines
13 KiB
TypeScript
454 lines
13 KiB
TypeScript
import {
|
|
OverlayIdentifier,
|
|
type SurfaceBlockComponent,
|
|
} from '@blocksuite/affine-block-surface';
|
|
import {
|
|
type LayoutType,
|
|
type LocalConnectorElementModel,
|
|
MindmapElementModel,
|
|
type MindmapNode,
|
|
} from '@blocksuite/affine-model';
|
|
import type { Bound, IVec } from '@blocksuite/global/gfx';
|
|
import {
|
|
type DragExtensionInitializeContext,
|
|
type ExtensionDragEndContext,
|
|
type ExtensionDragMoveContext,
|
|
type ExtensionDragStartContext,
|
|
type GfxModel,
|
|
type GfxPrimitiveElementModel,
|
|
isGfxGroupCompatibleModel,
|
|
TransformExtension,
|
|
} from '@blocksuite/std/gfx';
|
|
|
|
import type { MindMapIndicatorOverlay } from '../indicator-overlay';
|
|
import { isMindmapNode, isSingleMindMapNode } from '../utils';
|
|
import { NODE_HORIZONTAL_SPACING, NODE_VERTICAL_SPACING } from '../view/layout';
|
|
import {
|
|
containsNode,
|
|
createFromTree,
|
|
detachMindmap,
|
|
findTargetNode,
|
|
hideNodeConnector,
|
|
tryMoveNode,
|
|
} from '../view/utils';
|
|
import { calculateResponseArea } from './drag-utils';
|
|
|
|
type DragMindMapCtx = {
|
|
mindmap: MindmapElementModel;
|
|
node: MindmapNode;
|
|
/**
|
|
* Whether the dragged node is the root node of the mind map
|
|
*/
|
|
isRoot: boolean;
|
|
originalMindMapBound: Bound;
|
|
};
|
|
|
|
export class MindMapDragExtension extends TransformExtension {
|
|
static override key = 'mind-map-drag';
|
|
/**
|
|
* The response area of the mind map is calculated in real time.
|
|
* It only needs to be calculated once when the mind map is dragged.
|
|
*/
|
|
private readonly _responseAreaUpdated = new Set<MindmapElementModel>();
|
|
|
|
private get _indicatorOverlay() {
|
|
return this.std.getOptional(
|
|
OverlayIdentifier('mindmap-indicator')
|
|
) as MindMapIndicatorOverlay | null;
|
|
}
|
|
|
|
private _calcDragResponseArea(mindmap: MindmapElementModel) {
|
|
calculateResponseArea(mindmap);
|
|
this._responseAreaUpdated.add(mindmap);
|
|
}
|
|
|
|
/**
|
|
* Create handlers that can drag and drop mind map nodes
|
|
* @param dragMindMapCtx
|
|
* @param dragState
|
|
* @returns
|
|
*/
|
|
private _createManipulationHandlers(dragMindMapCtx: DragMindMapCtx): {
|
|
onDragMove?: (context: ExtensionDragMoveContext) => void;
|
|
onDragEnd?: (context: ExtensionDragEndContext) => void;
|
|
} {
|
|
let hoveredCtx: {
|
|
mindmap: MindmapElementModel | null;
|
|
node: MindmapNode | null;
|
|
detach?: boolean;
|
|
abort?: () => void;
|
|
merge?: () => void;
|
|
} | null = null;
|
|
|
|
return {
|
|
onDragMove: (context: ExtensionDragMoveContext) => {
|
|
const { x, y } = context.dragLastPos;
|
|
const hoveredMindMap = this._getHoveredMindMap([x, y], dragMindMapCtx);
|
|
const indicator = this._indicatorOverlay;
|
|
|
|
if (indicator) {
|
|
indicator.currentDragPos = [x, y];
|
|
indicator.refresh();
|
|
}
|
|
|
|
hoveredCtx?.abort?.();
|
|
|
|
const hoveredNode = hoveredMindMap
|
|
? findTargetNode(hoveredMindMap, [x, y])
|
|
: null;
|
|
|
|
hoveredCtx = {
|
|
mindmap: hoveredMindMap,
|
|
node: hoveredNode,
|
|
};
|
|
|
|
// hovered on the currently dragged mind map but
|
|
// 1. not hovered on any node or
|
|
// 2. hovered on the node that is itself or its children (which is not allowed)
|
|
// then consider user is trying to drop the node to its original position
|
|
if (
|
|
hoveredNode &&
|
|
hoveredMindMap &&
|
|
!containsNode(hoveredMindMap, hoveredNode, dragMindMapCtx.node)
|
|
) {
|
|
const operation = tryMoveNode(
|
|
hoveredMindMap,
|
|
hoveredNode,
|
|
dragMindMapCtx.mindmap,
|
|
dragMindMapCtx.node,
|
|
[x, y],
|
|
options => this._drawIndicator(options)
|
|
);
|
|
|
|
if (operation) {
|
|
hoveredCtx.abort = operation.abort;
|
|
hoveredCtx.merge = operation.merge;
|
|
}
|
|
} else if (dragMindMapCtx.isRoot) {
|
|
dragMindMapCtx.mindmap.layout();
|
|
hoveredCtx.merge = () => {
|
|
dragMindMapCtx.mindmap.layout();
|
|
};
|
|
} else {
|
|
// if `hoveredMindMap` is not null
|
|
// either the node is hovered on the dragged node's children
|
|
// or the there is no hovered node at all
|
|
// then consider user is trying to place the node to its original position
|
|
if (hoveredMindMap) {
|
|
const { node: draggedNode, mindmap } = dragMindMapCtx;
|
|
const nodeBound = draggedNode.element.elementBound;
|
|
|
|
hoveredCtx.abort = this._drawIndicator({
|
|
targetMindMap: mindmap,
|
|
target: draggedNode,
|
|
sourceMindMap: mindmap,
|
|
source: draggedNode,
|
|
newParent: draggedNode.parent!,
|
|
insertPosition: {
|
|
type: 'sibling',
|
|
layoutDir: mindmap.getLayoutDir(draggedNode) as Exclude<
|
|
LayoutType,
|
|
LayoutType.BALANCE
|
|
>,
|
|
position: y > nodeBound.y + nodeBound.h / 2 ? 'next' : 'prev',
|
|
},
|
|
path: mindmap.getPath(draggedNode),
|
|
});
|
|
} else {
|
|
hoveredCtx.detach = true;
|
|
|
|
const reset = (hoveredCtx.abort = hideNodeConnector(
|
|
dragMindMapCtx.mindmap,
|
|
dragMindMapCtx.node
|
|
));
|
|
|
|
hoveredCtx.abort = () => {
|
|
reset?.();
|
|
};
|
|
}
|
|
}
|
|
},
|
|
onDragEnd: (dragEndContext: ExtensionDragEndContext) => {
|
|
if (hoveredCtx?.merge) {
|
|
hoveredCtx.merge();
|
|
} else {
|
|
hoveredCtx?.abort?.();
|
|
|
|
if (hoveredCtx?.detach) {
|
|
const { x: startX, y: startY } = dragEndContext.dragStartPos;
|
|
const { x: endX, y: endY } = dragEndContext.dragLastPos;
|
|
|
|
dragMindMapCtx.node.element.xywh =
|
|
dragMindMapCtx.node.element.elementBound
|
|
.moveDelta(endX - startX, endY - startY)
|
|
.serialize();
|
|
|
|
if (dragMindMapCtx.node !== dragMindMapCtx.mindmap.tree) {
|
|
detachMindmap(dragMindMapCtx.mindmap, dragMindMapCtx.node);
|
|
const mindmap = createFromTree(
|
|
dragMindMapCtx.node,
|
|
dragMindMapCtx.mindmap.style,
|
|
dragMindMapCtx.mindmap.layoutType,
|
|
this.gfx.surface!
|
|
);
|
|
|
|
mindmap.layout();
|
|
} else {
|
|
dragMindMapCtx.mindmap.layout();
|
|
}
|
|
}
|
|
}
|
|
|
|
hoveredCtx = null;
|
|
this._responseAreaUpdated.clear();
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create handlers that can translate entire mind map
|
|
*/
|
|
private _createTranslationHandlers(ctx: {
|
|
mindmaps: Set<MindmapElementModel>;
|
|
nodes: Set<GfxModel>;
|
|
}): {
|
|
onDragStart?: (context: ExtensionDragStartContext) => void;
|
|
onDragMove?: (context: ExtensionDragMoveContext) => void;
|
|
onDragEnd?: (context: ExtensionDragEndContext) => void;
|
|
} {
|
|
return {
|
|
onDragStart: () => {
|
|
ctx.nodes.forEach(node => {
|
|
node.stash('xywh');
|
|
});
|
|
},
|
|
onDragEnd: () => {
|
|
ctx.mindmaps.forEach(mindmap => {
|
|
mindmap.layout();
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
private _drawIndicator(options: {
|
|
targetMindMap: MindmapElementModel;
|
|
target: MindmapNode;
|
|
sourceMindMap: MindmapElementModel;
|
|
source: MindmapNode;
|
|
newParent: MindmapNode;
|
|
insertPosition:
|
|
| {
|
|
type: 'sibling';
|
|
layoutDir: Exclude<LayoutType, LayoutType.BALANCE>;
|
|
position: 'prev' | 'next';
|
|
}
|
|
| { type: 'child'; layoutDir: Exclude<LayoutType, LayoutType.BALANCE> };
|
|
path: number[];
|
|
}) {
|
|
const indicatorOverlay = this._indicatorOverlay;
|
|
|
|
if (!indicatorOverlay) {
|
|
return () => {};
|
|
}
|
|
|
|
// draw the indicator at given position
|
|
const { newParent, insertPosition, targetMindMap, target, source, path } =
|
|
options;
|
|
const children = newParent.children.filter(
|
|
node => node.element.id !== source.id
|
|
);
|
|
|
|
indicatorOverlay.setIndicatorInfo({
|
|
targetMindMap,
|
|
target,
|
|
parent: newParent,
|
|
insertPosition,
|
|
parentChildren: children,
|
|
path,
|
|
});
|
|
|
|
return () => {
|
|
indicatorOverlay.clear();
|
|
};
|
|
}
|
|
|
|
private _getHoveredMindMap(
|
|
position: IVec,
|
|
dragMindMapCtx: DragMindMapCtx
|
|
): MindmapElementModel | null {
|
|
const mindmap =
|
|
(this.gfx
|
|
.getElementByPoint(position[0], position[1], {
|
|
all: true,
|
|
responsePadding: [NODE_HORIZONTAL_SPACING, NODE_VERTICAL_SPACING * 2],
|
|
})
|
|
.find(el => {
|
|
if (!(el instanceof MindmapElementModel)) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
el === dragMindMapCtx.mindmap &&
|
|
!dragMindMapCtx.originalMindMapBound.containsPoint(position)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}) as MindmapElementModel) ?? null;
|
|
|
|
if (
|
|
mindmap &&
|
|
(!this._responseAreaUpdated.has(mindmap) || !mindmap.tree.responseArea)
|
|
) {
|
|
this._calcDragResponseArea(mindmap);
|
|
}
|
|
|
|
return mindmap;
|
|
}
|
|
|
|
private _setupDragNodeImage(
|
|
mindmapNode: MindmapNode,
|
|
pos: { x: number; y: number }
|
|
) {
|
|
const surfaceBlock = this.gfx.surfaceComponent as SurfaceBlockComponent;
|
|
const renderer = surfaceBlock?.renderer;
|
|
const indicatorOverlay = this._indicatorOverlay;
|
|
|
|
if (!renderer || !indicatorOverlay) {
|
|
return;
|
|
}
|
|
|
|
const nodeBound = mindmapNode.element.elementBound;
|
|
|
|
const canvas = renderer.getCanvasByBound(
|
|
mindmapNode.element.elementBound,
|
|
[mindmapNode.element],
|
|
undefined,
|
|
undefined,
|
|
false
|
|
);
|
|
|
|
indicatorOverlay.dragNodePos = [nodeBound.x - pos.x, nodeBound.y - pos.y];
|
|
indicatorOverlay.dragNodeImage = canvas;
|
|
|
|
return () => {
|
|
indicatorOverlay.dragNodeImage = null;
|
|
indicatorOverlay.currentDragPos = null;
|
|
};
|
|
}
|
|
|
|
private _updateNodeOpacity(
|
|
mindmap: MindmapElementModel,
|
|
mindNode: MindmapNode
|
|
) {
|
|
const OPACITY = 0.3;
|
|
const updatedNodes = new Set<
|
|
GfxPrimitiveElementModel | LocalConnectorElementModel
|
|
>();
|
|
const traverse = (node: MindmapNode, parent: MindmapNode | null) => {
|
|
node.element.opacity = OPACITY;
|
|
updatedNodes.add(node.element);
|
|
|
|
if (parent) {
|
|
const connectorId = `#${parent.element.id}-${node.element.id}`;
|
|
const connector = mindmap.connectors.get(connectorId);
|
|
|
|
if (connector) {
|
|
connector.opacity = OPACITY;
|
|
updatedNodes.add(connector);
|
|
}
|
|
}
|
|
|
|
if (node.children.length) {
|
|
node.children.forEach(child => traverse(child, node));
|
|
}
|
|
};
|
|
|
|
const parentNode = mindmap.getParentNode(mindNode.element.id) ?? null;
|
|
|
|
traverse(mindNode, parentNode);
|
|
|
|
return () => {
|
|
updatedNodes.forEach(el => {
|
|
el.opacity = 1;
|
|
});
|
|
};
|
|
}
|
|
|
|
override onDragInitialize(context: DragExtensionInitializeContext) {
|
|
if (isSingleMindMapNode(context.elements)) {
|
|
const mindmap = context.elements[0].group as MindmapElementModel;
|
|
const mindmapNode = mindmap.getNode(context.elements[0].id)!;
|
|
const mindmapBound = mindmap.elementBound;
|
|
const isRoot = mindmapNode === mindmap.tree;
|
|
|
|
mindmapBound.x -= NODE_HORIZONTAL_SPACING;
|
|
mindmapBound.y -= NODE_VERTICAL_SPACING * 2;
|
|
mindmapBound.w += NODE_HORIZONTAL_SPACING * 2;
|
|
mindmapBound.h += NODE_VERTICAL_SPACING * 4;
|
|
|
|
this._calcDragResponseArea(mindmap);
|
|
|
|
const clearDragStatus = isRoot
|
|
? mindmap.stashTree(mindmapNode)
|
|
: this._setupDragNodeImage(mindmapNode, context.dragStartPos);
|
|
const clearOpacity = this._updateNodeOpacity(mindmap, mindmapNode);
|
|
|
|
if (!isRoot) {
|
|
context.elements.splice(0, 1);
|
|
}
|
|
|
|
const mindMapDragCtx: DragMindMapCtx = {
|
|
mindmap,
|
|
node: mindmapNode,
|
|
isRoot,
|
|
originalMindMapBound: mindmapBound,
|
|
};
|
|
|
|
return {
|
|
...this._createManipulationHandlers(mindMapDragCtx),
|
|
clear() {
|
|
clearOpacity();
|
|
clearDragStatus?.();
|
|
if (!isRoot) {
|
|
context.elements.push(mindmapNode.element);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
const mindmapNodes = new Set<GfxModel>();
|
|
const mindmaps = new Set<MindmapElementModel>();
|
|
|
|
context.elements.forEach(el => {
|
|
if (isMindmapNode(el)) {
|
|
const mindmap =
|
|
el.group instanceof MindmapElementModel
|
|
? el.group
|
|
: (el as MindmapElementModel);
|
|
|
|
mindmaps.add(mindmap);
|
|
mindmap.childElements.forEach(child => mindmapNodes.add(child));
|
|
} else if (isGfxGroupCompatibleModel(el)) {
|
|
el.descendantElements.forEach(desc => {
|
|
if (desc.group instanceof MindmapElementModel) {
|
|
mindmaps.add(desc.group);
|
|
desc.group.childElements.forEach(_el => mindmapNodes.add(_el));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
if (mindmapNodes.size > 1) {
|
|
mindmapNodes.forEach(node => context.elements.push(node));
|
|
return this._createTranslationHandlers({
|
|
mindmaps,
|
|
nodes: mindmapNodes,
|
|
});
|
|
}
|
|
|
|
return {};
|
|
}
|
|
}
|