UNPKG

reactflow-velocity

Version:

React Flow - A highly customizable React library for building node-based editors and interactive flow charts.

1,199 lines (1,177 loc) 169 kB
"use client" import { jsxs, Fragment, jsx } from 'react/jsx-runtime'; import { createContext, useContext, useMemo, useEffect, useRef, useState, useCallback, forwardRef, memo } from 'react'; import cc from 'classcat'; import { errorMessages, infiniteExtent, isInputDOMNode, fitView, getViewportForBounds, pointToRendererPoint, rendererPointToPoint, isNodeBase, isEdgeBase, getElementsToRemove, isRectObject, nodeToRect, getOverlappingArea, getDimensions, XYPanZoom, PanOnScrollMode, SelectionMode, getEventPosition, getNodesInside, XYDrag, snapPosition, calculateNodePosition, Position, isMouseEvent, XYHandle, getHostForElement, addEdge, getNodesBounds, clampPosition, internalsSymbol, getPositionWithOrigin, elementSelectionKeys, isEdgeVisible, MarkerType, createMarkerIds, isNumeric, getBezierEdgeCenter, getSmoothStepPath, getStraightPath, getBezierPath, getEdgePosition, getElevatedEdgeZIndex, getMarkerId, ConnectionMode, ConnectionLineType, updateConnectionLookup, adoptUserProvidedNodes, devWarn, updateNodeDimensions, updateAbsolutePositions, panBy, isMacOs, areConnectionMapsEqual, handleConnectionChange, getNodePositionWithOrigin, XYMinimap, getBoundsOfRects, ResizeControlVariant, XYResizer, XY_RESIZER_LINE_POSITIONS, XY_RESIZER_HANDLE_POSITIONS, getNodeToolbarTransform } from '@xyflow/system'; export { ConnectionLineType, ConnectionMode, MarkerType, PanOnScrollMode, Position, SelectionMode, addEdge, getBezierEdgeCenter, getBezierPath, getConnectedEdges, getEdgeCenter, getIncomers, getNodesBounds, getOutgoers, getSmoothStepPath, getStraightPath, getViewportForBounds, internalsSymbol, updateEdge } from '@xyflow/system'; import { useStoreWithEqualityFn, createWithEqualityFn } from 'zustand/traditional'; import { shallow } from 'zustand/shallow'; import { createPortal } from 'react-dom'; const StoreContext = createContext(null); const Provider$1 = StoreContext.Provider; const zustandErrorMessage = errorMessages['error001'](); /** * Hook for accessing the internal store. Should only be used in rare cases. * * @public * @param selector * @param equalityFn * @returns The selected state slice */ function useStore(selector, equalityFn) { const store = useContext(StoreContext); if (store === null) { throw new Error(zustandErrorMessage); } return useStoreWithEqualityFn(store, selector, equalityFn); } const useStoreApi = () => { const store = useContext(StoreContext); if (store === null) { throw new Error(zustandErrorMessage); } return useMemo(() => ({ getState: store.getState, setState: store.setState, subscribe: store.subscribe, destroy: store.destroy, }), [store]); }; const style = { display: 'none' }; const ariaLiveStyle = { position: 'absolute', width: 1, height: 1, margin: -1, border: 0, padding: 0, overflow: 'hidden', clip: 'rect(0px, 0px, 0px, 0px)', clipPath: 'inset(100%)', }; const ARIA_NODE_DESC_KEY = 'react-flow__node-desc'; const ARIA_EDGE_DESC_KEY = 'react-flow__edge-desc'; const ARIA_LIVE_MESSAGE = 'react-flow__aria-live'; const selector$q = (s) => s.ariaLiveMessage; function AriaLiveMessage({ rfId }) { const ariaLiveMessage = useStore(selector$q); return (jsx("div", { id: `${ARIA_LIVE_MESSAGE}-${rfId}`, "aria-live": "assertive", "aria-atomic": "true", style: ariaLiveStyle, children: ariaLiveMessage })); } function A11yDescriptions({ rfId, disableKeyboardA11y }) { return (jsxs(Fragment, { children: [jsxs("div", { id: `${ARIA_NODE_DESC_KEY}-${rfId}`, style: style, children: ["Press enter or space to select a node.", !disableKeyboardA11y && 'You can then use the arrow keys to move the node around.', " Press delete to remove it and escape to cancel.", ' '] }), jsx("div", { id: `${ARIA_EDGE_DESC_KEY}-${rfId}`, style: style, children: "Press enter or space to select an edge. You can then press delete to remove it or escape to cancel." }), !disableKeyboardA11y && jsx(AriaLiveMessage, { rfId: rfId })] })); } const selector$p = (s) => (s.userSelectionActive ? 'none' : 'all'); function Panel({ position = 'top-left', children, className, style, ...rest }) { const pointerEvents = useStore(selector$p); const positionClasses = `${position}`.split('-'); return (jsx("div", { className: cc(['react-flow__panel', className, ...positionClasses]), style: { ...style, pointerEvents }, ...rest, children: children })); } function Attribution({ proOptions, position = 'bottom-right' }) { if (proOptions?.hideAttribution) { return null; } return (jsx(Panel, { position: position, className: "react-flow__attribution", "data-message": "Please only hide this attribution when you are subscribed to React Flow Pro: https://pro.reactflow.dev", children: jsx("a", { href: "https://reactflow.dev", target: "_blank", rel: "noopener noreferrer", "aria-label": "React Flow attribution", children: "React Flow" }) })); } const selector$o = (s) => ({ selectedNodes: s.nodes.filter((n) => n.selected), selectedEdges: s.edges.filter((e) => e.selected), }); const selectId = (obj) => obj.id; function areEqual(a, b) { return (shallow(a.selectedNodes.map(selectId), b.selectedNodes.map(selectId)) && shallow(a.selectedEdges.map(selectId), b.selectedEdges.map(selectId))); } function SelectionListenerInner({ onSelectionChange }) { const store = useStoreApi(); const { selectedNodes, selectedEdges } = useStore(selector$o, areEqual); useEffect(() => { const params = { nodes: selectedNodes, edges: selectedEdges }; onSelectionChange?.(params); store.getState().onSelectionChangeHandlers.forEach((fn) => fn(params)); }, [selectedNodes, selectedEdges, onSelectionChange]); return null; } const changeSelector = (s) => !!s.onSelectionChangeHandlers; function SelectionListener({ onSelectionChange }) { const storeHasSelectionChangeHandlers = useStore(changeSelector); if (onSelectionChange || storeHasSelectionChangeHandlers) { return jsx(SelectionListenerInner, { onSelectionChange: onSelectionChange }); } return null; } /* * This component helps us to update the store with the vlues coming from the user. * We distinguish between values we can update directly with `useDirectStoreUpdater` (like `snapGrid`) * and values that have a dedicated setter function in the store (like `setNodes`). */ // these fields exist in the global store and we need to keep them up to date const reactFlowFieldsToTrack = [ 'nodes', 'edges', 'defaultNodes', 'defaultEdges', 'onConnect', 'onConnectStart', 'onConnectEnd', 'onClickConnectStart', 'onClickConnectEnd', 'nodesDraggable', 'nodesConnectable', 'nodesFocusable', 'edgesFocusable', 'edgesUpdatable', 'elevateNodesOnSelect', 'elevateEdgesOnSelect', 'minZoom', 'maxZoom', 'nodeExtent', 'onNodesChange', 'onEdgesChange', 'elementsSelectable', 'connectionMode', 'snapGrid', 'snapToGrid', 'translateExtent', 'connectOnClick', 'defaultEdgeOptions', 'fitView', 'fitViewOptions', 'onNodesDelete', 'onEdgesDelete', 'onDelete', 'onNodeDrag', 'onNodeDragStart', 'onNodeDragStop', 'onSelectionDrag', 'onSelectionDragStart', 'onSelectionDragStop', 'onMoveStart', 'onMove', 'onMoveEnd', 'noPanClassName', 'nodeOrigin', 'autoPanOnConnect', 'autoPanOnNodeDrag', 'onError', 'connectionRadius', 'isValidConnection', 'selectNodesOnDrag', 'nodeDragThreshold', 'onBeforeDelete', ]; // rfId doesn't exist in ReactFlowProps, but it's one of the fields we want to update const fieldsToTrack = [...reactFlowFieldsToTrack, 'rfId']; const selector$n = (s) => ({ setNodes: s.setNodes, setEdges: s.setEdges, setDefaultNodesAndEdges: s.setDefaultNodesAndEdges, setMinZoom: s.setMinZoom, setMaxZoom: s.setMaxZoom, setTranslateExtent: s.setTranslateExtent, setNodeExtent: s.setNodeExtent, reset: s.reset, }); function StoreUpdater(props) { const { setNodes, setEdges, setDefaultNodesAndEdges, setMinZoom, setMaxZoom, setTranslateExtent, setNodeExtent, reset, } = useStore(selector$n, shallow); const store = useStoreApi(); useEffect(() => { const edgesWithDefaults = props.defaultEdges?.map((e) => ({ ...e, ...props.defaultEdgeOptions })); setDefaultNodesAndEdges(props.defaultNodes, edgesWithDefaults); return () => { reset(); }; }, []); const previousFields = useRef({ // these are values that are also passed directly to other components // than the StoreUpdater. We can reduce the number of setStore calls // by setting the same values here as prev fields. translateExtent: infiniteExtent, nodeOrigin: initNodeOrigin, minZoom: 0.5, maxZoom: 2, elementsSelectable: true, noPanClassName: 'nopan', rfId: '1', }); useEffect(() => { for (const fieldName of fieldsToTrack) { const fieldValue = props[fieldName]; const previousFieldValue = previousFields.current[fieldName]; if (fieldValue === previousFieldValue) continue; if (typeof props[fieldName] === 'undefined') continue; // Custom handling with dedicated setters for some fields if (fieldName === 'nodes') setNodes(fieldValue); else if (fieldName === 'edges') setEdges(fieldValue); else if (fieldName === 'minZoom') setMinZoom(fieldValue); else if (fieldName === 'maxZoom') setMaxZoom(fieldValue); else if (fieldName === 'translateExtent') setTranslateExtent(fieldValue); else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue); // Renamed fields else if (fieldName === 'fitView') store.setState({ fitViewOnInit: fieldValue }); else if (fieldName === 'fitViewOptions') store.setState({ fitViewOnInitOptions: fieldValue }); // General case else store.setState({ [fieldName]: fieldValue }); } previousFields.current = props; }, // Only re-run the effect if one of the fields we track changes fieldsToTrack.map((fieldName) => props[fieldName])); return null; } function getMediaQuery() { if (typeof window === 'undefined' || !window.matchMedia) { return null; } return window.matchMedia('(prefers-color-scheme: dark)'); } /** * Hook for receiving the current color mode class 'dark' or 'light'. * * @internal * @param colorMode - The color mode to use ('dark', 'light' or 'system') */ function useColorModeClass(colorMode) { const [colorModeClass, setColorModeClass] = useState(colorMode === 'system' ? null : colorMode); useEffect(() => { if (colorMode !== 'system') { setColorModeClass(colorMode); return; } const mediaQuery = getMediaQuery(); const updateColorModeClass = () => setColorModeClass(mediaQuery?.matches ? 'dark' : 'light'); updateColorModeClass(); mediaQuery?.addEventListener('change', updateColorModeClass); return () => { mediaQuery?.removeEventListener('change', updateColorModeClass); }; }, [colorMode]); return colorModeClass !== null ? colorModeClass : getMediaQuery()?.matches ? 'dark' : 'light'; } const defaultDoc = typeof document !== 'undefined' ? document : null; /** * Hook for handling key events. * * @public * @param param.keyCode - The key code (string or array of strings) to use * @param param.options - Options * @returns boolean */ function useKeyPress( // the keycode can be a string 'a' or an array of strings ['a', 'a+d'] // a string means a single key 'a' or a combination when '+' is used 'a+d' // an array means different possibilites. Explainer: ['a', 'd+s'] here the // user can use the single key 'a' or the combination 'd' + 's' keyCode = null, options = { target: defaultDoc, actInsideInputWithModifier: true }) { const [keyPressed, setKeyPressed] = useState(false); // we need to remember if a modifier key is pressed in order to track it const modifierPressed = useRef(false); // we need to remember the pressed keys in order to support combinations const pressedKeys = useRef(new Set([])); // keyCodes = array with single keys [['a']] or key combinations [['a', 's']] // keysToWatch = array with all keys flattened ['a', 'd', 'ShiftLeft'] // used to check if we store event.code or event.key. When the code is in the list of keysToWatch // we use the code otherwise the key. Explainer: When you press the left "command" key, the code is "MetaLeft" // and the key is "Meta". We want users to be able to pass keys and codes so we assume that the key is meant when // we can't find it in the list of keysToWatch. const [keyCodes, keysToWatch] = useMemo(() => { if (keyCode !== null) { const keyCodeArr = Array.isArray(keyCode) ? keyCode : [keyCode]; const keys = keyCodeArr.filter((kc) => typeof kc === 'string').map((kc) => kc.split('+')); const keysFlat = keys.reduce((res, item) => res.concat(...item), []); return [keys, keysFlat]; } return [[], []]; }, [keyCode]); useEffect(() => { const target = options?.target || defaultDoc; if (keyCode !== null) { const downHandler = (event) => { modifierPressed.current = event.ctrlKey || event.metaKey || event.shiftKey; const preventAction = (!modifierPressed.current || (modifierPressed.current && !options.actInsideInputWithModifier)) && isInputDOMNode(event); if (preventAction) { return false; } const keyOrCode = useKeyOrCode(event.code, keysToWatch); pressedKeys.current.add(event[keyOrCode]); if (isMatchingKey(keyCodes, pressedKeys.current, false)) { event.preventDefault(); setKeyPressed(true); } }; const upHandler = (event) => { const preventAction = (!modifierPressed.current || (modifierPressed.current && !options.actInsideInputWithModifier)) && isInputDOMNode(event); if (preventAction) { return false; } const keyOrCode = useKeyOrCode(event.code, keysToWatch); if (isMatchingKey(keyCodes, pressedKeys.current, true)) { setKeyPressed(false); pressedKeys.current.clear(); } else { pressedKeys.current.delete(event[keyOrCode]); } modifierPressed.current = false; }; const resetHandler = () => { pressedKeys.current.clear(); setKeyPressed(false); }; target?.addEventListener('keydown', downHandler); target?.addEventListener('keyup', upHandler); window.addEventListener('blur', resetHandler); return () => { target?.removeEventListener('keydown', downHandler); target?.removeEventListener('keyup', upHandler); window.removeEventListener('blur', resetHandler); }; } }, [keyCode, setKeyPressed]); return keyPressed; } // utils function isMatchingKey(keyCodes, pressedKeys, isUp) { return (keyCodes // we only want to compare same sizes of keyCode definitions // and pressed keys. When the user specified 'Meta' as a key somewhere // this would also be truthy without this filter when user presses 'Meta' + 'r' .filter((keys) => isUp || keys.length === pressedKeys.size) // since we want to support multiple possibilities only one of the // combinations need to be part of the pressed keys .some((keys) => keys.every((k) => pressedKeys.has(k)))); } function useKeyOrCode(eventCode, keysToWatch) { return keysToWatch.includes(eventCode) ? 'code' : 'key'; } const selector$m = (s) => !!s.panZoom; /** * Hook for getting viewport helper functions. * * @internal * @returns viewport helper functions */ const useViewportHelper = () => { const store = useStoreApi(); const panZoomInitialized = useStore(selector$m); const viewportHelperFunctions = useMemo(() => { return { zoomIn: (options) => store.getState().panZoom?.scaleBy(1.2, { duration: options?.duration }), zoomOut: (options) => store.getState().panZoom?.scaleBy(1 / 1.2, { duration: options?.duration }), zoomTo: (zoomLevel, options) => store.getState().panZoom?.scaleTo(zoomLevel, { duration: options?.duration }), getZoom: () => store.getState().transform[2], setViewport: (viewport, options) => { const { transform: [tX, tY, tZoom], panZoom, } = store.getState(); panZoom?.setViewport({ x: viewport.x ?? tX, y: viewport.y ?? tY, zoom: viewport.zoom ?? tZoom, }, { duration: options?.duration }); }, getViewport: () => { const [x, y, zoom] = store.getState().transform; return { x, y, zoom }; }, fitView: (options) => { const { nodes, width, height, nodeOrigin, minZoom, maxZoom, panZoom } = store.getState(); return panZoom ? fitView({ nodes, width, height, nodeOrigin, minZoom, maxZoom, panZoom, }, options) : false; }, setCenter: (x, y, options) => { const { width, height, maxZoom, panZoom } = store.getState(); const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom; const centerX = width / 2 - x * nextZoom; const centerY = height / 2 - y * nextZoom; panZoom?.setViewport({ x: centerX, y: centerY, zoom: nextZoom, }, { duration: options?.duration }); }, fitBounds: (bounds, options) => { const { width, height, minZoom, maxZoom, panZoom } = store.getState(); const viewport = getViewportForBounds(bounds, width, height, minZoom, maxZoom, options?.padding ?? 0.1); panZoom?.setViewport(viewport, { duration: options?.duration }); }, screenToFlowPosition: (clientPosition, options = { snapToGrid: true }) => { const { transform, snapGrid, domNode } = store.getState(); if (!domNode) { return clientPosition; } const { x: domX, y: domY } = domNode.getBoundingClientRect(); const correctedPosition = { x: clientPosition.x - domX, y: clientPosition.y - domY, }; return pointToRendererPoint(correctedPosition, transform, options.snapToGrid, snapGrid); }, flowToScreenPosition: (flowPosition) => { const { transform, domNode } = store.getState(); if (!domNode) { return flowPosition; } const { x: domX, y: domY } = domNode.getBoundingClientRect(); const rendererPosition = rendererPointToPoint(flowPosition, transform); return { x: rendererPosition.x + domX, y: rendererPosition.y + domY, }; }, viewportInitialized: panZoomInitialized, }; }, [panZoomInitialized]); return viewportHelperFunctions; }; function handleParentExpand(updatedElements, updateItem) { for (const [index, item] of updatedElements.entries()) { if (item.id === updateItem.parentNode) { const parent = { ...item }; parent.computed ??= {}; const extendWidth = updateItem.position.x + updateItem.computed.width - parent.computed.width; const extendHeight = updateItem.position.y + updateItem.computed.height - parent.computed.height; if (extendWidth > 0 || extendHeight > 0 || updateItem.position.x < 0 || updateItem.position.y < 0) { parent.width = parent.width ?? parent.computed.width; parent.height = parent.height ?? parent.computed.height; if (extendWidth > 0) { parent.width += extendWidth; } if (extendHeight > 0) { parent.height += extendHeight; } if (updateItem.position.x < 0) { const xDiff = Math.abs(updateItem.position.x); parent.position.x = parent.position.x - xDiff; parent.width += xDiff; updateItem.position.x = 0; } if (updateItem.position.y < 0) { const yDiff = Math.abs(updateItem.position.y); parent.position.y = parent.position.y - yDiff; parent.height += yDiff; updateItem.position.y = 0; } parent.computed.width = parent.width; parent.computed.height = parent.height; updatedElements[index] = parent; } break; } } } // This function applies changes to nodes or edges that are triggered by React Flow internally. // When you drag a node for example, React Flow will send a position change update. // This function then applies the changes and returns the updated elements. function applyChanges(changes, elements) { const updatedElements = []; // By storing a map of changes for each element, we can a quick lookup as we // iterate over the elements array! const changesMap = new Map(); for (const change of changes) { if (change.type === 'add') { updatedElements.push(change.item); continue; } else if (change.type === 'remove' || change.type === 'replace') { // For a 'remove' change we can safely ignore any other changes queued for // the same element, it's going to be removed anyway! changesMap.set(change.id, [change]); } else { const elementChanges = changesMap.get(change.id); if (elementChanges) { // If we have some changes queued already, we can do a mutable update of // that array and save ourselves some copying. elementChanges.push(change); } else { changesMap.set(change.id, [change]); } } } for (const element of elements) { const changes = changesMap.get(element.id); // When there are no changes for an element we can just push it unmodified, // no need to copy it. if (!changes) { updatedElements.push(element); continue; } // If we have a 'remove' change queued, it'll be the only change in the array if (changes[0].type === 'remove') { continue; } if (changes[0].type === 'replace') { updatedElements.push({ ...changes[0].item }); continue; } // For other types of changes, we want to start with a shallow copy of the // object so React knows this element has changed. Sequential changes will /// each _mutate_ this object, so there's only ever one copy. const updatedElement = { ...element }; for (const change of changes) { applyChange(change, updatedElement, updatedElements); } updatedElements.push(updatedElement); } return updatedElements; } // Applies a single change to an element. This is a *mutable* update. function applyChange(change, element, elements = []) { switch (change.type) { case 'select': { element.selected = change.selected; break; } case 'position': { if (typeof change.position !== 'undefined') { element.position = change.position; } if (typeof change.positionAbsolute !== 'undefined') { element.computed ??= {}; element.computed.positionAbsolute = change.positionAbsolute; } if (typeof change.dragging !== 'undefined') { element.dragging = change.dragging; } if (element.expandParent) { handleParentExpand(elements, element); } break; } case 'dimensions': { if (typeof change.dimensions !== 'undefined') { element.computed ??= {}; element.computed.width = change.dimensions.width; element.computed.height = change.dimensions.height; if (change.resizing) { element.width = change.dimensions.width; element.height = change.dimensions.height; } } if (typeof change.resizing === 'boolean') { element.resizing = change.resizing; } if (element.expandParent) { handleParentExpand(elements, element); } break; } } } /** * Drop in function that applies node changes to an array of nodes. * @public * @remarks Various events on the <ReactFlow /> component can produce an {@link NodeChange} that describes how to update the edges of your flow in some way. If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges. * @param changes - Array of changes to apply * @param nodes - Array of nodes to apply the changes to * @returns Array of updated nodes * @example * const onNodesChange = useCallback( (changes) => { setNodes((oldNodes) => applyNodeChanges(changes, oldNodes)); }, [setNodes], ); return ( <ReactFLow nodes={nodes} edges={edges} onNodesChange={onNodesChange} /> ); */ function applyNodeChanges(changes, nodes) { return applyChanges(changes, nodes); } /** * Drop in function that applies edge changes to an array of edges. * @public * @remarks Various events on the <ReactFlow /> component can produce an {@link EdgeChange} that describes how to update the edges of your flow in some way. If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges. * @param changes - Array of changes to apply * @param edges - Array of edge to apply the changes to * @returns Array of updated edges * @example * const onEdgesChange = useCallback( (changes) => { setEdges((oldEdges) => applyEdgeChanges(changes, oldEdges)); }, [setEdges], ); return ( <ReactFlow nodes={nodes} edges={edges} onEdgesChange={onEdgesChange} /> ); */ function applyEdgeChanges(changes, edges) { return applyChanges(changes, edges); } const createSelectionChange = (id, selected) => ({ id, type: 'select', selected, }); function getSelectionChanges(items, selectedIds = new Set(), mutateItem = false) { const changes = []; for (const item of items) { const willBeSelected = selectedIds.has(item.id); // we don't want to set all items to selected=false on the first selection if (!(item.selected === undefined && !willBeSelected) && item.selected !== willBeSelected) { if (mutateItem) { // this hack is needed for nodes. When the user dragged a node, it's selected. // When another node gets dragged, we need to deselect the previous one, // in order to have only one selected node at a time - the onNodesChange callback comes too late here :/ item.selected = willBeSelected; } changes.push(createSelectionChange(item.id, willBeSelected)); } } return changes; } function getElementsDiffChanges({ items = [], lookup, }) { const changes = []; const itemsLookup = new Map(items.map((item) => [item.id, item])); for (const item of items) { const storeItem = lookup.get(item.id); if (storeItem !== undefined && storeItem !== item) { changes.push({ id: item.id, item: item, type: 'replace' }); } if (storeItem === undefined) { changes.push({ item: item, type: 'add' }); } } for (const [id] of lookup) { const nextNode = itemsLookup.get(id); if (nextNode === undefined) { changes.push({ id, type: 'remove' }); } } return changes; } /** * Test whether an object is useable as a Node * @public * @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Node if it returns true * @param element - The element to test * @returns A boolean indicating whether the element is an Node */ const isNode = (element) => isNodeBase(element); /** * Test whether an object is useable as an Edge * @public * @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Edge if it returns true * @param element - The element to test * @returns A boolean indicating whether the element is an Edge */ const isEdge = (element) => isEdgeBase(element); /** * Hook for accessing the ReactFlow instance. * * @public * @returns ReactFlowInstance */ function useReactFlow() { const viewportHelper = useViewportHelper(); const store = useStoreApi(); const getNodes = useCallback(() => { return store.getState().nodes.map((n) => ({ ...n })); }, []); const getNode = useCallback((id) => { return store.getState().nodeLookup.get(id); }, []); const getEdges = useCallback(() => { const { edges = [] } = store.getState(); return edges.map((e) => ({ ...e })); }, []); const getEdge = useCallback((id) => { const { edges = [] } = store.getState(); return edges.find((e) => e.id === id); }, []); // this is used to handle multiple syncronous setNodes calls const setNodesData = useRef(); const setNodesTimeout = useRef(); const setNodes = useCallback((payload) => { const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState(); const nextNodes = typeof payload === 'function' ? payload(setNodesData.current || nodes) : payload; setNodesData.current = nextNodes; if (setNodesTimeout.current) { clearTimeout(setNodesTimeout.current); } // if there are multiple synchronous setNodes calls, we only want to call onNodesChange once // for this, we use a timeout to wait for the last call and store updated nodes in setNodesData // this is not perfect, but should work in most cases setNodesTimeout.current = setTimeout(() => { if (hasDefaultNodes) { setNodes(nextNodes); } else if (onNodesChange) { const changes = getElementsDiffChanges({ items: setNodesData.current, lookup: nodeLookup }); onNodesChange(changes); } setNodesData.current = undefined; }, 0); }, []); // this is used to handle multiple syncronous setEdges calls const setEdgesData = useRef(); const setEdgesTimeout = useRef(); const setEdges = useCallback((payload) => { const { edges = [], setEdges, hasDefaultEdges, onEdgesChange, edgeLookup } = store.getState(); const nextEdges = typeof payload === 'function' ? payload(setEdgesData.current || edges) : payload; setEdgesData.current = nextEdges; if (setEdgesTimeout.current) { clearTimeout(setEdgesTimeout.current); } setEdgesTimeout.current = setTimeout(() => { if (hasDefaultEdges) { setEdges(nextEdges); } else if (onEdgesChange) { const changes = getElementsDiffChanges({ items: nextEdges, lookup: edgeLookup }); onEdgesChange(changes); } setEdgesData.current = undefined; }, 0); }, []); const addNodes = useCallback((payload) => { const nodes = Array.isArray(payload) ? payload : [payload]; const { nodes: currentNodes, hasDefaultNodes, onNodesChange, setNodes } = store.getState(); if (hasDefaultNodes) { const nextNodes = [...currentNodes, ...nodes]; setNodes(nextNodes); } else if (onNodesChange) { const changes = nodes.map((node) => ({ item: node, type: 'add' })); onNodesChange(changes); } }, []); const addEdges = useCallback((payload) => { const nextEdges = Array.isArray(payload) ? payload : [payload]; const { edges = [], setEdges, hasDefaultEdges, onEdgesChange } = store.getState(); if (hasDefaultEdges) { setEdges([...edges, ...nextEdges]); } else if (onEdgesChange) { const changes = nextEdges.map((edge) => ({ item: edge, type: 'add' })); onEdgesChange(changes); } }, []); const toObject = useCallback(() => { const { nodes = [], edges = [], transform } = store.getState(); const [x, y, zoom] = transform; return { nodes: nodes.map((n) => ({ ...n })), edges: edges.map((e) => ({ ...e })), viewport: { x, y, zoom, }, }; }, []); const deleteElements = useCallback(async ({ nodes: nodesToRemove = [], edges: edgesToRemove = [] }) => { const { nodes, edges, hasDefaultNodes, hasDefaultEdges, onNodesDelete, onEdgesDelete, onNodesChange, onEdgesChange, onDelete, onBeforeDelete, } = store.getState(); const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove({ nodesToRemove, edgesToRemove, nodes, edges, onBeforeDelete, }); const hasMatchingEdges = matchingEdges.length > 0; const hasMatchingNodes = matchingNodes.length > 0; if (hasMatchingEdges) { if (hasDefaultEdges) { const nextEdges = edges.filter((e) => !matchingEdges.some((mE) => mE.id === e.id)); store.getState().setEdges(nextEdges); } onEdgesDelete?.(matchingEdges); onEdgesChange?.(matchingEdges.map((edge) => ({ id: edge.id, type: 'remove', }))); } if (hasMatchingNodes) { if (hasDefaultNodes) { const nextNodes = nodes.filter((n) => !matchingNodes.some((mN) => mN.id === n.id)); store.getState().setNodes(nextNodes); } onNodesDelete?.(matchingNodes); onNodesChange?.(matchingNodes.map((node) => ({ id: node.id, type: 'remove' }))); } if (hasMatchingNodes || hasMatchingEdges) { onDelete?.({ nodes: matchingNodes, edges: matchingEdges }); } return { deletedNodes: matchingNodes, deletedEdges: matchingEdges }; }, []); const getNodeRect = useCallback((nodeOrRect) => { const isRect = isRectObject(nodeOrRect); const node = isRect ? null : store.getState().nodeLookup.get(nodeOrRect.id); if (!isRect && !node) { return [null, null, isRect]; } const nodeRect = isRect ? nodeOrRect : nodeToRect(node); return [nodeRect, node, isRect]; }, []); const getIntersectingNodes = useCallback((nodeOrRect, partially = true, nodes) => { const [nodeRect, node, isRect] = getNodeRect(nodeOrRect); if (!nodeRect) { return []; } return (nodes || store.getState().nodes).filter((n) => { if (!isRect && (n.id === node.id || !n.computed?.positionAbsolute)) { return false; } const currNodeRect = nodeToRect(n); const overlappingArea = getOverlappingArea(currNodeRect, nodeRect); const partiallyVisible = partially && overlappingArea > 0; return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height; }); }, []); const isNodeIntersecting = useCallback((nodeOrRect, area, partially = true) => { const [nodeRect] = getNodeRect(nodeOrRect); if (!nodeRect) { return false; } const overlappingArea = getOverlappingArea(nodeRect, area); const partiallyVisible = partially && overlappingArea > 0; return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height; }, []); const updateNode = useCallback((id, nodeUpdate, options = { replace: true }) => { setNodes((prevNodes) => prevNodes.map((node) => { if (node.id === id) { const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node) : nodeUpdate; return options.replace && isNode(nextNode) ? nextNode : { ...node, ...nextNode }; } return node; })); }, [setNodes]); const updateNodeData = useCallback((id, dataUpdate, options = { replace: false }) => { updateNode(id, (node) => { const nextData = typeof dataUpdate === 'function' ? dataUpdate(node) : dataUpdate; return options.replace ? { ...node, data: nextData } : { ...node, data: { ...node.data, ...nextData } }; }, options); }, [updateNode]); return useMemo(() => { return { ...viewportHelper, getNodes, getNode, getEdges, getEdge, setNodes, setEdges, addNodes, addEdges, toObject, deleteElements, getIntersectingNodes, isNodeIntersecting, updateNode, updateNodeData, }; }, [ viewportHelper, getNodes, getNode, getEdges, getEdge, setNodes, setEdges, addNodes, addEdges, toObject, deleteElements, getIntersectingNodes, isNodeIntersecting, updateNode, updateNodeData, ]); } const selected = (item) => item.selected; const deleteKeyOptions = { actInsideInputWithModifier: false }; /** * Hook for handling global key events. * * @internal */ function useGlobalKeyHandler({ deleteKeyCode, multiSelectionKeyCode, }) { const store = useStoreApi(); const { deleteElements } = useReactFlow(); const deleteKeyPressed = useKeyPress(deleteKeyCode, deleteKeyOptions); const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode); useEffect(() => { if (deleteKeyPressed) { const { edges, nodes } = store.getState(); deleteElements({ nodes: nodes.filter(selected), edges: edges.filter(selected) }); store.setState({ nodesSelectionActive: false }); } }, [deleteKeyPressed]); useEffect(() => { store.setState({ multiSelectionActive: multiSelectionKeyPressed }); }, [multiSelectionKeyPressed]); } /** * Hook for handling resize events. * * @internal */ function useResizeHandler(domNode) { const store = useStoreApi(); useEffect(() => { const updateDimensions = () => { if (!domNode.current) { return false; } const size = getDimensions(domNode.current); if (size.height === 0 || size.width === 0) { store.getState().onError?.('004', errorMessages['error004']()); } store.setState({ width: size.width || 500, height: size.height || 500 }); }; if (domNode.current) { updateDimensions(); window.addEventListener('resize', updateDimensions); const resizeObserver = new ResizeObserver(() => updateDimensions()); resizeObserver.observe(domNode.current); return () => { window.removeEventListener('resize', updateDimensions); if (resizeObserver && domNode.current) { resizeObserver.unobserve(domNode.current); } }; } }, []); } const containerStyle = { position: 'absolute', width: '100%', height: '100%', top: 0, left: 0, }; const selector$l = (s) => ({ userSelectionActive: s.userSelectionActive, lib: s.lib, }); function ZoomPane({ onPaneContextMenu, zoomOnScroll = true, zoomOnPinch = true, panOnScroll = false, panOnScrollSpeed = 0.5, panOnScrollMode = PanOnScrollMode.Free, zoomOnDoubleClick = true, panOnDrag = true, defaultViewport, translateExtent, minZoom, maxZoom, zoomActivationKeyCode, preventScrolling = true, children, noWheelClassName, noPanClassName, onViewportChange, isControlledViewport, }) { const store = useStoreApi(); const zoomPane = useRef(null); const { userSelectionActive, lib } = useStore(selector$l, shallow); const zoomActivationKeyPressed = useKeyPress(zoomActivationKeyCode); const panZoom = useRef(); useResizeHandler(zoomPane); useEffect(() => { if (zoomPane.current) { panZoom.current = XYPanZoom({ domNode: zoomPane.current, minZoom, maxZoom, translateExtent, viewport: defaultViewport, onTransformChange: (transform) => { onViewportChange?.({ x: transform[0], y: transform[1], zoom: transform[2] }); if (!isControlledViewport) { store.setState({ transform }); } }, onDraggingChange: (paneDragging) => store.setState({ paneDragging }), onPanZoomStart: (event, vp) => { const { onViewportChangeStart, onMoveStart } = store.getState(); onMoveStart?.(event, vp); onViewportChangeStart?.(vp); }, onPanZoom: (event, vp) => { const { onViewportChange, onMove } = store.getState(); onMove?.(event, vp); onViewportChange?.(vp); }, onPanZoomEnd: (event, vp) => { const { onViewportChangeEnd, onMoveEnd } = store.getState(); onMoveEnd?.(event, vp); onViewportChangeEnd?.(vp); }, }); const { x, y, zoom } = panZoom.current.getViewport(); store.setState({ panZoom: panZoom.current, transform: [x, y, zoom], domNode: zoomPane.current.closest('.react-flow'), }); return () => { panZoom.current?.destroy(); }; } }, []); useEffect(() => { panZoom.current?.update({ onPaneContextMenu, zoomOnScroll, zoomOnPinch, panOnScroll, panOnScrollSpeed, panOnScrollMode, zoomOnDoubleClick, panOnDrag, zoomActivationKeyPressed, preventScrolling, noPanClassName, userSelectionActive, noWheelClassName, lib, }); }, [ onPaneContextMenu, zoomOnScroll, zoomOnPinch, panOnScroll, panOnScrollSpeed, panOnScrollMode, zoomOnDoubleClick, panOnDrag, zoomActivationKeyPressed, preventScrolling, noPanClassName, userSelectionActive, noWheelClassName, lib, ]); return (jsx("div", { className: "react-flow__renderer", ref: zoomPane, style: containerStyle, children: children })); } const selector$k = (s) => ({ userSelectionActive: s.userSelectionActive, userSelectionRect: s.userSelectionRect, }); function UserSelection() { const { userSelectionActive, userSelectionRect } = useStore(selector$k, shallow); const isActive = userSelectionActive && userSelectionRect; if (!isActive) { return null; } return (jsx("div", { className: "react-flow__selection react-flow__container", style: { width: userSelectionRect.width, height: userSelectionRect.height, transform: `translate(${userSelectionRect.x}px, ${userSelectionRect.y}px)`, } })); } const wrapHandler = (handler, containerRef) => { return (event) => { if (event.target !== containerRef.current) { return; } handler?.(event); }; }; const selector$j = (s) => ({ userSelectionActive: s.userSelectionActive, elementsSelectable: s.elementsSelectable, dragging: s.paneDragging, }); function Pane({ isSelecting, selectionMode = SelectionMode.Full, panOnDrag, onSelectionStart, onSelectionEnd, onPaneClick, onPaneContextMenu, onPaneScroll, onPaneMouseEnter, onPaneMouseMove, onPaneMouseLeave, children, }) { const container = useRef(null); const store = useStoreApi(); const prevSelectedNodesCount = useRef(0); const prevSelectedEdgesCount = useRef(0); const containerBounds = useRef(); const { userSelectionActive, elementsSelectable, dragging } = useStore(selector$j, shallow); const resetUserSelection = () => { store.setState({ userSelectionActive: false, userSelectionRect: null }); prevSelectedNodesCount.current = 0; prevSelectedEdgesCount.current = 0; }; const onClick = (event) => { onPaneClick?.(event); store.getState().resetSelectedElements(); store.setState({ nodesSelectionActive: false }); }; const onContextMenu = (event) => { if (Array.isArray(panOnDrag) && panOnDrag?.includes(2)) { event.preventDefault(); return; } onPaneContextMenu?.(event); }; const onWheel = onPaneScroll ? (event) => onPaneScroll(event) : undefined; const onMouseDown = (event) => { const { resetSelectedElements, domNode } = store.getState(); containerBounds.current = domNode?.getBoundingClientRect(); if (!elementsSelectable || !isSelecting || event.button !== 0 || event.target !== container.current || !containerBounds.current) { return; } const { x, y } = getEventPosition(event.nativeEvent, containerBounds.current); resetSelectedElements(); store.setState({ userSelectionRect: { width: 0, height: 0, startX: x, startY: y, x, y, }, }); onSelectionStart?.(event); }; const onMouseMove = (event) => { const { userSelectionRect, edges, transform, nodeOrigin, nodes, onNodesChange, onEdgesChange } = store.getState(); if (!isSelecting || !containerBounds.current || !userSelectionRect) { return; } store.setState({ userSelectionActive: true, nodesSelectionActive: false }); const mousePos = getEventPosition(event.nativeEvent, containerBounds.current); const startX = userSelectionRect.startX ?? 0; const startY = userSelectionRect.startY ?? 0; const nextUserSelectRect = { ...userSelectionRect, x: mousePos.x < startX ? mousePos.x : startX, y: mousePos.y < startY ? mousePos.y : startY, width: Math.abs(mousePos.x - startX), height: Math.abs(mousePos.y - startY), }; const selectedNodes = getNodesInside(nodes, nextUserSelectRect, transform, selectionMode === SelectionMode.Partial, true, nodeOrigin); const selectedEdgeIds = new Set(); const selectedNodeIds = new Set(); for (const selectedNode of selectedNodes) { selectedNodeIds.add(selectedNode.id); for (const edge of edges) { if (edge.source === selectedNode.id || edge.target === selectedNode.id) { selectedEdgeIds.add(edge.id); } } } if (prevSelectedNodesCount.current !== selectedNodeIds.size) { prevSelectedNodesCount.current = selectedNodeIds.size; const changes = getSelectionChanges(nodes, selectedNodeIds, true); if (changes.length) { onNodesChange?.(changes); } } if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) { prevSelectedEdgesCount.current = selectedEdgeIds.size; const changes = getSelectionChanges(edges, selectedEdgeIds); if (changes.length) { onEdgesChange?.(changes); } } store.setState({ userSelectionRect: nextUserSelectRect,