diff --git a/docs/xflow/api.md b/docs/xflow/api.md index b8ce064a6..664189381 100644 --- a/docs/xflow/api.md +++ b/docs/xflow/api.md @@ -171,6 +171,7 @@ Handle 配置继承自 React Flow 的 Handle 配置,用于控制节点连接 | getSettingSchema | 动态获取节点的业务配置信息,返回值同settingSchema。同时设置`settingSchema`和`getSettingSchema`只生效`getSettingSchema` | `(nodeId: string, nodeType: string, nodeItem: TNodeItem, nodeData: any, form: ReturnType) => Promise` | | | renderHandle | 自定义渲染节点的handle,以实现某个节点自定义的出口数量.`sourceHandle`是原handle组件 | `(sourceHandle: SourceHandleType,sourceHandleProps:ComponentProps,nodeProps: {id: string;type: string;data: any;layout: 'LR';isConnectable: boolean;readOnly: boolean;}) => React.JSX.Element`| | | disabledShortcutDelete | 是否禁用该节点的快捷键删除功能 | `boolean` | false | +| disabledShortcutCopy | 是否禁用该节点的快捷键复制功能 | `boolean` | false | ## TNodeSelector diff --git a/docs/xflow/demo/quickStart/index.tsx b/docs/xflow/demo/quickStart/index.tsx index c39da786b..c155edffe 100644 --- a/docs/xflow/demo/quickStart/index.tsx +++ b/docs/xflow/demo/quickStart/index.tsx @@ -8,6 +8,7 @@ export default () => { type: 'Start', hidden: true, disabledShortcutDelete: true, + disabledShortcutCopy: true, targetHandleHidden: true, icon: { type: 'icon-start', diff --git a/packages/x-flow/src/XFlow.tsx b/packages/x-flow/src/XFlow.tsx index c6cf8984f..62c620001 100644 --- a/packages/x-flow/src/XFlow.tsx +++ b/packages/x-flow/src/XFlow.tsx @@ -8,7 +8,7 @@ import { import '@xyflow/react/dist/style.css'; import { useEventListener, useMemoizedFn } from 'ahooks'; import { produce, setAutoFreeze } from 'immer'; -import { debounce, isFunction } from 'lodash'; +import { isFunction, isString } from 'lodash'; import type { FC } from 'react'; import React, { memo, @@ -87,7 +87,8 @@ const XFlow: FC = memo(props => { ); const { record } = useTemporalStore(); const [activeNode, setActiveNode] = useState(null); - const { settingMap, globalConfig, readOnly, logPanel } = useContext(ConfigContext); + const { settingMap, globalConfig, readOnly, logPanel } = + useContext(ConfigContext); const [openPanel, setOpenPanel] = useState(true); const [openLogPanel, setOpenLogPanel] = useState(true); const { @@ -101,8 +102,6 @@ const XFlow: FC = memo(props => { const nodeEditorRef = useRef(null); const { copyNode, pasteNodeSimple } = useFlow(); const { undo, redo } = useTemporalStore(); - const isNodeCopyingRef = useRef(false); // 是否正在进行节点复制 - const lastClickedNodeRef = useRef(false); // 最后一次点击是否是节点 useEffect(() => { zoomTo(0.8); @@ -116,7 +115,7 @@ const XFlow: FC = memo(props => { }; }, []); - useEventListener('keydown', e => { + const handleKeyDown = useMemoizedFn((e: KeyboardEvent) => { if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey)) e.preventDefault(); if ((e.key === 'z' || e.key === 'Z') && (e.ctrlKey || e.metaKey)) { @@ -130,74 +129,53 @@ const XFlow: FC = memo(props => { if ((e.key === 's' || e.key === 'S') && (e.ctrlKey || e.metaKey)) e.preventDefault(); if ((e.key === 'c' || e.key === 'C') && (e.ctrlKey || e.metaKey)) { - const selectedNode = nodes?.find(node => node.selected); - // 获取当前选中的文本(非节点的内容) - const selectedText = window.getSelection()?.toString(); - // 如果最后点击的是节点 且 有节点被选中 则 复制节点 - if (selectedNode && lastClickedNodeRef.current) { - // 复制节点 - isNodeCopyingRef.current = true; // 标记为节点复制 - copyNode(selectedNode.id); - e.preventDefault(); + const latestNodes = storeApi.getState().nodes; + let isNodeCopyEvent = false; + if (e.target instanceof HTMLElement) { + const target = e.target as HTMLElement; + if ( + (isString(target.tagName) && + target.tagName.toLowerCase() === 'body') || + (target.tagName.toLowerCase() === 'div' && + target.classList && + isFunction(target.classList.contains) && + (target.classList.contains('ant-drawer') || + target.classList.contains('react-flow__node') || + target.id === 'xflow-container')) + ) { + isNodeCopyEvent = true; + } + } + const selectedNode = latestNodes?.find(node => node.selected); - // 清空系统剪贴板,确保粘贴时使用节点而非之前的文本 - try { - navigator.clipboard.writeText('').catch(() => {}); - } catch (err) {} - } else if (selectedText) { - // 复制文本 - // 清除之前的节点复制状态 - const { copyNodes, copyTimeoutId } = storeApi.getState(); - if (copyNodes?.length > 0) { - if (copyTimeoutId) { - clearTimeout(copyTimeoutId); + if (isNodeCopyEvent && selectedNode?.id) { + const nodeType = selectedNode?.data?._nodeType; + if (isString(nodeType) && nodeType) { + const nodeConfig = settingMap[nodeType]; + if (nodeConfig?.disabledShortcutCopy) { + message.warning( + `${selectedNode.data?.title || selectedNode.id}节点不允许复制` + ); + return; } - storeApi.setState({ - copyNodes: [], - copyTimeoutId: null, - isAddingNode: false, - }); } + // 复制节点 + e.preventDefault(); + copyNode(selectedNode.id); } } else if ((e.key === 'v' || e.key === 'V') && (e.ctrlKey || e.metaKey)) { const { copyNodes } = storeApi.getState(); - // 只有在有节点复制状态时才拦截粘贴操作 if (copyNodes?.length > 0) { - pasteNodeSimple(); e.preventDefault(); + pasteNodeSimple(); } - } else if (copyNodes.length > 0) { - // 只在有复制节点时才检查其他操作 - const { copyTimeoutId } = storeApi.getState(); - if (copyTimeoutId) { - clearTimeout(copyTimeoutId); - storeApi.setState({ - copyTimeoutId: null, - isAddingNode: false, - }); - } - } else if (e.key === 'Escape'){ - setOpenPanel(false) + } else if (e.key === 'Escape') { + setOpenPanel(false); + workflowContainerRef.current?.focus(); } }); - - // 添加 copy 事件监听,获取实际复制的内容 - useEventListener('copy', (e: ClipboardEvent) => { - if (!isNodeCopyingRef.current) { - // 清除节点复制状态,因为用户复制了其他内容 - const { copyNodes, copyTimeoutId } = storeApi.getState(); - if (copyNodes?.length > 0) { - if (copyTimeoutId) { - clearTimeout(copyTimeoutId); - } - storeApi.setState({ - copyNodes: [], - copyTimeoutId: null, - isAddingNode: false, - }); - } - } - isNodeCopyingRef.current = false; + useEventListener('keydown', handleKeyDown, { + target: workflowContainerRef, }); useEventListener( @@ -221,28 +199,6 @@ const XFlow: FC = memo(props => { } ); - // 当点击非节点区域时重置标记 - useEventListener('mousedown', (e: MouseEvent) => { - const target = e.target as HTMLElement; - const isClickingNode = - target.closest('.xflow-node-container') || - target.closest('.candidate-node'); - // 如果点击的不是节点,重置标记 - if (!isClickingNode) { - lastClickedNodeRef.current = false; - - // 清除复制状态 - const { copyTimeoutId, copyNodes } = storeApi.getState(); - if (copyTimeoutId && copyNodes?.length > 0) { - clearTimeout(copyTimeoutId); - storeApi.setState({ - copyTimeoutId: null, - isAddingNode: false, - }); - } - } - }); - const { eventEmitter } = useEventEmitterContextContext(); eventEmitter?.useSubscription((v: any) => { // 整理画布 @@ -278,28 +234,6 @@ const XFlow: FC = memo(props => { setCandidateNode(newNode); }; - // 插入节点 - // const handleInsertNode = () => { - // const newNode = { - // id: uuid(), - // data: { label: 'new node' }, - // position: { - // x: 0, - // y: 0, - // }, - // }; - // addNodes(newNode); - // addEdges({ - // id: uuid(), - // source: '2', - // target: newNode.id, - // }); - // const targetEdge = edges.find(edge => edge.source === '2'); - // updateEdge(targetEdge?.id as string, { - // source: newNode.id, - // }); - // }; - // edge 移入/移出效果 const getUpdateEdgeConfig = useMemoizedFn((edge: any, color: string) => { const newEdges = produce(edges, draft => { @@ -323,7 +257,6 @@ const XFlow: FC = memo(props => { const { _nodeType, _status, ...restData } = data || {}; const nodeSetting = settingMap[_nodeType] || {}; const showPanel = nodeSetting?.nodePanel?.showPanel ?? true; - return ( = memo(props => { layout={layout} status={_status} onClick={async e => { - // 记录用户点击了节点 - lastClickedNodeRef.current = true; - if (nodeEditorRef?.current?.validateForm) { const result = await nodeEditorRef?.current?.validateForm(); if (!result) { @@ -356,7 +286,7 @@ const XFlow: FC = memo(props => { } setOpenLogPanel(true); }} - onDelete={()=>{ + onDelete={() => { // 删除节点并关闭弹窗 setActiveNode(null); }} @@ -400,7 +330,14 @@ const XFlow: FC = memo(props => { const panelonClose = globalConfig?.nodePanel?.onClose; return ( -
+
{ + workflowContainerRef.current?.focus(); + }} + > = memo(props => { }, deletable: deletable, //默认连线属性受此项控制 }} - onBeforeDelete={async (elements) => { + onBeforeDelete={async elements => { if (readOnly) { return false; } @@ -436,10 +373,14 @@ const XFlow: FC = memo(props => { : false; }); if (blockedNodes?.length > 0) { - message.warning(`${blockedNodes.map(n => n.data?.title || n.id).join(', ')}节点不允许删除!`); + message.warning( + `${blockedNodes + .map(n => n.data?.title || n.id) + .join(', ')}节点不允许删除!` + ); return false; } - return true + return true; }} onConnect={onConnect} onNodesChange={changes => { @@ -465,24 +406,24 @@ const XFlow: FC = memo(props => { }); }} onEdgeMouseEnter={(_, edge: any) => { - if(!edge.style.stroke || edge.style.stroke === '#c9c9c9'){ + if (!edge.style.stroke || edge.style.stroke === '#c9c9c9') { getUpdateEdgeConfig(edge, '#2970ff'); } }} onEdgeMouseLeave={(_, edge) => { - if(['#2970ff',"#c9c9c9"].includes(edge.style.stroke)){ + if (['#2970ff', '#c9c9c9'].includes(edge.style.stroke)) { getUpdateEdgeConfig(edge, '#c9c9c9'); } }} onNodesDelete={() => { - setActiveNode(null); + setActiveNode(null); }} onNodeClick={(event, node) => { onNodeClick && onNodeClick(event, node); }} deleteKeyCode={globalConfig?.deleteKeyCode} - onEdgeClick={(event,edge)=>{ - onEdgeClick && onEdgeClick(event,edge) + onEdgeClick={(event, edge) => { + onEdgeClick && onEdgeClick(event, edge); }} > @@ -504,6 +445,7 @@ const XFlow: FC = memo(props => { return; } setOpenPanel(false); + workflowContainerRef.current?.focus(); // 如果日志面板关闭 if (!isTruthy(activeNode?._status) || !openLogPanel) { @@ -520,19 +462,22 @@ const XFlow: FC = memo(props => { {NodeEditorWrap} )} - {isTruthy(activeNode?._status) && openLogPanel && Boolean(logPanel?.enable ?? true) && ( - { - setOpenLogPanel(false); - !openPanel && setActiveNode(null); - }} - data={activeNode?.values} - > - {NodeLogWrap} - - )} + {isTruthy(activeNode?._status) && + openLogPanel && + Boolean(logPanel?.enable ?? true) && ( + { + setOpenLogPanel(false); + !openPanel && setActiveNode(null); + workflowContainerRef.current?.focus(); + }} + data={activeNode?.values} + > + {NodeLogWrap} + + )}
); diff --git a/packages/x-flow/src/types.ts b/packages/x-flow/src/types.ts index 60a91f2af..174d03e2b 100644 --- a/packages/x-flow/src/types.ts +++ b/packages/x-flow/src/types.ts @@ -58,6 +58,7 @@ export interface TNodeItem { showTestingBtn?: boolean; // 是否显示单点调试按钮 className?: string;// 自定义节点class disabledShortcutDelete?: boolean; // 是否禁用快捷键删除 + disabledShortcutCopy?: boolean; // 是否禁用快捷键复制 } export interface TNodeGroup {