import type React from 'react'; import { useCallback, useEffect, useRef, useState, useContext, useMemo } from 'react'; import { useObserve } from '@flowgram.ai/reactive'; import { getNodeForm } from '@flowgram.ai/node'; import { FlowNodeRenderData } from '@flowgram.ai/document'; import { MouseTouchEvent, PlaygroundEntityContext, useListenEvents, useService, } from '@flowgram.ai/core'; import { WorkflowDragService, WorkflowSelectService } from '../service'; import { WorkflowNodePortsData } from '../entity-datas'; import { type WorkflowNodeEntity } from '../entities'; import { usePlaygroundReadonlyState } from './use-playground-readonly-state'; import { type NodeRenderReturnType } from './typings'; function checkTargetDraggable(el: any): boolean { return ( el && el.tagName !== 'INPUT' && el.tagName !== 'TEXTAREA' && !el.closest('.flow-canvas-not-draggable') ); } export function useNodeRender(nodeFromProps?: WorkflowNodeEntity): NodeRenderReturnType { const node = nodeFromProps || useContext(PlaygroundEntityContext); const renderData = node.getData(FlowNodeRenderData)!; const portsData = node.getData(WorkflowNodePortsData)!; const readonly = usePlaygroundReadonlyState(); const dragService = useService(WorkflowDragService); const selectionService = useService(WorkflowSelectService); const isDragging = useRef(false); const [formValueVersion, updateFormValueVersion] = useState(0); const formValueDependRef = useRef(false); formValueDependRef.current = false; const nodeRef = useRef(null); const [linkingNodeId, setLinkingNodeId] = useState(''); useEffect(() => { const disposable = dragService.onDragLineEventChange(({ type, onDragNodeId }) => { if (type === 'onDrag') { setLinkingNodeId(onDragNodeId || ''); } else { setLinkingNodeId(''); } }); return () => { disposable.dispose(); }; }, []); const startDrag = useCallback( (e: React.MouseEvent) => { MouseTouchEvent.preventDefault(e); if (!selectionService.isSelected(node.id)) { selectNode(e); } if (!MouseTouchEvent.isTouchEvent(e as unknown as React.TouchEvent)) { // 输入框不能拖拽 if (!checkTargetDraggable(e.target) || !checkTargetDraggable(document.activeElement)) { return; } } isDragging.current = true; // 拖拽选中的节点 dragService.startDragSelectedNodes(e)?.finally(() => setTimeout(() => { isDragging.current = false; }) ); }, [dragService, node] ); /** * 单选节点 */ const selectNode = useCallback( (e: React.MouseEvent) => { // 触发了拖拽就不要再触发单选 if (isDragging.current) { return; } // 追加选择 if (e.shiftKey) { selectionService.toggleSelect(node); } else { selectionService.selectNode(node); } if (e.target) { (e.target as HTMLDivElement).focus(); } }, [node] ); const deleteNode = useCallback(() => node.dispose(), [node]); // 监听端口变化 useListenEvents(portsData.onDataChange); /** * - 下面的 firefox 为了修复一个 bug:https://meego.feishu.cn/bot_bot/issue/detail/3001017843 * - firefox 下 draggable 属性会影响节点 input 内容 focus:https://jsfiddle.net/Aydar/ztsvbyep/3/ * - 该 bug 在 firefox 浏览器上存在了很久,需要作兼容:https://bugzilla.mozilla.org/show_bug.cgi?id=739071 */ const isFirefox = navigator?.userAgent?.includes?.('Firefox'); const onFocus = useCallback(() => { if (isFirefox) { nodeRef.current?.setAttribute('draggable', 'false'); } }, []); const onBlur = useCallback(() => { if (isFirefox) { nodeRef.current?.setAttribute('draggable', 'true'); } }, []); const getExtInfo = useCallback(() => node.getExtInfo() as any, [node]); const updateExtInfo = useCallback( (data: any) => { node.updateExtInfo(data); }, [node] ); const form = useMemo(() => getNodeForm(node), [node]); // Listen FormState change const formState = useObserve(form?.state); const toggleExpand = useCallback(() => { renderData.toggleExpand(); }, [renderData]); const selected = selectionService.isSelected(node.id); const activated = selectionService.isActivated(node.id); const expanded = renderData.expanded; useEffect(() => { const toDispose = form?.onFormValuesChange(() => { if (formValueDependRef.current) { updateFormValueVersion((v) => v + 1); } }); return () => toDispose?.dispose(); }, [form]); return useMemo( () => ({ id: node.id, type: node.flowNodeType, get data() { if (form) { formValueDependRef.current = true; return form.values; } return getExtInfo(); }, updateData(values: any) { if (form) { form.updateFormValues(values); } else { updateExtInfo(values); } }, node, selected, activated, expanded, startDrag, get ports() { return portsData.allPorts; }, deleteNode, selectNode, readonly, linkingNodeId, nodeRef, onFocus, onBlur, getExtInfo, updateExtInfo, toggleExpand, get form() { if (!form) return undefined; return { ...form, get values() { formValueDependRef.current = true; return form.values!; }, get state() { return formState; }, }; }, }), [ node, selected, activated, expanded, startDrag, deleteNode, selectNode, readonly, linkingNodeId, nodeRef, onFocus, onBlur, getExtInfo, updateExtInfo, toggleExpand, formValueVersion, ] ); }