Merge branch 'main' into middlewares
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { useCallback } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { getEdgeToolbarTransform } from '@xyflow/system';
|
||||
|
||||
import { EdgeLabelRenderer } from '../../components/EdgeLabelRenderer';
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { Edge, ReactFlowState } from '../../types';
|
||||
import type { EdgeToolbarProps } from './types';
|
||||
|
||||
const zoomSelector = (state: ReactFlowState) => state.transform[2];
|
||||
|
||||
/**
|
||||
* This component can render a toolbar or tooltip to one side of a custom edge. This
|
||||
* toolbar doesn't scale with the viewport so that the content stays the same size.
|
||||
*
|
||||
* @public
|
||||
* @example
|
||||
* ```jsx
|
||||
* import { EdgeToolbar, BaseEdge, getBezierPath, type EdgeProps } from "@xyflow/react";
|
||||
*
|
||||
* export function CustomEdge({ id, data, ...props }: EdgeProps) {
|
||||
* const [edgePath, centerX, centerY] = getBezierPath(props);
|
||||
*
|
||||
* return (
|
||||
* <>
|
||||
* <BaseEdge id={id} path={edgePath} />
|
||||
* <EdgeToolbar edgeId={id} x={centerX} y={centerY} isVisible>
|
||||
* <button onClick={() => console.log('edge', id, 'click')}}>Click me</button>
|
||||
* </EdgeToolbar>
|
||||
* </>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function EdgeToolbar({
|
||||
edgeId,
|
||||
x,
|
||||
y,
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
isVisible,
|
||||
alignX = 'center',
|
||||
alignY = 'center',
|
||||
...rest
|
||||
}: EdgeToolbarProps) {
|
||||
const edgeSelector = useCallback((state: ReactFlowState): Edge | undefined => state.edgeLookup.get(edgeId), [edgeId]);
|
||||
const edge = useStore(edgeSelector, shallow);
|
||||
const isActive = typeof isVisible === 'boolean' ? isVisible : edge?.selected;
|
||||
const zoom = useStore(zoomSelector);
|
||||
|
||||
if (!isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const zIndex = (edge?.zIndex ?? 0) + 1;
|
||||
const transform = getEdgeToolbarTransform(x, y, zoom, alignX, alignY);
|
||||
|
||||
return (
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform,
|
||||
zIndex,
|
||||
pointerEvents: 'all',
|
||||
transformOrigin: '0 0',
|
||||
...style,
|
||||
}}
|
||||
className={cc(['react-flow__edge-toolbar', className])}
|
||||
data-id={edge?.id ?? ''}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { EdgeToolbar } from './EdgeToolbar';
|
||||
export type { EdgeToolbarProps } from './types';
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import type { EdgeToolbarBaseProps } from '@xyflow/system';
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type EdgeToolbarProps = EdgeToolbarBaseProps &
|
||||
HTMLAttributes<HTMLDivElement> & {
|
||||
/**
|
||||
* An edge toolbar must be attached to an edge.
|
||||
*/
|
||||
edgeId: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
@@ -3,3 +3,4 @@ export * from './Controls';
|
||||
export * from './MiniMap';
|
||||
export * from './NodeResizer';
|
||||
export * from './NodeToolbar';
|
||||
export * from './EdgeToolbar';
|
||||
|
||||
@@ -69,7 +69,7 @@ const ConnectionLine = <NodeType extends Node = Node>({
|
||||
CustomComponent,
|
||||
isValid,
|
||||
}: ConnectionLineProps<NodeType>) => {
|
||||
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition } =
|
||||
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition, pointer } =
|
||||
useConnection<NodeType>();
|
||||
|
||||
if (!inProgress) {
|
||||
@@ -92,6 +92,7 @@ const ConnectionLine = <NodeType extends Node = Node>({
|
||||
connectionStatus={getConnectionStatus(isValid)}
|
||||
toNode={toNode}
|
||||
toHandle={toHandle}
|
||||
pointer={pointer}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo, useRef, type KeyboardEvent, useCallback, JSX } from 'react';
|
||||
import { useState, useMemo, useRef, type KeyboardEvent, useCallback, JSX, memo } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import {
|
||||
@@ -15,7 +15,7 @@ import { builtinEdgeTypes, nullPosition } from './utils';
|
||||
import { EdgeUpdateAnchors } from './EdgeUpdateAnchors';
|
||||
import type { Edge, EdgeWrapperProps } from '../../types';
|
||||
|
||||
export function EdgeWrapper<EdgeType extends Edge = Edge>({
|
||||
function EdgeWrapper<EdgeType extends Edge = Edge>({
|
||||
id,
|
||||
edgesFocusable,
|
||||
edgesReconnectable,
|
||||
@@ -263,3 +263,5 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(EdgeWrapper) as typeof EdgeWrapper;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type MouseEvent, type KeyboardEvent } from 'react';
|
||||
import { type MouseEvent, type KeyboardEvent, memo } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import {
|
||||
@@ -20,7 +20,7 @@ import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from '.
|
||||
import { useNodeObserver } from './useNodeObserver';
|
||||
import type { InternalNode, Node, NodeWrapperProps } from '../../types';
|
||||
|
||||
export function NodeWrapper<NodeType extends Node>({
|
||||
function NodeWrapper<NodeType extends Node>({
|
||||
id,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
@@ -249,3 +249,5 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(NodeWrapper) as typeof NodeWrapper;
|
||||
|
||||
@@ -69,7 +69,6 @@ const reactFlowFieldsToTrack = [
|
||||
'onBeforeDelete',
|
||||
'debug',
|
||||
'autoPanSpeed',
|
||||
'paneClickDistance',
|
||||
'ariaLabelConfig',
|
||||
] as const;
|
||||
|
||||
@@ -93,7 +92,6 @@ const selector = (s: ReactFlowState) => ({
|
||||
setNodeExtent: s.setNodeExtent,
|
||||
reset: s.reset,
|
||||
setDefaultNodesAndEdges: s.setDefaultNodesAndEdges,
|
||||
setPaneClickDistance: s.setPaneClickDistance,
|
||||
});
|
||||
|
||||
const initPrevValues = {
|
||||
@@ -109,7 +107,6 @@ const initPrevValues = {
|
||||
elementsSelectable: true,
|
||||
noPanClassName: 'nopan',
|
||||
rfId: '1',
|
||||
paneClickDistance: 0,
|
||||
};
|
||||
|
||||
export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
||||
@@ -124,7 +121,6 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
|
||||
setNodeExtent,
|
||||
reset,
|
||||
setDefaultNodesAndEdges,
|
||||
setPaneClickDistance,
|
||||
} = useStore(selector, shallow);
|
||||
const store = useStoreApi<NodeType, EdgeType>();
|
||||
|
||||
@@ -155,7 +151,6 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
|
||||
else if (fieldName === 'maxZoom') setMaxZoom(fieldValue as number);
|
||||
else if (fieldName === 'translateExtent') setTranslateExtent(fieldValue as CoordinateExtent);
|
||||
else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue as CoordinateExtent);
|
||||
else if (fieldName === 'paneClickDistance') setPaneClickDistance(fieldValue as number);
|
||||
else if (fieldName === 'ariaLabelConfig')
|
||||
store.setState({ ariaLabelConfig: mergeAriaLabelConfig(fieldValue as AriaLabelConfig) });
|
||||
// Renamed fields
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useStore } from '../../hooks/useStore';
|
||||
import { useVisibleEdgeIds } from '../../hooks/useVisibleEdgeIds';
|
||||
import MarkerDefinitions from './MarkerDefinitions';
|
||||
import { GraphViewProps } from '../GraphView';
|
||||
import { EdgeWrapper } from '../../components/EdgeWrapper';
|
||||
import EdgeWrapper from '../../components/EdgeWrapper';
|
||||
import type { Edge, ReactFlowState, Node } from '../../types';
|
||||
|
||||
type EdgeRendererProps<EdgeType extends Edge = Edge> = Pick<
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { memo, type ReactNode } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { useGlobalKeyHandler } from '../../hooks/useGlobalKeyHandler';
|
||||
@@ -72,7 +73,7 @@ function FlowRendererComponent<NodeType extends Node = Node>({
|
||||
onViewportChange,
|
||||
isControlledViewport,
|
||||
}: FlowRendererProps<NodeType>) {
|
||||
const { nodesSelectionActive, userSelectionActive } = useStore(selector);
|
||||
const { nodesSelectionActive, userSelectionActive } = useStore(selector, shallow);
|
||||
const selectionKeyPressed = useKeyPress(selectionKeyCode, { target: win });
|
||||
const panActivationKeyPressed = useKeyPress(panActivationKeyCode, { target: win });
|
||||
|
||||
@@ -105,6 +106,7 @@ function FlowRendererComponent<NodeType extends Node = Node>({
|
||||
onViewportChange={onViewportChange}
|
||||
isControlledViewport={isControlledViewport}
|
||||
paneClickDistance={paneClickDistance}
|
||||
selectionOnDrag={_selectionOnDrag}
|
||||
>
|
||||
<Pane
|
||||
onSelectionStart={onSelectionStart}
|
||||
@@ -119,6 +121,7 @@ function FlowRendererComponent<NodeType extends Node = Node>({
|
||||
isSelecting={!!isSelecting}
|
||||
selectionMode={selectionMode}
|
||||
selectionKeyPressed={selectionKeyPressed}
|
||||
paneClickDistance={paneClickDistance}
|
||||
selectionOnDrag={_selectionOnDrag}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useStore } from '../../hooks/useStore';
|
||||
import { containerStyle } from '../../styles/utils';
|
||||
import { GraphViewProps } from '../GraphView';
|
||||
import { useResizeObserver } from './useResizeObserver';
|
||||
import { NodeWrapper } from '../../components/NodeWrapper';
|
||||
import NodeWrapper from '../../components/NodeWrapper';
|
||||
import type { Node, ReactFlowState } from '../../types';
|
||||
|
||||
export type NodeRendererProps<NodeType extends Node> = Pick<
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
/**
|
||||
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift
|
||||
*/
|
||||
|
||||
import {
|
||||
useRef,
|
||||
type MouseEventHandler,
|
||||
type MutableRefObject,
|
||||
type MouseEvent as ReactMouseEvent,
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
type WheelEvent as ReactWheelEvent,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import cc from 'classcat';
|
||||
import {
|
||||
getNodesInside,
|
||||
getEventPosition,
|
||||
SelectionMode,
|
||||
areSetsEqual,
|
||||
type NodeChange,
|
||||
type EdgeChange,
|
||||
} from '@xyflow/system';
|
||||
import { getNodesInside, getEventPosition, SelectionMode, areSetsEqual } from '@xyflow/system';
|
||||
|
||||
import { UserSelection } from '../../components/UserSelection';
|
||||
import { containerStyle } from '../../styles/utils';
|
||||
@@ -29,6 +21,7 @@ type PaneProps = {
|
||||
isSelecting: boolean;
|
||||
selectionKeyPressed: boolean;
|
||||
children: ReactNode;
|
||||
paneClickDistance: number;
|
||||
} & Partial<
|
||||
Pick<
|
||||
ReactFlowProps,
|
||||
@@ -47,9 +40,9 @@ type PaneProps = {
|
||||
>;
|
||||
|
||||
const wrapHandler = (
|
||||
handler: React.MouseEventHandler | undefined,
|
||||
containerRef: React.MutableRefObject<HTMLDivElement | null>
|
||||
): React.MouseEventHandler => {
|
||||
handler: MouseEventHandler | undefined,
|
||||
containerRef: MutableRefObject<HTMLDivElement | null>
|
||||
): MouseEventHandler => {
|
||||
return (event: ReactMouseEvent) => {
|
||||
if (event.target !== containerRef.current) {
|
||||
return;
|
||||
@@ -70,6 +63,7 @@ export function Pane({
|
||||
selectionKeyPressed,
|
||||
selectionMode = SelectionMode.Full,
|
||||
panOnDrag,
|
||||
paneClickDistance,
|
||||
selectionOnDrag,
|
||||
onSelectionStart,
|
||||
onSelectionEnd,
|
||||
@@ -83,17 +77,15 @@ export function Pane({
|
||||
}: PaneProps) {
|
||||
const store = useStoreApi();
|
||||
const { userSelectionActive, elementsSelectable, dragging, connectionInProgress } = useStore(selector, shallow);
|
||||
const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive);
|
||||
const isSelectionEnabled = elementsSelectable && (isSelecting || userSelectionActive);
|
||||
|
||||
const container = useRef<HTMLDivElement | null>(null);
|
||||
const containerBounds = useRef<DOMRect>();
|
||||
|
||||
const selectedNodeIds = useRef<Set<string>>(new Set());
|
||||
const selectedEdgeIds = useRef<Set<string>>(new Set());
|
||||
|
||||
// Used to prevent click events when the user lets go of the selectionKey during a selection
|
||||
const selectionInProgress = useRef<boolean>(false);
|
||||
const selectionStarted = useRef<boolean>(false);
|
||||
|
||||
const onClick = (event: ReactMouseEvent) => {
|
||||
// We prevent click events when the user let go of the selectionKey during a selection
|
||||
@@ -117,31 +109,37 @@ export function Pane({
|
||||
onPaneContextMenu?.(event);
|
||||
};
|
||||
|
||||
const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined;
|
||||
const onWheel = onPaneScroll ? (event: ReactWheelEvent) => onPaneScroll(event) : undefined;
|
||||
|
||||
const onPointerDown = (event: ReactPointerEvent): void => {
|
||||
const { resetSelectedElements, domNode } = store.getState();
|
||||
const onClickCapture = (event: ReactMouseEvent) => {
|
||||
if (selectionInProgress.current) {
|
||||
event.stopPropagation();
|
||||
selectionInProgress.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
// We are using capture here in order to prevent other pointer events
|
||||
// to be able to create a selection above a node or an edge
|
||||
const onPointerDownCapture = (event: ReactPointerEvent): void => {
|
||||
const { domNode } = store.getState();
|
||||
containerBounds.current = domNode?.getBoundingClientRect();
|
||||
if (!containerBounds.current) return;
|
||||
|
||||
if (
|
||||
!elementsSelectable ||
|
||||
!isSelecting ||
|
||||
event.button !== 0 ||
|
||||
event.target !== container.current ||
|
||||
!containerBounds.current
|
||||
) {
|
||||
const eventTargetIsContainer = event.target === container.current;
|
||||
// if a child element has the 'nokey' class, we don't want to swallow the event and don't start a selection
|
||||
const isNoKeyEvent = !eventTargetIsContainer && !!(event.target as HTMLElement).closest('.nokey');
|
||||
const isSelectionActive = (selectionOnDrag && eventTargetIsContainer) || selectionKeyPressed;
|
||||
|
||||
if (isNoKeyEvent || !isSelecting || !isSelectionActive || event.button !== 0 || !event.isPrimary) {
|
||||
return;
|
||||
}
|
||||
|
||||
(event.target as Partial<Element> | null)?.setPointerCapture?.(event.pointerId);
|
||||
(event.target as Partial<Element>)?.setPointerCapture?.(event.pointerId);
|
||||
|
||||
selectionStarted.current = true;
|
||||
selectionInProgress.current = false;
|
||||
|
||||
const { x, y } = getEventPosition(event.nativeEvent, containerBounds.current);
|
||||
|
||||
resetSelectedElements();
|
||||
|
||||
store.setState({
|
||||
userSelectionRect: {
|
||||
width: 0,
|
||||
@@ -153,7 +151,10 @@ export function Pane({
|
||||
},
|
||||
});
|
||||
|
||||
onSelectionStart?.(event);
|
||||
if (!eventTargetIsContainer) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerMove = (event: ReactPointerEvent): void => {
|
||||
@@ -166,17 +167,28 @@ export function Pane({
|
||||
triggerNodeChanges,
|
||||
triggerEdgeChanges,
|
||||
defaultEdgeOptions,
|
||||
resetSelectedElements,
|
||||
} = store.getState();
|
||||
|
||||
if (!containerBounds.current || !userSelectionRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectionInProgress.current = true;
|
||||
|
||||
const { x: mouseX, y: mouseY } = getEventPosition(event.nativeEvent, containerBounds.current);
|
||||
const { startX, startY } = userSelectionRect;
|
||||
|
||||
if (!selectionInProgress.current) {
|
||||
const requiredDistance = selectionKeyPressed ? 0 : paneClickDistance;
|
||||
const distance = Math.hypot(mouseX - startX, mouseY - startY);
|
||||
if (distance <= requiredDistance) {
|
||||
return;
|
||||
}
|
||||
resetSelectedElements();
|
||||
onSelectionStart?.(event);
|
||||
}
|
||||
|
||||
selectionInProgress.current = true;
|
||||
|
||||
const nextUserSelectRect = {
|
||||
startX,
|
||||
startY,
|
||||
@@ -211,12 +223,12 @@ export function Pane({
|
||||
}
|
||||
|
||||
if (!areSetsEqual(prevSelectedNodeIds, selectedNodeIds.current)) {
|
||||
const changes = getSelectionChanges(nodeLookup, selectedNodeIds.current, true) as NodeChange[];
|
||||
const changes = getSelectionChanges(nodeLookup, selectedNodeIds.current, true);
|
||||
triggerNodeChanges(changes);
|
||||
}
|
||||
|
||||
if (!areSetsEqual(prevSelectedEdgeIds, selectedEdgeIds.current)) {
|
||||
const changes = getSelectionChanges(edgeLookup, selectedEdgeIds.current) as EdgeChange[];
|
||||
const changes = getSelectionChanges(edgeLookup, selectedEdgeIds.current);
|
||||
triggerEdgeChanges(changes);
|
||||
}
|
||||
|
||||
@@ -228,37 +240,32 @@ export function Pane({
|
||||
};
|
||||
|
||||
const onPointerUp = (event: ReactPointerEvent) => {
|
||||
if (event.button !== 0 || !selectionStarted.current) {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
(event.target as Partial<Element>)?.releasePointerCapture?.(event.pointerId);
|
||||
const { userSelectionRect } = store.getState();
|
||||
|
||||
/*
|
||||
* We only want to trigger click functions when in selection mode if
|
||||
* the user did not move the mouse.
|
||||
*/
|
||||
if (!userSelectionActive && userSelectionRect && event.target === container.current) {
|
||||
if (!userSelectionActive && event.target === container.current && store.getState().userSelectionRect) {
|
||||
onClick?.(event);
|
||||
}
|
||||
|
||||
store.setState({
|
||||
userSelectionActive: false,
|
||||
userSelectionRect: null,
|
||||
nodesSelectionActive: selectedNodeIds.current.size > 0,
|
||||
});
|
||||
onSelectionEnd?.(event);
|
||||
|
||||
/*
|
||||
* If the user kept holding the selectionKey during the selection,
|
||||
* we need to reset the selectionInProgress, so the next click event is not prevented
|
||||
*/
|
||||
if (selectionKeyPressed || selectionOnDrag) {
|
||||
selectionInProgress.current = false;
|
||||
if (selectionInProgress.current) {
|
||||
onSelectionEnd?.(event);
|
||||
|
||||
store.setState({
|
||||
nodesSelectionActive: selectedNodeIds.current.size > 0,
|
||||
});
|
||||
}
|
||||
|
||||
selectionStarted.current = false;
|
||||
};
|
||||
|
||||
const draggable = panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0));
|
||||
@@ -266,13 +273,14 @@ export function Pane({
|
||||
return (
|
||||
<div
|
||||
className={cc(['react-flow__pane', { draggable, dragging, selection: isSelecting }])}
|
||||
onClick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
|
||||
onClick={isSelectionEnabled ? undefined : wrapHandler(onClick, container)}
|
||||
onContextMenu={wrapHandler(onContextMenu, container)}
|
||||
onWheel={wrapHandler(onWheel, container)}
|
||||
onPointerEnter={hasActiveSelection ? undefined : onPaneMouseEnter}
|
||||
onPointerDown={hasActiveSelection ? onPointerDown : onPaneMouseMove}
|
||||
onPointerMove={hasActiveSelection ? onPointerMove : onPaneMouseMove}
|
||||
onPointerUp={hasActiveSelection ? onPointerUp : undefined}
|
||||
onPointerEnter={isSelectionEnabled ? undefined : onPaneMouseEnter}
|
||||
onPointerMove={isSelectionEnabled ? onPointerMove : onPaneMouseMove}
|
||||
onPointerUp={isSelectionEnabled ? onPointerUp : undefined}
|
||||
onPointerDownCapture={isSelectionEnabled ? onPointerDownCapture : undefined}
|
||||
onClickCapture={isSelectionEnabled ? onClickCapture : undefined}
|
||||
onPointerLeave={onPaneMouseLeave}
|
||||
ref={container}
|
||||
style={containerStyle}
|
||||
|
||||
@@ -104,7 +104,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
||||
onPaneMouseLeave,
|
||||
onPaneScroll,
|
||||
onPaneContextMenu,
|
||||
paneClickDistance = 0,
|
||||
paneClickDistance = 1,
|
||||
nodeClickDistance = 0,
|
||||
children,
|
||||
onReconnect,
|
||||
@@ -309,7 +309,6 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
||||
nodeDragThreshold={nodeDragThreshold}
|
||||
connectionDragThreshold={connectionDragThreshold}
|
||||
onBeforeDelete={onBeforeDelete}
|
||||
paneClickDistance={paneClickDistance}
|
||||
debug={debug}
|
||||
ariaLabelConfig={ariaLabelConfig}
|
||||
/>
|
||||
|
||||
@@ -12,12 +12,7 @@ import type { ReactFlowState } from '../../types';
|
||||
|
||||
type ZoomPaneProps = Omit<
|
||||
FlowRendererProps,
|
||||
| 'deleteKeyCode'
|
||||
| 'selectionKeyCode'
|
||||
| 'multiSelectionKeyCode'
|
||||
| 'noDragClassName'
|
||||
| 'disableKeyboardA11y'
|
||||
| 'selectionOnDrag'
|
||||
'deleteKeyCode' | 'selectionKeyCode' | 'multiSelectionKeyCode' | 'noDragClassName' | 'disableKeyboardA11y'
|
||||
> & {
|
||||
isControlledViewport: boolean;
|
||||
};
|
||||
@@ -25,6 +20,7 @@ type ZoomPaneProps = Omit<
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
userSelectionActive: s.userSelectionActive,
|
||||
lib: s.lib,
|
||||
connectionInProgress: s.connection.inProgress,
|
||||
});
|
||||
|
||||
export function ZoomPane({
|
||||
@@ -48,10 +44,11 @@ export function ZoomPane({
|
||||
onViewportChange,
|
||||
isControlledViewport,
|
||||
paneClickDistance,
|
||||
selectionOnDrag,
|
||||
}: ZoomPaneProps) {
|
||||
const store = useStoreApi();
|
||||
const zoomPane = useRef<HTMLDivElement>(null);
|
||||
const { userSelectionActive, lib } = useStore(selector, shallow);
|
||||
const { userSelectionActive, lib, connectionInProgress } = useStore(selector, shallow);
|
||||
const zoomActivationKeyPressed = useKeyPress(zoomActivationKeyCode);
|
||||
const panZoom = useRef<PanZoomInstance>();
|
||||
|
||||
@@ -76,7 +73,6 @@ export function ZoomPane({
|
||||
maxZoom,
|
||||
translateExtent,
|
||||
viewport: defaultViewport,
|
||||
paneClickDistance,
|
||||
onDraggingChange: (paneDragging: boolean) => store.setState({ paneDragging }),
|
||||
onPanZoomStart: (event, vp) => {
|
||||
const { onViewportChangeStart, onMoveStart } = store.getState();
|
||||
@@ -126,6 +122,9 @@ export function ZoomPane({
|
||||
noWheelClassName,
|
||||
lib,
|
||||
onTransformChange,
|
||||
connectionInProgress,
|
||||
selectionOnDrag,
|
||||
paneClickDistance,
|
||||
});
|
||||
}, [
|
||||
onPaneContextMenu,
|
||||
@@ -143,6 +142,9 @@ export function ZoomPane({
|
||||
noWheelClassName,
|
||||
lib,
|
||||
onTransformChange,
|
||||
connectionInProgress,
|
||||
selectionOnDrag,
|
||||
paneClickDistance,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -202,6 +202,11 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
||||
|
||||
return { deletedNodes: matchingNodes, deletedEdges: matchingEdges };
|
||||
},
|
||||
/**
|
||||
* Partial is defined as "the 2 nodes/areas are intersecting partially".
|
||||
* If a is contained in b or b is contained in a, they are both
|
||||
* considered fully intersecting.
|
||||
*/
|
||||
getIntersectingNodes: (nodeOrRect, partially = true, nodes) => {
|
||||
const isRect = isRectObject(nodeOrRect);
|
||||
const nodeRect = isRect ? nodeOrRect : getNodeRect(nodeOrRect);
|
||||
@@ -240,7 +245,11 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
||||
const overlappingArea = getOverlappingArea(nodeRect, area);
|
||||
const partiallyVisible = partially && overlappingArea > 0;
|
||||
|
||||
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
|
||||
return (
|
||||
partiallyVisible ||
|
||||
overlappingArea >= area.width * area.height ||
|
||||
overlappingArea >= nodeRect.width * nodeRect.height
|
||||
);
|
||||
},
|
||||
updateNode,
|
||||
updateNodeData: (id, dataUpdate, options = { replace: false }) => {
|
||||
|
||||
@@ -13,7 +13,7 @@ export function useResizeHandler(domNode: MutableRefObject<HTMLDivElement | null
|
||||
|
||||
useEffect(() => {
|
||||
const updateDimensions = () => {
|
||||
if (!domNode.current) {
|
||||
if (!domNode.current || !(domNode.current.checkVisibility?.() ?? true)) {
|
||||
return false;
|
||||
}
|
||||
const size = getDimensions(domNode.current);
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
NodeOrigin,
|
||||
CoordinateExtent,
|
||||
fitViewport,
|
||||
getHandlePosition,
|
||||
Position,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
|
||||
@@ -176,7 +178,7 @@ const createStore = ({
|
||||
updateNodePositions: (nodeDragItems, dragging = false) => {
|
||||
const parentExpandChildren: ParentExpandChild[] = [];
|
||||
let changes = [];
|
||||
const { nodeLookup, triggerNodeChanges, onNodesChangeMiddlewareMap } = get();
|
||||
const { nodeLookup, triggerNodeChanges, connection, updateConnection, onNodesChangeMiddlewareMap } = get();
|
||||
|
||||
for (const [id, dragItem] of nodeDragItems) {
|
||||
// we are using the nodelookup to be sure to use the current expandParent and parentId value
|
||||
@@ -195,6 +197,11 @@ const createStore = ({
|
||||
dragging,
|
||||
};
|
||||
|
||||
if (node && connection.inProgress && connection.fromNode.id === node.id) {
|
||||
const updatedFrom = getHandlePosition(node, connection.fromHandle, Position.Left, true);
|
||||
updateConnection({ ...connection, from: updatedFrom });
|
||||
}
|
||||
|
||||
if (expandParent && node.parentId) {
|
||||
parentExpandChildren.push({
|
||||
id,
|
||||
@@ -316,9 +323,6 @@ const createStore = ({
|
||||
|
||||
set({ translateExtent });
|
||||
},
|
||||
setPaneClickDistance: (clickDistance) => {
|
||||
get().panZoom?.setClickDistance(clickDistance);
|
||||
},
|
||||
resetSelectedElements: () => {
|
||||
const { edges, nodes, triggerNodeChanges, triggerEdgeChanges, elementsSelectable } = get();
|
||||
|
||||
|
||||
@@ -79,8 +79,8 @@ const getInitialState = ({
|
||||
|
||||
return {
|
||||
rfId: '1',
|
||||
width: 0,
|
||||
height: 0,
|
||||
width: width ?? 0,
|
||||
height: height ?? 0,
|
||||
transform,
|
||||
nodes: storeNodes,
|
||||
nodesInitialized,
|
||||
|
||||
@@ -612,6 +612,7 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
|
||||
elevateNodesOnSelect?: boolean;
|
||||
/**
|
||||
* Enabling this option will raise the z-index of edges when they are selected.
|
||||
* @default false
|
||||
*/
|
||||
elevateEdgesOnSelect?: boolean;
|
||||
/**
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
OnError,
|
||||
OnReconnect,
|
||||
FinalConnectionState,
|
||||
XYPosition,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { EdgeTypes, InternalNode, Node } from '.';
|
||||
@@ -149,7 +150,7 @@ export type EdgeTextProps = Omit<SVGAttributes<SVGElement>, 'x' | 'y'> &
|
||||
*/
|
||||
export type EdgeProps<EdgeType extends Edge = Edge> = Pick<
|
||||
EdgeType,
|
||||
'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target' | 'selectable' | 'deletable'
|
||||
'id' | 'type' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target' | 'selectable' | 'deletable'
|
||||
> &
|
||||
EdgePosition &
|
||||
EdgeLabelOptions & {
|
||||
@@ -279,6 +280,7 @@ export type ConnectionLineComponentProps<NodeType extends Node = Node> = {
|
||||
connectionStatus: 'valid' | 'invalid' | null;
|
||||
toNode: InternalNode<NodeType> | null;
|
||||
toHandle: Handle | null;
|
||||
pointer: XYPosition;
|
||||
};
|
||||
|
||||
export type ConnectionLineComponent<NodeType extends Node = Node> = ComponentType<
|
||||
|
||||
@@ -228,16 +228,16 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
|
||||
nodeId: string;
|
||||
handleId?: string | null;
|
||||
}) => NodeConnection[];
|
||||
// /**
|
||||
// * Fits the view.
|
||||
// *
|
||||
// * @param options.padding - optional padding
|
||||
// * @param options.includeHiddenNodes - optional includeHiddenNodes
|
||||
// * @param options.minZoom - optional minZoom
|
||||
// * @param options.maxZoom - optional maxZoom
|
||||
// * @param options.duration - optional duration. If set, a transition will be applied
|
||||
// * @param options.nodes - optional nodes to fit the view to
|
||||
// */
|
||||
/**
|
||||
* Fits the view based on the passed params. By default it fits the view to all nodes.
|
||||
*
|
||||
* @param options.padding - optional padding
|
||||
* @param options.nodes - optional nodes to fit the view to
|
||||
* @param options.minZoom - optional minZoom
|
||||
* @param options.maxZoom - optional maxZoom
|
||||
* @param options.duration - optional duration. If set, a transition will be applied
|
||||
* @param options.includeHiddenNodes - optional includeHiddenNodes
|
||||
*/
|
||||
fitView: FitView<NodeType>;
|
||||
};
|
||||
/**
|
||||
|
||||
@@ -178,7 +178,6 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
|
||||
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
|
||||
panBy: PanBy;
|
||||
setCenter: SetCenter;
|
||||
setPaneClickDistance: (distance: number) => void;
|
||||
};
|
||||
|
||||
export type ReactFlowState<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ReactFlowStore<
|
||||
|
||||
@@ -125,9 +125,9 @@ function applyChange(change: any, element: any): any {
|
||||
|
||||
case 'dimensions': {
|
||||
if (typeof change.dimensions !== 'undefined') {
|
||||
element.measured ??= {};
|
||||
element.measured.width = change.dimensions.width;
|
||||
element.measured.height = change.dimensions.height;
|
||||
element.measured = {
|
||||
...change.dimensions,
|
||||
};
|
||||
|
||||
if (change.setAttributes) {
|
||||
if (change.setAttributes === true || change.setAttributes === 'width') {
|
||||
|
||||
Reference in New Issue
Block a user