Skip to content

Commit ba84889

Browse files
authored
feat: Canvas pasting follows the mouse, fixing the problem of duplicate pasting of canvas pasting (#5151)
1 parent 58291cf commit ba84889

1 file changed

Lines changed: 150 additions & 1 deletion

File tree

ui/src/workflow/common/shortcut.ts

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,50 @@ import { MsgSuccess, MsgError, MsgConfirm } from '@/utils/message'
55
import { WorkflowType } from '@/enums/application'
66
import { t } from '@/locales'
77
import { copyClick } from '@/utils/clipboard'
8+
import { randomId } from '@/utils/common'
89
import { getMenuNodes, workflowModelDict } from './data'
10+
let activeCanvasId: string | null = null
11+
type Point = { x: number; y: number }
12+
const lastMouse = {
13+
x: 0,
14+
y: 0,
15+
hasValue: false,
16+
}
917
let selected: any | null = null
18+
const bindMousePosition = (lf: any) => {
19+
const updateMouse = (e: MouseEvent) => {
20+
lastMouse.x = e.clientX
21+
lastMouse.y = e.clientY
22+
lastMouse.hasValue = true
23+
}
24+
25+
// 推荐直接监听容器,这样鼠标在节点上移动也能拿到
26+
lf.container.addEventListener('mousemove', updateMouse)
27+
28+
return () => {
29+
lf.container.removeEventListener('mousemove', updateMouse)
30+
}
31+
}
32+
const bindCanvasActive = (lf: any) => {
33+
const container = lf.container as HTMLElement
34+
if (!container) return
1035

36+
// 让容器可聚焦
37+
container.tabIndex = 0
38+
39+
const activate = () => {
40+
activeCanvasId = lf.graphModel.flowId
41+
container.focus()
42+
}
43+
44+
container.addEventListener('mousedown', activate)
45+
container.addEventListener('focus', activate)
46+
47+
return () => {
48+
container.removeEventListener('mousedown', activate)
49+
container.removeEventListener('focus', activate)
50+
}
51+
}
1152
function translationNodeData(nodeData: any, distance: any) {
1253
nodeData.x += distance
1354
nodeData.y += distance
@@ -44,6 +85,8 @@ const TRANSLATION_DISTANCE = 40
4485
let CHILDREN_TRANSLATION_DISTANCE = 40
4586

4687
export function initDefaultShortcut(lf: LogicFlow, graph: GraphModel) {
88+
bindMousePosition(lf)
89+
bindCanvasActive(lf)
4790
const { keyboard } = lf
4891
const {
4992
options: { keyboard: keyboardOptions },
@@ -72,18 +115,124 @@ export function initDefaultShortcut(lf: LogicFlow, graph: GraphModel) {
72115
copyClick(JSON.stringify(selected))
73116
return false
74117
}
118+
// 3. 求节点包围盒
119+
const getBounds = (nodes: any[]) => {
120+
if (!nodes.length) {
121+
return { minX: 0, maxX: 0, minY: 0, maxY: 0 }
122+
}
123+
124+
let minX = nodes[0].x
125+
let maxX = nodes[0].x
126+
let minY = nodes[0].y
127+
let maxY = nodes[0].y
128+
129+
for (const node of nodes) {
130+
if (node.x < minX) minX = node.x
131+
if (node.x > maxX) maxX = node.x
132+
if (node.y < minY) minY = node.y
133+
if (node.y > maxY) maxY = node.y
134+
}
135+
136+
return { minX, maxX, minY, maxY }
137+
}
138+
139+
// 4. 整体平移
140+
const moveData = (data: any, dx: number, dy: number) => {
141+
for (const node of data.nodes ?? []) {
142+
node.x += dx
143+
node.y += dy
144+
}
145+
146+
for (const edge of data.edges ?? []) {
147+
if (edge.startPoint) {
148+
edge.startPoint.x += dx
149+
edge.startPoint.y += dy
150+
}
151+
if (edge.endPoint) {
152+
edge.endPoint.x += dx
153+
edge.endPoint.y += dy
154+
}
155+
if (edge.text && typeof edge.text.x === 'number' && typeof edge.text.y === 'number') {
156+
edge.text.x += dx
157+
edge.text.y += dy
158+
}
159+
if (Array.isArray(edge.pointsList)) {
160+
edge.pointsList = edge.pointsList.map((p: Point) => ({
161+
...p,
162+
x: p.x + dx,
163+
y: p.y + dy,
164+
}))
165+
}
166+
}
167+
}
168+
const resetData = (data: any) => {
169+
const idMap = new Map<string, string>()
170+
171+
const getOrCreateId = (oldId: string) => {
172+
let newId = idMap.get(oldId)
173+
if (!newId) {
174+
newId = randomId()
175+
idMap.set(oldId, newId)
176+
}
177+
return newId
178+
}
179+
180+
for (const node of data.nodes) {
181+
node.id = getOrCreateId(node.id)
182+
}
183+
184+
for (const edge of data.edges) {
185+
const oldEdgeId = edge.id
186+
const oldSourceNodeId = edge.sourceNodeId
187+
const oldTargetNodeId = edge.targetNodeId
188+
189+
edge.id = getOrCreateId(oldEdgeId)
190+
edge.sourceNodeId = getOrCreateId(oldSourceNodeId)
191+
edge.targetNodeId = getOrCreateId(oldTargetNodeId)
192+
193+
if (typeof edge.sourceAnchorId === 'string') {
194+
edge.sourceAnchorId = edge.sourceAnchorId.replace(oldSourceNodeId, edge.sourceNodeId)
195+
}
196+
197+
if (typeof edge.targetAnchorId === 'string') {
198+
edge.targetAnchorId = edge.targetAnchorId.replace(oldTargetNodeId, edge.targetNodeId)
199+
}
200+
}
201+
202+
return data
203+
}
75204

76205
const paste_node = async (e: ClipboardEvent) => {
206+
if (lf.graphModel.flowId !== activeCanvasId) {
207+
return true
208+
}
77209
if (!keyboardOptions?.enabled) return true
78210
if (graph.textEditElement) return true
79211
const text = e.clipboardData?.getData('text/plain') || ''
80212
const data = parseAndValidate(text)
81-
selected = data
213+
selected = resetData(data)
82214
const workflowMode = lf.graphModel.get_provide(null, null).workflowMode
83215
const menus = getMenuNodes(workflowMode)
84216
const nodes = menus?.flatMap((m: any) => m.list).map((n) => n.type)
85217

86218
if (selected && (selected.nodes || selected.edges)) {
219+
if (!lastMouse.hasValue) {
220+
moveData(data, 40, 40)
221+
} else {
222+
// LogicFlow 文档里 getPointByClient 会把页面坐标转成画布坐标
223+
const point = lf.graphModel.getPointByClient({
224+
x: lastMouse.x,
225+
y: lastMouse.y,
226+
})
227+
const mouseCanvasX = point.canvasOverlayPosition.x
228+
const mouseCanvasY = point.canvasOverlayPosition.y
229+
230+
const { minX, maxX, minY, maxY } = getBounds(selected.nodes)
231+
const centerX = (minX + maxX) / 2
232+
const centerY = (minY + maxY) / 2
233+
moveData(data, mouseCanvasX - centerX, mouseCanvasY - centerY)
234+
}
235+
87236
selected.nodes = selected.nodes.filter(
88237
(n: any) => nodes?.includes(n.type) || workflowModelDict[workflowMode](n),
89238
)

0 commit comments

Comments
 (0)