88import '@xyflow/react/dist/style.css' ;
99import { useEventListener , useMemoizedFn } from 'ahooks' ;
1010import { produce , setAutoFreeze } from 'immer' ;
11- import { debounce , isFunction } from 'lodash' ;
11+ import { isFunction , isString } from 'lodash' ;
1212import type { FC } from 'react' ;
1313import 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,49 @@ 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 ) {
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 ) ;
150+ const targetElement =
151+ e . target instanceof HTMLElement ? e . target : undefined ;
152+ const targetTagName = targetElement ?. tagName ;
153+ const targetClassList = targetElement ?. classList ;
154+
155+ if ( isNodeCopyEvent && selectedNode ?. id ) {
138156 // 复制节点
139- isNodeCopyingRef . current = true ; // 标记为节点复制
140- copyNode ( selectedNode . id ) ;
141157 e . preventDefault ( ) ;
142-
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 ) ;
154- }
155- storeApi . setState ( {
156- copyNodes : [ ] ,
157- copyTimeoutId : null ,
158- isAddingNode : false ,
159- } ) ;
160- }
158+ copyNode ( selectedNode . id ) ;
161159 }
160+
161+
162162 } else if ( ( e . key === 'v' || e . key === 'V' ) && ( e . ctrlKey || e . metaKey ) ) {
163163 const { copyNodes } = storeApi . getState ( ) ;
164- // 只有在有节点复制状态时才拦截粘贴操作
165164 if ( copyNodes ?. length > 0 ) {
166- pasteNodeSimple ( ) ;
167165 e . preventDefault ( ) ;
166+ pasteNodeSimple ( ) ;
168167 }
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 )
168+ } else if ( e . key === 'Escape' ) {
169+ setOpenPanel ( false ) ;
170+ workflowContainerRef . current ?. focus ( ) ;
181171 }
182172 } ) ;
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 ;
173+ useEventListener ( 'keydown' , handleKeyDown , {
174+ target : workflowContainerRef ,
201175 } ) ;
202176
203177 useEventListener (
@@ -221,28 +195,6 @@ const XFlow: FC<FlowProps> = memo(props => {
221195 }
222196 ) ;
223197
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-
246198 const { eventEmitter } = useEventEmitterContextContext ( ) ;
247199 eventEmitter ?. useSubscription ( ( v : any ) => {
248200 // 整理画布
@@ -278,28 +230,6 @@ const XFlow: FC<FlowProps> = memo(props => {
278230 setCandidateNode ( newNode ) ;
279231 } ;
280232
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-
303233 // edge 移入/移出效果
304234 const getUpdateEdgeConfig = useMemoizedFn ( ( edge : any , color : string ) => {
305235 const newEdges = produce ( edges , draft => {
@@ -323,7 +253,6 @@ const XFlow: FC<FlowProps> = memo(props => {
323253 const { _nodeType, _status, ...restData } = data || { } ;
324254 const nodeSetting = settingMap [ _nodeType ] || { } ;
325255 const showPanel = nodeSetting ?. nodePanel ?. showPanel ?? true ;
326-
327256 return (
328257 < CustomNode
329258 { ...rest }
@@ -333,9 +262,6 @@ const XFlow: FC<FlowProps> = memo(props => {
333262 layout = { layout }
334263 status = { _status }
335264 onClick = { async e => {
336- // 记录用户点击了节点
337- lastClickedNodeRef . current = true ;
338-
339265 if ( nodeEditorRef ?. current ?. validateForm ) {
340266 const result = await nodeEditorRef ?. current ?. validateForm ( ) ;
341267 if ( ! result ) {
@@ -400,7 +326,14 @@ const XFlow: FC<FlowProps> = memo(props => {
400326 const panelonClose = globalConfig ?. nodePanel ?. onClose ;
401327
402328 return (
403- < div id = "xflow-container" ref = { workflowContainerRef } >
329+ < div
330+ id = "xflow-container"
331+ ref = { workflowContainerRef }
332+ tabIndex = { 0 }
333+ onMouseDown = { ( ) => {
334+ workflowContainerRef . current ?. focus ( ) ;
335+ } }
336+ >
404337 < ReactFlow
405338 panOnDrag = { panOnDrag }
406339 nodeTypes = { nodeTypes }
@@ -504,6 +437,7 @@ const XFlow: FC<FlowProps> = memo(props => {
504437 return ;
505438 }
506439 setOpenPanel ( false ) ;
440+ workflowContainerRef . current ?. focus ( ) ;
507441
508442 // 如果日志面板关闭
509443 if ( ! isTruthy ( activeNode ?. _status ) || ! openLogPanel ) {
@@ -520,19 +454,22 @@ const XFlow: FC<FlowProps> = memo(props => {
520454 { NodeEditorWrap }
521455 </ PanelContainer >
522456 ) }
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- ) }
457+ { isTruthy ( activeNode ?. _status ) &&
458+ openLogPanel &&
459+ Boolean ( logPanel ?. enable ?? true ) && (
460+ < PanelStatusLogContainer
461+ id = { activeNode ?. id }
462+ nodeType = { activeNode ?. _nodeType }
463+ onClose = { ( ) => {
464+ setOpenLogPanel ( false ) ;
465+ ! openPanel && setActiveNode ( null ) ;
466+ workflowContainerRef . current ?. focus ( ) ;
467+ } }
468+ data = { activeNode ?. values }
469+ >
470+ { NodeLogWrap }
471+ </ PanelStatusLogContainer >
472+ ) }
536473 </ ReactFlow >
537474 </ div >
538475 ) ;
0 commit comments