|
@@ -1,22 +1,163 @@
|
|
|
-import { Graph, Cell } from "@antv/x6";
|
|
|
-import { BorderSize, TopicType } from "@/enum";
|
|
|
-import { addTopic, updateTopic } from "@/pages/mindmap/mindMap";
|
|
|
-import { MindMapProjectInfo } from "@/types";
|
|
|
+import { Graph, Cell, Node, Edge } from "@antv/x6";
|
|
|
+import { BorderSize, StructureType, TopicType } from "@/enum";
|
|
|
+import {
|
|
|
+ addTopic,
|
|
|
+ getMindMapProjectByLocal,
|
|
|
+ updateTopic,
|
|
|
+} from "@/pages/mindmap/mindMap";
|
|
|
+import { MindMapProjectInfo, TopicItem } from "@/types";
|
|
|
+import { Dnd } from "@antv/x6-plugin-dnd";
|
|
|
+import { selectTopic } from "@/utils/mindmapHander";
|
|
|
+import { uuid } from "@/utils";
|
|
|
+
|
|
|
+enum positionType {
|
|
|
+ left = 'left',
|
|
|
+ right = 'right',
|
|
|
+ outside = 'outside',
|
|
|
+}
|
|
|
+// 拖拽主题时指示器
|
|
|
+let indicatorNode: Node | undefined;
|
|
|
+let indicatorEdge: Edge | undefined;
|
|
|
+// 插入的节点位置
|
|
|
+let insertNode: Node | undefined;
|
|
|
+let moveStart: { x: number; y: number } | undefined;
|
|
|
+let dragging = false;
|
|
|
+let currentShadowNode: Node | undefined;
|
|
|
|
|
|
export const bindMindMapEvents = (
|
|
|
graph: Graph,
|
|
|
mindProjectInfo?: MindMapProjectInfo,
|
|
|
setMindProjectInfo?: (info: MindMapProjectInfo) => void,
|
|
|
- setSelectedCell?: (cell: Cell[]) => void
|
|
|
+ setSelectedCell?: (cell: Cell[]) => void,
|
|
|
+ dndRef?: React.MutableRefObject<Dnd | undefined>
|
|
|
) => {
|
|
|
- // 选中的节点/边发生改变(增删)时触发
|
|
|
- graph.on('selection:changed', ({selected}: {selected: Cell[];}) => {
|
|
|
+ graph.on("selection:changed", ({ selected }: { selected: Cell[] }) => {
|
|
|
setSelectedCell?.(selected);
|
|
|
- })
|
|
|
+ });
|
|
|
+
|
|
|
+ graph.on("node:mouseenter", (args) => {
|
|
|
+ if (args.node.data?.type !== TopicType.main) {
|
|
|
+ graph.disablePanning();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ graph.on("node:mouseleave", (args) => {
|
|
|
+ graph.enablePanning();
|
|
|
+ });
|
|
|
+
|
|
|
+ /*********************************** 拖拽开始 *********************************/
|
|
|
+ graph.on("node:mousedown", (args) => {
|
|
|
+ moveStart = {
|
|
|
+ x: args.x,
|
|
|
+ y: args.y,
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ graph.on("node:mousemove", (args) => {
|
|
|
+ if (!args.node.data?.parentId || args.node.data?.shadow) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (
|
|
|
+ !dragging &&
|
|
|
+ moveStart &&
|
|
|
+ (Math.abs(args.x - moveStart.x) > 5 || Math.abs(args.y - moveStart.y) > 5)
|
|
|
+ ) {
|
|
|
+ // 判断开始点是否有值,有值时当拖拽距离>5时开启影子节点
|
|
|
+ dragging = true;
|
|
|
+ const node = args.node;
|
|
|
+ const shadowNode = graph.createNode({
|
|
|
+ shape: "rect",
|
|
|
+ width: node.size().width,
|
|
|
+ height: node.size().height,
|
|
|
+ x: args.x,
|
|
|
+ y: args.y,
|
|
|
+ label: node.data?.label,
|
|
|
+ attrs: {
|
|
|
+ body: {
|
|
|
+ rx: node.data?.borderSize,
|
|
|
+ ry: node.data?.borderSize,
|
|
|
+ stroke: "#666",
|
|
|
+ strokeWidth: 1,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ ...node.data,
|
|
|
+ shadow: true,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ setShadowMode(true, node, graph);
|
|
|
+ currentShadowNode = shadowNode;
|
|
|
+ dndRef?.current?.init();
|
|
|
+ dndRef?.current?.start(shadowNode, args.e as unknown as MouseEvent);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ graph.on("node:mousemove", (args) => {
|
|
|
+ // 节点拖拽-处理影子节点吸附问题
|
|
|
+ if (currentShadowNode && setMindProjectInfo) {
|
|
|
+ topicDragHander(
|
|
|
+ graph,
|
|
|
+ setMindProjectInfo,
|
|
|
+ { x: args.x, y: args.y },
|
|
|
+ args.node
|
|
|
+ );
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ graph.on("node:mouseup", (args) => {
|
|
|
+ // 拖拽结束
|
|
|
+ if (indicatorNode && insertNode) {
|
|
|
+ graph.removeCell(args.node.id + '-edge');
|
|
|
+ setMindProjectInfo && handleSwitchPosition(setMindProjectInfo, args.node.id, insertNode.id);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 成为自由节点
|
|
|
+ if(currentShadowNode && !indicatorNode && args.node.data?.parentId) {
|
|
|
+ setMindProjectInfo && handleSwitchPosition(setMindProjectInfo, args.node.id, undefined, { x: args.x, y: args.y});
|
|
|
+ }
|
|
|
+
|
|
|
+ currentShadowNode && setShadowMode(false, args.node, graph);
|
|
|
+ moveStart = undefined;
|
|
|
+ dragging = false;
|
|
|
+ currentShadowNode = undefined;
|
|
|
+ dndRef?.current?.remove();
|
|
|
+ if (indicatorEdge && indicatorNode) {
|
|
|
+ graph.removeCells([indicatorEdge, indicatorNode]);
|
|
|
+ indicatorEdge = undefined;
|
|
|
+ indicatorNode = undefined;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ graph.on("node:move", (args) => {
|
|
|
+ // 自由节点拖拽
|
|
|
+ if(!args.node.data?.parentId) {
|
|
|
+ setShadowMode(true, args.node, graph);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ graph.on("node:moving", (args) => {
|
|
|
+ if(!args.node.data?.parentId && setMindProjectInfo) {
|
|
|
+ setShadowMode(false, args.node, graph);
|
|
|
+ topicDragHander(
|
|
|
+ graph,
|
|
|
+ setMindProjectInfo,
|
|
|
+ { x: args.x, y: args.y },
|
|
|
+ args.node
|
|
|
+ );
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ graph.on("node:moved", (args) => {
|
|
|
+ if(!args.node.data?.parentId) {
|
|
|
+ setShadowMode(false, args.node, graph);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ /*********************************** 拖拽结束 *********************************/
|
|
|
|
|
|
// 双击画布空白-新增自由主题
|
|
|
graph.on("blank:dblclick", (args) => {
|
|
|
- if(setMindProjectInfo) {
|
|
|
+ if (setMindProjectInfo) {
|
|
|
const topic = addTopic(TopicType.branch, setMindProjectInfo, undefined, {
|
|
|
x: args.x,
|
|
|
y: args.y,
|
|
@@ -26,7 +167,7 @@ export const bindMindMapEvents = (
|
|
|
borderSize: BorderSize.medium,
|
|
|
});
|
|
|
|
|
|
- graph.resetSelection(topic?.id)
|
|
|
+ selectTopic(graph, topic);
|
|
|
}
|
|
|
});
|
|
|
|
|
@@ -34,23 +175,369 @@ export const bindMindMapEvents = (
|
|
|
graph.on("node:change:*", (args) => {
|
|
|
// console.log("node:change:*", args);
|
|
|
const { current, previous } = args;
|
|
|
- if(args.key === "data") {
|
|
|
+ if (args.key === "data") {
|
|
|
// 收折子项
|
|
|
if (current.collapsed !== previous.collapsed) {
|
|
|
- setMindProjectInfo && updateTopic(args.cell.id, { collapsed: current.collapsed }, setMindProjectInfo);
|
|
|
+ setMindProjectInfo &&
|
|
|
+ updateTopic(
|
|
|
+ args.cell.id,
|
|
|
+ { collapsed: current.collapsed },
|
|
|
+ setMindProjectInfo
|
|
|
+ );
|
|
|
} else {
|
|
|
updateTopic(args.cell.id, current, (info) => {
|
|
|
localStorage.setItem("minMapProjectInfo", JSON.stringify(info));
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
- if(args.key === "size") {
|
|
|
- updateTopic(args.cell.id, {
|
|
|
- width: current.width,
|
|
|
- height: current.height
|
|
|
- }, (info) => {
|
|
|
- localStorage.setItem("minMapProjectInfo", JSON.stringify(info));
|
|
|
- });
|
|
|
+ if (args.key === "size") {
|
|
|
+ updateTopic(
|
|
|
+ args.cell.id,
|
|
|
+ {
|
|
|
+ width: current.width,
|
|
|
+ height: current.height,
|
|
|
+ },
|
|
|
+ (info) => {
|
|
|
+ localStorage.setItem("minMapProjectInfo", JSON.stringify(info));
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 遍历主题树
|
|
|
+ * @param topics
|
|
|
+ * @param callback
|
|
|
+ * @returns
|
|
|
+ */
|
|
|
+const traverseNode = (
|
|
|
+ topics: TopicItem[],
|
|
|
+ callback: (topic: TopicItem, index: number) => void
|
|
|
+): TopicItem[] => {
|
|
|
+ return topics.map((topic, index) => {
|
|
|
+ callback && callback(topic, index);
|
|
|
+ if (topic.children?.length) {
|
|
|
+ topic.children = traverseNode(topic.children, callback);
|
|
|
}
|
|
|
+ return topic;
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 判断当前点在主图的位置
|
|
|
+const atNodePosition = (
|
|
|
+ position: { x: number; y: number },
|
|
|
+ x: number,
|
|
|
+ y: number,
|
|
|
+ width: number,
|
|
|
+ height: number
|
|
|
+): positionType => {
|
|
|
+ if (
|
|
|
+ position.x < x + width / 2 &&
|
|
|
+ position.x > x &&
|
|
|
+ position.y < y + height &&
|
|
|
+ position.y > y
|
|
|
+ ) {
|
|
|
+ return positionType.left;
|
|
|
+ }
|
|
|
+ if (
|
|
|
+ position.x < x + width &&
|
|
|
+ position.x > x + width / 2 &&
|
|
|
+ position.y < y + height &&
|
|
|
+ position.y > y
|
|
|
+ ) {
|
|
|
+ return positionType.right;
|
|
|
+ }
|
|
|
+ return positionType.outside;
|
|
|
+};
|
|
|
+
|
|
|
+// 创建一个指示器
|
|
|
+const addIndicator = (
|
|
|
+ x: number,
|
|
|
+ y: number,
|
|
|
+ graph: Graph,
|
|
|
+ targetNode: Node,
|
|
|
+ atPosition: positionType
|
|
|
+) => {
|
|
|
+ if (indicatorEdge && indicatorNode) {
|
|
|
+ graph.removeCells([indicatorEdge, indicatorNode]);
|
|
|
+ }
|
|
|
+ indicatorNode = graph.addNode({
|
|
|
+ shape: "rect",
|
|
|
+ width: 100,
|
|
|
+ height: 26,
|
|
|
+ x,
|
|
|
+ y,
|
|
|
+ attrs: {
|
|
|
+ body: {
|
|
|
+ fill: "#fecb99",
|
|
|
+ stroke: "#fc7e00",
|
|
|
+ strokeWidth: 1,
|
|
|
+ rx: 2,
|
|
|
+ ry: 2,
|
|
|
+ style: {
|
|
|
+ opacity: 0.6
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ indicatorEdge = graph.addEdge({
|
|
|
+ source: {
|
|
|
+ cell: targetNode.id,
|
|
|
+ anchor: {
|
|
|
+ name: atPosition === positionType.left ? "left" : "right",
|
|
|
+ args: {
|
|
|
+ dx: atPosition === positionType.left ? 3 : -3,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ target: {
|
|
|
+ cell: indicatorNode.id,
|
|
|
+ anchor: {
|
|
|
+ name: atPosition === positionType.left ? "right" : "left",
|
|
|
+ args: {
|
|
|
+ dx: atPosition === positionType.left ? -3 : 3,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ connector: "normal",
|
|
|
+ attrs: {
|
|
|
+ line: {
|
|
|
+ stroke: "#fc7e00",
|
|
|
+ strokeWidth: 2,
|
|
|
+ sourceMarker: {
|
|
|
+ name: "",
|
|
|
+ },
|
|
|
+ targetMarker: {
|
|
|
+ name: "",
|
|
|
+ },
|
|
|
+ style: {
|
|
|
+ opacity: 0.6
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 添加指示器
|
|
|
+const setIndicator = (
|
|
|
+ atPosition: positionType,
|
|
|
+ targetNode: Node,
|
|
|
+ graph: Graph
|
|
|
+) => {
|
|
|
+ switch (atPosition) {
|
|
|
+ // 1、左侧位置
|
|
|
+ case positionType.left: {
|
|
|
+ const x = targetNode.position().x - 120;
|
|
|
+ const y = targetNode.position().y + targetNode.size().height / 2 - 13;
|
|
|
+ // 判断是左侧节点还是右侧节点
|
|
|
+ if (targetNode.data?.parentId) {
|
|
|
+ const parentNode = graph.getCellById(targetNode.data.parentId);
|
|
|
+ // 左侧朝向的结构
|
|
|
+ if (
|
|
|
+ parentNode?.isNode() &&
|
|
|
+ targetNode.position().x < parentNode.position().x
|
|
|
+ ) {
|
|
|
+ addIndicator(x, y, graph, targetNode, atPosition);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const mindProjectInfo = getMindMapProjectByLocal();
|
|
|
+ if (mindProjectInfo?.structure === StructureType.left) {
|
|
|
+ addIndicator(x, y, graph, targetNode, atPosition);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ // 2、右侧位置
|
|
|
+ case positionType.right: {
|
|
|
+ const x = targetNode.position().x + targetNode.size().width + 20;
|
|
|
+ const y = targetNode.position().y + targetNode.size().height / 2 - 13;
|
|
|
+ // 判断是左侧节点还是右侧节点
|
|
|
+ if (targetNode.data?.parentId) {
|
|
|
+ const parentNode = graph.getCellById(targetNode.data.parentId);
|
|
|
+ // 右侧朝向的结构
|
|
|
+ if (
|
|
|
+ parentNode?.isNode() &&
|
|
|
+ targetNode.position().x > parentNode.position().x
|
|
|
+ ) {
|
|
|
+ addIndicator(x, y, graph, targetNode, atPosition);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const mindProjectInfo = getMindMapProjectByLocal();
|
|
|
+ if (mindProjectInfo?.structure === StructureType.right) {
|
|
|
+ addIndicator(x, y, graph, targetNode, atPosition);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ // 外部位置
|
|
|
+ case positionType.outside: {
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 判断目标节点是不是后代节点
|
|
|
+const isDescendantNode = (targetNode: Node, originNode: Node, graph: Graph) => {
|
|
|
+ if (targetNode.data?.parentId === originNode.id) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ let isChild = false; // 是后代节点
|
|
|
+ const findParent = (parentId: string) => {
|
|
|
+ const cell = graph.getCellById(parentId);
|
|
|
+ if (cell?.isNode() && cell.data.parentId) {
|
|
|
+ if (cell.data.parentId === originNode.id) {
|
|
|
+ isChild = true;
|
|
|
+ } else {
|
|
|
+ findParent(cell.data.parentId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ targetNode.data?.parentId && findParent(targetNode.data.parentId);
|
|
|
+
|
|
|
+ return isChild;
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 主题拖拽放置
|
|
|
+ * @param graph
|
|
|
+ * @param setMindProjectInfo
|
|
|
+ */
|
|
|
+export const topicDragHander = (
|
|
|
+ graph: Graph,
|
|
|
+ setMindProjectInfo: (info: MindMapProjectInfo) => void,
|
|
|
+ position: { x: number; y: number },
|
|
|
+ originNode: Node
|
|
|
+) => {
|
|
|
+ if (indicatorEdge) graph.removeCell(indicatorEdge);
|
|
|
+ if (indicatorNode) graph.removeCell(indicatorNode);
|
|
|
+ indicatorEdge = undefined;
|
|
|
+ indicatorNode = undefined;
|
|
|
+ // 1、 获取位置的地方附件是否有节点
|
|
|
+ const nodes = graph.getNodes();
|
|
|
+ nodes.forEach((targetNode) => {
|
|
|
+ // 目标节点是自己、自己的后代、节点不做处理
|
|
|
+ if (
|
|
|
+ targetNode.id === originNode.id ||
|
|
|
+ isDescendantNode(targetNode, originNode, graph)
|
|
|
+ ) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const { x, y } = targetNode.position();
|
|
|
+ const { width, height } = targetNode.size();
|
|
|
+ // 2、 找到是在节点哪个区域内
|
|
|
+ const atPosition = atNodePosition(position, x, y, width, height);
|
|
|
+ // 3、添加插入指示
|
|
|
+ setIndicator(atPosition, targetNode, graph);
|
|
|
+ // 4、 根据位置确定插入位置
|
|
|
+ if ([positionType.left, positionType.right].includes(atPosition)) {
|
|
|
+ insertNode = targetNode;
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 拖拽完毕,切换主题位置
|
|
|
+ * @param setMindProjectInfo
|
|
|
+ * @param sourceId 拖拽的节点
|
|
|
+ * @param targetId 放置的节点
|
|
|
+ * @param position 自由节点放置的位置
|
|
|
+ * @returns
|
|
|
+ */
|
|
|
+const handleSwitchPosition = (
|
|
|
+ setMindProjectInfo: (info: MindMapProjectInfo) => void,
|
|
|
+ sourceId: string,
|
|
|
+ targetId?: string,
|
|
|
+ position?: { x: number; y: number }
|
|
|
+) => {
|
|
|
+ const mindmapProjectInfo = getMindMapProjectByLocal();
|
|
|
+ if(!mindmapProjectInfo) return;
|
|
|
+
|
|
|
+ // 找到要拖拽的节点并删除
|
|
|
+ let source: TopicItem | undefined;
|
|
|
+ mindmapProjectInfo.topics.forEach(topic => {
|
|
|
+ if(topic.id === sourceId) {
|
|
|
+ source = topic;
|
|
|
+ };
|
|
|
+ mindmapProjectInfo.topics = mindmapProjectInfo.topics.filter(item => item.id !== sourceId);
|
|
|
})
|
|
|
+ const topics = source
|
|
|
+ ? mindmapProjectInfo.topics
|
|
|
+ :traverseNode(mindmapProjectInfo.topics, (topic) => {
|
|
|
+ const findItem = topic?.children?.find(item => item.id === sourceId);
|
|
|
+ if(findItem) {
|
|
|
+ source = findItem;
|
|
|
+ topic.children = topic.children?.filter(item => item.id !== sourceId);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if(!source) return;
|
|
|
+
|
|
|
+ if (targetId) {
|
|
|
+ // 加入到目标节点下
|
|
|
+ mindmapProjectInfo.topics = traverseNode(topics, (topic) => {
|
|
|
+ if(topic.id === targetId) {
|
|
|
+ if(!topic.children) {
|
|
|
+ topic.children = [];
|
|
|
+ }
|
|
|
+ source && topic.children.push({
|
|
|
+ ...source,
|
|
|
+ type: topic.type === TopicType.main ? TopicType.branch : TopicType.sub,
|
|
|
+ parentId: topic.id
|
|
|
+ })
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ const id = uuid();
|
|
|
+ // 成为自由节点
|
|
|
+ mindmapProjectInfo.topics.push({
|
|
|
+ ...source,
|
|
|
+ id,
|
|
|
+ children: (source.children || []).map((item) => {
|
|
|
+ item.parentId = id;
|
|
|
+ return item;
|
|
|
+ }),
|
|
|
+ opacity: 100,
|
|
|
+ x: position?.x,
|
|
|
+ y: position?.y,
|
|
|
+ type: TopicType.branch,
|
|
|
+ parentId: null
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ setMindProjectInfo(mindmapProjectInfo);
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 设置影子模式-拖拽时变灰
|
|
|
+ * @param node
|
|
|
+ * @param graph
|
|
|
+ */
|
|
|
+export const setShadowMode = (enable: boolean, node: Node, graph: Graph) => {
|
|
|
+ const data = node.getData();
|
|
|
+ node.setData({
|
|
|
+ opacity: enable ? 60 : 100,
|
|
|
+ });
|
|
|
+
|
|
|
+ if (data?.children?.length) {
|
|
|
+ traverseNode(data.children, (topic) => {
|
|
|
+ const cell = graph.getCellById(topic.id);
|
|
|
+ const edge = graph.getCellById(topic.id + "-edge");
|
|
|
+ if (cell && cell.isNode()) {
|
|
|
+ cell.setData({
|
|
|
+ opacity: enable ? 60 : 100,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (edge && edge.isEdge()) {
|
|
|
+ edge.setAttrs({
|
|
|
+ line: {
|
|
|
+ style: {
|
|
|
+ opacity: enable ? 0.6 : 1,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
};
|