Skip to content

Commit 17d2cb4

Browse files
authored
Merge pull request #1709 from alibaba/xflow-fix-copy
Xflow fix 快捷键复制粘贴
2 parents 6e432d4 + b13e472 commit 17d2cb4

File tree

4 files changed

+81
-133
lines changed

4 files changed

+81
-133
lines changed

docs/xflow/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ Handle 配置继承自 React Flow 的 Handle 配置,用于控制节点连接
171171
| getSettingSchema | 动态获取节点的业务配置信息,返回值同settingSchema。同时设置`settingSchema``getSettingSchema`只生效`getSettingSchema` | `(nodeId: string, nodeType: string, nodeItem: TNodeItem, nodeData: any, form: ReturnType<typeof useForm>) => Promise<Schema>` | |
172172
| renderHandle | 自定义渲染节点的handle,以实现某个节点自定义的出口数量.`sourceHandle`是原handle组件 | `(sourceHandle: SourceHandleType,sourceHandleProps:ComponentProps<SourceHandleType>,nodeProps: {id: string;type: string;data: any;layout: 'LR';isConnectable: boolean;readOnly: boolean;}) => React.JSX.Element`| |
173173
| disabledShortcutDelete | 是否禁用该节点的快捷键删除功能 | `boolean` | false |
174+
| disabledShortcutCopy | 是否禁用该节点的快捷键复制功能 | `boolean` | false |
174175

175176

176177
## TNodeSelector

docs/xflow/demo/quickStart/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export default () => {
88
type: 'Start',
99
hidden: true,
1010
disabledShortcutDelete: true,
11+
disabledShortcutCopy: true,
1112
targetHandleHidden: true,
1213
icon: {
1314
type: 'icon-start',

packages/x-flow/src/XFlow.tsx

Lines changed: 78 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
import '@xyflow/react/dist/style.css';
99
import { useEventListener, useMemoizedFn } from 'ahooks';
1010
import { produce, setAutoFreeze } from 'immer';
11-
import { debounce, isFunction } from 'lodash';
11+
import { isFunction, isString } from 'lodash';
1212
import type { FC } from 'react';
1313
import React, {
1414
memo,
@@ -87,7 +87,8 @@ const XFlow: FC<FlowProps> = memo(props => {
8787
);
8888
const { record } = useTemporalStore();
8989
const [activeNode, setActiveNode] = useState<any>(null);
90-
const { settingMap, globalConfig, readOnly, logPanel } = useContext(ConfigContext);
90+
const { settingMap, globalConfig, readOnly, logPanel } =
91+
useContext(ConfigContext);
9192
const [openPanel, setOpenPanel] = useState<boolean>(true);
9293
const [openLogPanel, setOpenLogPanel] = useState<boolean>(true);
9394
const {
@@ -101,8 +102,6 @@ const XFlow: FC<FlowProps> = memo(props => {
101102
const nodeEditorRef = useRef(null);
102103
const { copyNode, pasteNodeSimple } = useFlow();
103104
const { undo, redo } = useTemporalStore();
104-
const isNodeCopyingRef = useRef(false); // 是否正在进行节点复制
105-
const lastClickedNodeRef = useRef(false); // 最后一次点击是否是节点
106105

107106
useEffect(() => {
108107
zoomTo(0.8);
@@ -116,7 +115,7 @@ const XFlow: FC<FlowProps> = memo(props => {
116115
};
117116
}, []);
118117

119-
useEventListener('keydown', e => {
118+
const handleKeyDown = useMemoizedFn((e: KeyboardEvent) => {
120119
if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey))
121120
e.preventDefault();
122121
if ((e.key === 'z' || e.key === 'Z') && (e.ctrlKey || e.metaKey)) {
@@ -130,74 +129,53 @@ const XFlow: FC<FlowProps> = memo(props => {
130129
if ((e.key === 's' || e.key === 'S') && (e.ctrlKey || e.metaKey))
131130
e.preventDefault();
132131
if ((e.key === 'c' || e.key === 'C') && (e.ctrlKey || e.metaKey)) {
133-
const selectedNode = nodes?.find(node => node.selected);
134-
// 获取当前选中的文本(非节点的内容)
135-
const selectedText = window.getSelection()?.toString();
136-
// 如果最后点击的是节点 且 有节点被选中 则 复制节点
137-
if (selectedNode && lastClickedNodeRef.current) {
138-
// 复制节点
139-
isNodeCopyingRef.current = true; // 标记为节点复制
140-
copyNode(selectedNode.id);
141-
e.preventDefault();
132+
const latestNodes = storeApi.getState().nodes;
133+
let isNodeCopyEvent = false;
134+
if (e.target instanceof HTMLElement) {
135+
const target = e.target as HTMLElement;
136+
if (
137+
(isString(target.tagName) &&
138+
target.tagName.toLowerCase() === 'body') ||
139+
(target.tagName.toLowerCase() === 'div' &&
140+
target.classList &&
141+
isFunction(target.classList.contains) &&
142+
(target.classList.contains('ant-drawer') ||
143+
target.classList.contains('react-flow__node') ||
144+
target.id === 'xflow-container'))
145+
) {
146+
isNodeCopyEvent = true;
147+
}
148+
}
149+
const selectedNode = latestNodes?.find(node => node.selected);
142150

143-
// 清空系统剪贴板,确保粘贴时使用节点而非之前的文本
144-
try {
145-
navigator.clipboard.writeText('').catch(() => {});
146-
} catch (err) {}
147-
} else if (selectedText) {
148-
// 复制文本
149-
// 清除之前的节点复制状态
150-
const { copyNodes, copyTimeoutId } = storeApi.getState();
151-
if (copyNodes?.length > 0) {
152-
if (copyTimeoutId) {
153-
clearTimeout(copyTimeoutId);
151+
if (isNodeCopyEvent && selectedNode?.id) {
152+
const nodeType = selectedNode?.data?._nodeType;
153+
if (isString(nodeType) && nodeType) {
154+
const nodeConfig = settingMap[nodeType];
155+
if (nodeConfig?.disabledShortcutCopy) {
156+
message.warning(
157+
`${selectedNode.data?.title || selectedNode.id}节点不允许复制`
158+
);
159+
return;
154160
}
155-
storeApi.setState({
156-
copyNodes: [],
157-
copyTimeoutId: null,
158-
isAddingNode: false,
159-
});
160161
}
162+
// 复制节点
163+
e.preventDefault();
164+
copyNode(selectedNode.id);
161165
}
162166
} else if ((e.key === 'v' || e.key === 'V') && (e.ctrlKey || e.metaKey)) {
163167
const { copyNodes } = storeApi.getState();
164-
// 只有在有节点复制状态时才拦截粘贴操作
165168
if (copyNodes?.length > 0) {
166-
pasteNodeSimple();
167169
e.preventDefault();
170+
pasteNodeSimple();
168171
}
169-
} else if (copyNodes.length > 0) {
170-
// 只在有复制节点时才检查其他操作
171-
const { copyTimeoutId } = storeApi.getState();
172-
if (copyTimeoutId) {
173-
clearTimeout(copyTimeoutId);
174-
storeApi.setState({
175-
copyTimeoutId: null,
176-
isAddingNode: false,
177-
});
178-
}
179-
} else if (e.key === 'Escape'){
180-
setOpenPanel(false)
172+
} else if (e.key === 'Escape') {
173+
setOpenPanel(false);
174+
workflowContainerRef.current?.focus();
181175
}
182176
});
183-
184-
// 添加 copy 事件监听,获取实际复制的内容
185-
useEventListener('copy', (e: ClipboardEvent) => {
186-
if (!isNodeCopyingRef.current) {
187-
// 清除节点复制状态,因为用户复制了其他内容
188-
const { copyNodes, copyTimeoutId } = storeApi.getState();
189-
if (copyNodes?.length > 0) {
190-
if (copyTimeoutId) {
191-
clearTimeout(copyTimeoutId);
192-
}
193-
storeApi.setState({
194-
copyNodes: [],
195-
copyTimeoutId: null,
196-
isAddingNode: false,
197-
});
198-
}
199-
}
200-
isNodeCopyingRef.current = false;
177+
useEventListener('keydown', handleKeyDown, {
178+
target: workflowContainerRef,
201179
});
202180

203181
useEventListener(
@@ -221,28 +199,6 @@ const XFlow: FC<FlowProps> = memo(props => {
221199
}
222200
);
223201

224-
// 当点击非节点区域时重置标记
225-
useEventListener('mousedown', (e: MouseEvent) => {
226-
const target = e.target as HTMLElement;
227-
const isClickingNode =
228-
target.closest('.xflow-node-container') ||
229-
target.closest('.candidate-node');
230-
// 如果点击的不是节点,重置标记
231-
if (!isClickingNode) {
232-
lastClickedNodeRef.current = false;
233-
234-
// 清除复制状态
235-
const { copyTimeoutId, copyNodes } = storeApi.getState();
236-
if (copyTimeoutId && copyNodes?.length > 0) {
237-
clearTimeout(copyTimeoutId);
238-
storeApi.setState({
239-
copyTimeoutId: null,
240-
isAddingNode: false,
241-
});
242-
}
243-
}
244-
});
245-
246202
const { eventEmitter } = useEventEmitterContextContext();
247203
eventEmitter?.useSubscription((v: any) => {
248204
// 整理画布
@@ -278,28 +234,6 @@ const XFlow: FC<FlowProps> = memo(props => {
278234
setCandidateNode(newNode);
279235
};
280236

281-
// 插入节点
282-
// const handleInsertNode = () => {
283-
// const newNode = {
284-
// id: uuid(),
285-
// data: { label: 'new node' },
286-
// position: {
287-
// x: 0,
288-
// y: 0,
289-
// },
290-
// };
291-
// addNodes(newNode);
292-
// addEdges({
293-
// id: uuid(),
294-
// source: '2',
295-
// target: newNode.id,
296-
// });
297-
// const targetEdge = edges.find(edge => edge.source === '2');
298-
// updateEdge(targetEdge?.id as string, {
299-
// source: newNode.id,
300-
// });
301-
// };
302-
303237
// edge 移入/移出效果
304238
const getUpdateEdgeConfig = useMemoizedFn((edge: any, color: string) => {
305239
const newEdges = produce(edges, draft => {
@@ -323,7 +257,6 @@ const XFlow: FC<FlowProps> = memo(props => {
323257
const { _nodeType, _status, ...restData } = data || {};
324258
const nodeSetting = settingMap[_nodeType] || {};
325259
const showPanel = nodeSetting?.nodePanel?.showPanel ?? true;
326-
327260
return (
328261
<CustomNode
329262
{...rest}
@@ -333,9 +266,6 @@ const XFlow: FC<FlowProps> = memo(props => {
333266
layout={layout}
334267
status={_status}
335268
onClick={async e => {
336-
// 记录用户点击了节点
337-
lastClickedNodeRef.current = true;
338-
339269
if (nodeEditorRef?.current?.validateForm) {
340270
const result = await nodeEditorRef?.current?.validateForm();
341271
if (!result) {
@@ -356,7 +286,7 @@ const XFlow: FC<FlowProps> = memo(props => {
356286
}
357287
setOpenLogPanel(true);
358288
}}
359-
onDelete={()=>{
289+
onDelete={() => {
360290
// 删除节点并关闭弹窗
361291
setActiveNode(null);
362292
}}
@@ -400,7 +330,14 @@ const XFlow: FC<FlowProps> = memo(props => {
400330
const panelonClose = globalConfig?.nodePanel?.onClose;
401331

402332
return (
403-
<div id="xflow-container" ref={workflowContainerRef}>
333+
<div
334+
id="xflow-container"
335+
ref={workflowContainerRef}
336+
tabIndex={0}
337+
onMouseDown={() => {
338+
workflowContainerRef.current?.focus();
339+
}}
340+
>
404341
<ReactFlow
405342
panOnDrag={panOnDrag}
406343
nodeTypes={nodeTypes}
@@ -424,7 +361,7 @@ const XFlow: FC<FlowProps> = memo(props => {
424361
},
425362
deletable: deletable, //默认连线属性受此项控制
426363
}}
427-
onBeforeDelete={async (elements) => {
364+
onBeforeDelete={async elements => {
428365
if (readOnly) {
429366
return false;
430367
}
@@ -436,10 +373,14 @@ const XFlow: FC<FlowProps> = memo(props => {
436373
: false;
437374
});
438375
if (blockedNodes?.length > 0) {
439-
message.warning(`${blockedNodes.map(n => n.data?.title || n.id).join(', ')}节点不允许删除!`);
376+
message.warning(
377+
`${blockedNodes
378+
.map(n => n.data?.title || n.id)
379+
.join(', ')}节点不允许删除!`
380+
);
440381
return false;
441382
}
442-
return true
383+
return true;
443384
}}
444385
onConnect={onConnect}
445386
onNodesChange={changes => {
@@ -465,24 +406,24 @@ const XFlow: FC<FlowProps> = memo(props => {
465406
});
466407
}}
467408
onEdgeMouseEnter={(_, edge: any) => {
468-
if(!edge.style.stroke || edge.style.stroke === '#c9c9c9'){
409+
if (!edge.style.stroke || edge.style.stroke === '#c9c9c9') {
469410
getUpdateEdgeConfig(edge, '#2970ff');
470411
}
471412
}}
472413
onEdgeMouseLeave={(_, edge) => {
473-
if(['#2970ff',"#c9c9c9"].includes(edge.style.stroke)){
414+
if (['#2970ff', '#c9c9c9'].includes(edge.style.stroke)) {
474415
getUpdateEdgeConfig(edge, '#c9c9c9');
475416
}
476417
}}
477418
onNodesDelete={() => {
478-
setActiveNode(null);
419+
setActiveNode(null);
479420
}}
480421
onNodeClick={(event, node) => {
481422
onNodeClick && onNodeClick(event, node);
482423
}}
483424
deleteKeyCode={globalConfig?.deleteKeyCode}
484-
onEdgeClick={(event,edge)=>{
485-
onEdgeClick && onEdgeClick(event,edge)
425+
onEdgeClick={(event, edge) => {
426+
onEdgeClick && onEdgeClick(event, edge);
486427
}}
487428
>
488429
<CandidateNode />
@@ -504,6 +445,7 @@ const XFlow: FC<FlowProps> = memo(props => {
504445
return;
505446
}
506447
setOpenPanel(false);
448+
workflowContainerRef.current?.focus();
507449

508450
// 如果日志面板关闭
509451
if (!isTruthy(activeNode?._status) || !openLogPanel) {
@@ -520,19 +462,22 @@ const XFlow: FC<FlowProps> = memo(props => {
520462
{NodeEditorWrap}
521463
</PanelContainer>
522464
)}
523-
{isTruthy(activeNode?._status) && openLogPanel && Boolean(logPanel?.enable ?? true) && (
524-
<PanelStatusLogContainer
525-
id={activeNode?.id}
526-
nodeType={activeNode?._nodeType}
527-
onClose={() => {
528-
setOpenLogPanel(false);
529-
!openPanel && setActiveNode(null);
530-
}}
531-
data={activeNode?.values}
532-
>
533-
{NodeLogWrap}
534-
</PanelStatusLogContainer>
535-
)}
465+
{isTruthy(activeNode?._status) &&
466+
openLogPanel &&
467+
Boolean(logPanel?.enable ?? true) && (
468+
<PanelStatusLogContainer
469+
id={activeNode?.id}
470+
nodeType={activeNode?._nodeType}
471+
onClose={() => {
472+
setOpenLogPanel(false);
473+
!openPanel && setActiveNode(null);
474+
workflowContainerRef.current?.focus();
475+
}}
476+
data={activeNode?.values}
477+
>
478+
{NodeLogWrap}
479+
</PanelStatusLogContainer>
480+
)}
536481
</ReactFlow>
537482
</div>
538483
);

packages/x-flow/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export interface TNodeItem {
5858
showTestingBtn?: boolean; // 是否显示单点调试按钮
5959
className?: string;// 自定义节点class
6060
disabledShortcutDelete?: boolean; // 是否禁用快捷键删除
61+
disabledShortcutCopy?: boolean; // 是否禁用快捷键复制
6162
}
6263

6364
export interface TNodeGroup {

0 commit comments

Comments
 (0)