chore(hooks): tsdoc update

This commit is contained in:
moklick
2025-02-11 13:59:17 +01:00
parent ebec872318
commit d8ab3bf0bd
50 changed files with 895 additions and 271 deletions

View File

@@ -57,7 +57,7 @@
"build": "rollup --config node:@xyflow/rollup-config --environment NODE_ENV:production && npm run css",
"css": "postcss src/styles/{base,style}.css --config ./../../tooling/postcss-config/ --dir dist ",
"css-watch": "pnpm css --watch",
"lint": "eslint --ext .js,.jsx,.ts,.tsx src",
"lint": "eslint --ext .js,.jsx,.ts,.tsx src --fix",
"typecheck": "tsc --noEmit"
},
"dependencies": {

View File

@@ -90,4 +90,57 @@ function BackgroundComponent({
BackgroundComponent.displayName = 'Background';
/**
* The `<Background />` component makes it convenient to render different types of backgrounds common in node-based UIs. It comes with three variants: lines, dots and cross.
*
* @example
*
* A simple example of how to use the Background component.
*
* ```tsx
* import { useState } from 'react';
* import { ReactFlow, Background, BackgroundVariant } from '@xyflow/react';
*
* export default function Flow() {
* return (
* <ReactFlow defaultNodes={[...]} defaultEdges={[...]}>
* <Background color="#ccc" variant={BackgroundVariant.Dots} />
* </ReactFlow>
* );
* }
* ```
*
* @example
*
* In this example you can see how to combine multiple backgrounds
*
* ```tsx
* import { ReactFlow, Background, BackgroundVariant } from '@xyflow/react';
* import '@xyflow/react/dist/style.css';
*
* export default function Flow() {
* return (
* <ReactFlow defaultNodes={[...]} defaultEdges={[...]}>
* <Background
* id="1"
* gap={10}
* color="#f1f1f1"
* variant={BackgroundVariant.Lines}
* />
* <Background
* id="2"
* gap={100}
* color="#ccc"
* variant={BackgroundVariant.Lines}
* />
* </ReactFlow>
* );
* }
* ```
*
* @remarks
*
* When combining multiple <Background /> components its important to give each of them a unique id prop!
*
*/
export const Background = memo(BackgroundComponent);

View File

@@ -24,7 +24,8 @@ export type BackgroundProps = {
offset?: number | [number, number];
/** Line width of the Line pattern */
lineWidth?: number;
/** Variant of the pattern
/**
* Variant of the pattern
* @example BackgroundVariant.Lines, BackgroundVariant.Dots, BackgroundVariant.Cross
* 'lines', 'dots', 'cross'
*/

View File

@@ -20,7 +20,8 @@ export type ControlProps = {
onFitView?: () => void;
/** Callback when interactivity is toggled */
onInteractiveChange?: (interactiveStatus: boolean) => void;
/** Position of the controls on the pane
/**
* Position of the controls on the pane
* @example PanelPosition.TopLeft, PanelPosition.TopRight,
* PanelPosition.BottomLeft, PanelPosition.BottomRight
*/

View File

@@ -44,8 +44,10 @@ function MiniMapComponent<NodeType extends Node = Node>({
nodeClassName = '',
nodeBorderRadius = 5,
nodeStrokeWidth,
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
// a component properly.
/*
* We need to rename the prop to be `CapitalCase` so that JSX will render it as
* a component properly.
*/
nodeComponent,
bgColor,
maskColor,
@@ -111,16 +113,16 @@ function MiniMapComponent<NodeType extends Node = Node>({
const onSvgClick = onClick
? (event: MouseEvent) => {
const [x, y] = minimapInstance.current?.pointer(event) || [0, 0];
onClick(event, { x, y });
}
const [x, y] = minimapInstance.current?.pointer(event) || [0, 0];
onClick(event, { x, y });
}
: undefined;
const onSvgNodeClick = onNodeClick
? useCallback((event: MouseEvent, nodeId: string) => {
const node = store.getState().nodeLookup.get(nodeId)!;
onNodeClick(event, node);
}, [])
const node = store.getState().nodeLookup.get(nodeId)!;
onNodeClick(event, node);
}, [])
: undefined;
return (

View File

@@ -21,8 +21,10 @@ function MiniMapNodes<NodeType extends Node>({
nodeClassName = '',
nodeBorderRadius = 5,
nodeStrokeWidth,
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
// a component properly.
/*
* We need to rename the prop to be `CapitalCase` so that JSX will render it as
* a component properly.
*/
nodeComponent: NodeComponent = MiniMapNode,
onClick,
}: MiniMapNodesProps<NodeType>) {
@@ -36,11 +38,13 @@ function MiniMapNodes<NodeType extends Node>({
return (
<>
{nodeIds.map((nodeId) => (
// The split of responsibilities between MiniMapNodes and
// NodeComponentWrapper may appear weird. However, its designed to
// minimize the cost of updates when individual nodes change.
//
// For more details, see a similar commit in `NodeRenderer/index.tsx`.
/*
* The split of responsibilities between MiniMapNodes and
* NodeComponentWrapper may appear weird. However, its designed to
* minimize the cost of updates when individual nodes change.
*
* For more details, see a similar commit in `NodeRenderer/index.tsx`.
*/
<NodeComponentWrapper<NodeType>
key={nodeId}
id={nodeId}

View File

@@ -27,7 +27,8 @@ export type MiniMapProps<NodeType extends Node = Node> = Omit<HTMLAttributes<SVG
maskStrokeColor?: string;
/** Stroke width of mask representing viewport */
maskStrokeWidth?: number;
/** Position of minimap on pane
/**
* Position of minimap on pane
* @example PanelPosition.TopLeft, PanelPosition.TopRight,
* PanelPosition.BottomLeft, PanelPosition.BottomRight
*/

View File

@@ -99,8 +99,10 @@ function ResizeControl({
const parentExpandChanges = handleExpandParent([child], nodeLookup, parentLookup, nodeOrigin);
changes.push(...parentExpandChanges);
// when the parent was expanded by the child node, its position will be clamped at
// 0,0 when node origin is 0,0 and to width, height if it's 1,1
/*
* when the parent was expanded by the child node, its position will be clamped at
* 0,0 when node origin is 0,0 and to width, height if it's 1,1
*/
nextPosition.x = change.x ? Math.max(origin[0] * width, change.x) : undefined;
nextPosition.y = change.y ? Math.max(origin[1] * height, change.y) : undefined;
}

View File

@@ -10,7 +10,8 @@ import type {
} from '@xyflow/system';
export type NodeResizerProps = {
/** Id of the node it is resizing
/**
* Id of the node it is resizing
* @remarks optional if used inside custom node
*/
nodeId?: string;
@@ -60,12 +61,14 @@ export type ResizeControlProps = Pick<
| 'onResize'
| 'onResizeEnd'
> & {
/** Position of the control
/**
* Position of the control
* @example ControlPosition.TopLeft, ControlPosition.TopRight,
* ControlPosition.BottomLeft, ControlPosition.BottomRight
*/
position?: ControlPosition;
/** Variant of the control
/**
* Variant of the control
* @example ResizeControlVariant.Handle, ResizeControlVariant.Line
*/
variant?: ResizeControlVariant;

View File

@@ -6,14 +6,16 @@ export type NodeToolbarProps = HTMLAttributes<HTMLDivElement> & {
nodeId?: string | string[];
/** If true, node toolbar is visible even if node is not selected */
isVisible?: boolean;
/** Position of the toolbar relative to the node
/**
* Position of the toolbar relative to the node
* @example Position.TopLeft, Position.TopRight,
* Position.BottomLeft, Position.BottomRight
*/
position?: Position;
/** Offset the toolbar from the node */
offset?: number;
/** Align the toolbar relative to the node
/**
* Align the toolbar relative to the node
* @example Align.Start, Align.Center, Align.End
*/
align?: Align;

View File

@@ -30,9 +30,11 @@ export function BatchProvider<NodeType extends Node = Node, EdgeType extends Edg
const nodeQueueHandler = useCallback((queueItems: QueueItem<NodeType>[]) => {
const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState();
// This is essentially an `Array.reduce` in imperative clothing. Processing
// this queue is a relatively hot path so we'd like to avoid the overhead of
// array methods where we can.
/*
* This is essentially an `Array.reduce` in imperative clothing. Processing
* this queue is a relatively hot path so we'd like to avoid the overhead of
* array methods where we can.
*/
let next = nodes as NodeType[];
for (const payload of queueItems) {
next = typeof payload === 'function' ? payload(next) : payload;

View File

@@ -12,21 +12,27 @@ import { Queue, QueueItem } from './types';
* @returns a Queue object
*/
export function useQueue<T>(runQueue: (items: QueueItem<T>[]) => void) {
// Because we're using a ref above, we need some way to let React know when to
// actually process the queue. We increment this number any time we mutate the
// queue, creating a new state to trigger the layout effect below.
// Using a boolean dirty flag here instead would lead to issues related to
// automatic batching. (https://github.com/xyflow/xyflow/issues/4779)
/*
* Because we're using a ref above, we need some way to let React know when to
* actually process the queue. We increment this number any time we mutate the
* queue, creating a new state to trigger the layout effect below.
* Using a boolean dirty flag here instead would lead to issues related to
* automatic batching. (https://github.com/xyflow/xyflow/issues/4779)
*/
const [serial, setSerial] = useState(BigInt(0));
// A reference of all the batched updates to process before the next render. We
// want a reference here so multiple synchronous calls to `setNodes` etc can be
// batched together.
/*
* A reference of all the batched updates to process before the next render. We
* want a reference here so multiple synchronous calls to `setNodes` etc can be
* batched together.
*/
const [queue] = useState(() => createQueue<T>(() => setSerial(n => n + BigInt(1))));
// Layout effects are guaranteed to run before the next render which means we
// shouldn't run into any issues with stale state or weird issues that come from
// rendering things one frame later than expected (we used to use `setTimeout`).
/*
* Layout effects are guaranteed to run before the next render which means we
* shouldn't run into any issues with stale state or weird issues that come from
* rendering things one frame later than expected (we used to use `setTimeout`).
*/
useIsomorphicLayoutEffect(() => {
const queueItems = queue.get();

View File

@@ -136,28 +136,28 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
const onEdgeDoubleClick = onDoubleClick
? (event: React.MouseEvent) => {
onDoubleClick(event, { ...edge });
}
onDoubleClick(event, { ...edge });
}
: undefined;
const onEdgeContextMenu = onContextMenu
? (event: React.MouseEvent) => {
onContextMenu(event, { ...edge });
}
onContextMenu(event, { ...edge });
}
: undefined;
const onEdgeMouseEnter = onMouseEnter
? (event: React.MouseEvent) => {
onMouseEnter(event, { ...edge });
}
onMouseEnter(event, { ...edge });
}
: undefined;
const onEdgeMouseMove = onMouseMove
? (event: React.MouseEvent) => {
onMouseMove(event, { ...edge });
}
onMouseMove(event, { ...edge });
}
: undefined;
const onEdgeMouseLeave = onMouseLeave
? (event: React.MouseEvent) => {
onMouseLeave(event, { ...edge });
}
onMouseLeave(event, { ...edge });
}
: undefined;
const onKeyDown = (event: KeyboardEvent) => {

View File

@@ -1,6 +1,8 @@
// We distinguish between internal and exported edges
// The internal edges are used directly like custom edges and always get an id, source and target props
// If you import an edge from the library, the id is optional and source and target are not used at all
/*
* We distinguish between internal and exported edges
* The internal edges are used directly like custom edges and always get an id, source and target props
* If you import an edge from the library, the id is optional and source and target are not used at all
*/
export { SimpleBezierEdge, SimpleBezierEdgeInternal } from './SimpleBezierEdge';
export { SmoothStepEdge, SmoothStepEdgeInternal } from './SmoothStepEdge';

View File

@@ -228,8 +228,10 @@ function HandleComponent(
connectingfrom: connectingFrom,
connectingto: connectingTo,
valid,
// shows where you can start a connection from
// and where you can end it while connecting
/*
* shows where you can start a connection from
* and where you can end it while connecting
*/
connectionindicator:
isConnectable &&
(!connectionInProcess || isPossibleEndHandle) &&

View File

@@ -108,8 +108,10 @@ export function NodeWrapper<NodeType extends Node>({
const { selectNodesOnDrag, nodeDragThreshold } = store.getState();
if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) {
// this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
// here we only need to call it when selectNodesOnDrag=false
/*
* this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
* here we only need to call it when selectNodesOnDrag=false
*/
handleNodeClick({
id,
store,

View File

@@ -49,8 +49,10 @@ export function useNodeObserver({
useEffect(() => {
if (nodeRef.current) {
// when the user programmatically changes the source or handle position, we need to update the internals
// to make sure the edges are updated correctly
/*
* when the user programmatically changes the source or handle position, we need to update the internals
* to make sure the edges are updated correctly
*/
const typeChanged = prevType.current !== nodeType;
const sourcePosChanged = prevSourcePosition.current !== node.sourcePosition;
const targetPosChanged = prevTargetPosition.current !== node.targetPosition;

View File

@@ -23,9 +23,9 @@ export const builtinNodeTypes: NodeTypes = {
export function getNodeInlineStyleDimensions<NodeType extends Node = Node>(
node: InternalNode<NodeType>
): {
width: number | string | undefined;
height: number | string | undefined;
} {
width: number | string | undefined;
height: number | string | undefined;
} {
if (node.internals.handleBounds === undefined) {
return {
width: node.width ?? node.initialWidth ?? node.style?.width,

View File

@@ -4,10 +4,12 @@ import { errorMessages } from '@xyflow/system';
import type { ReactFlowState } from '../../types';
// this handler is called by
// 1. the click handler when node is not draggable or selectNodesOnDrag = false
// or
// 2. the on drag start handler when node is draggable and selectNodesOnDrag = true
/*
* this handler is called by
* 1. the click handler when node is not draggable or selectNodesOnDrag = false
* or
* 2. the on drag start handler when node is draggable and selectNodesOnDrag = true
*/
export function handleNodeClick({
id,
store,

View File

@@ -61,9 +61,9 @@ export function NodesSelection<NodeType extends Node>({
const onContextMenu = onSelectionContextMenu
? (event: MouseEvent) => {
const selectedNodes = store.getState().nodes.filter((n) => n.selected);
onSelectionContextMenu(event, selectedNodes);
}
const selectedNodes = store.getState().nodes.filter((n) => n.selected);
onSelectionContextMenu(event, selectedNodes);
}
: undefined;
const onKeyDown = (event: KeyboardEvent) => {

View File

@@ -6,7 +6,8 @@ import { useStore } from '../../hooks/useStore';
import type { ReactFlowState } from '../../types';
export type PanelProps = HTMLAttributes<HTMLDivElement> & {
/** Set position of the panel
/**
* Set position of the panel
* @example 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
*/
position?: PanelPosition;

View File

@@ -94,9 +94,11 @@ const selector = (s: ReactFlowState) => ({
});
const initPrevValues = {
// 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.
/*
* 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: defaultNodeOrigin,
minZoom: 0.5,

View File

@@ -42,9 +42,11 @@ const Marker = ({
);
};
// when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore
// when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper
// that we can then use for creating our unique marker ids
/*
* when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore
* when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper
* that we can then use for creating our unique marker ids
*/
const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => {
const edges = useStore((s) => s.edges);
const defaultEdgeOptions = useStore((s) => s.defaultEdgeOptions);

View File

@@ -44,29 +44,31 @@ function NodeRendererComponent<NodeType extends Node>(props: NodeRendererProps<N
<div className="react-flow__nodes" style={containerStyle}>
{nodeIds.map((nodeId) => {
return (
// The split of responsibilities between NodeRenderer and
// NodeComponentWrapper may appear weird. However, its designed to
// minimize the cost of updates when individual nodes change.
//
// For example, when youre dragging a single node, that node gets
// updated multiple times per second. If `NodeRenderer` were to update
// every time, it would have to re-run the `nodes.map()` loop every
// time. This gets pricey with hundreds of nodes, especially if every
// loop cycle does more than just rendering a JSX element!
//
// As a result of this choice, we took the following implementation
// decisions:
// - NodeRenderer subscribes *only* to node IDs and therefore
// rerender *only* when visible nodes are added or removed.
// - NodeRenderer performs all operations the result of which can be
// shared between nodes (such as creating the `ResizeObserver`
// instance, or subscribing to `selector`). This means extra prop
// drilling into `NodeComponentWrapper`, but it means we need to run
// these operations only once instead of once per node.
// - Any operations that youd normally write inside `nodes.map` are
// moved into `NodeComponentWrapper`. This ensures they are
// memorized so if `NodeRenderer` *has* to rerender, it only
// needs to regenerate the list of nodes, nothing else.
/*
* The split of responsibilities between NodeRenderer and
* NodeComponentWrapper may appear weird. However, its designed to
* minimize the cost of updates when individual nodes change.
*
* For example, when youre dragging a single node, that node gets
* updated multiple times per second. If `NodeRenderer` were to update
* every time, it would have to re-run the `nodes.map()` loop every
* time. This gets pricey with hundreds of nodes, especially if every
* loop cycle does more than just rendering a JSX element!
*
* As a result of this choice, we took the following implementation
* decisions:
* - NodeRenderer subscribes *only* to node IDs and therefore
* rerender *only* when visible nodes are added or removed.
* - NodeRenderer performs all operations the result of which can be
* shared between nodes (such as creating the `ResizeObserver`
* instance, or subscribing to `selector`). This means extra prop
* drilling into `NodeComponentWrapper`, but it means we need to run
* these operations only once instead of once per node.
* - Any operations that youd normally write inside `nodes.map` are
* moved into `NodeComponentWrapper`. This ensures they are
* memorized so if `NodeRenderer` *has* to rerender, it only
* needs to regenerate the list of nodes, nothing else.
*/
<NodeWrapper<NodeType>
key={nodeId}
id={nodeId}

View File

@@ -233,8 +233,10 @@ export function Pane({
(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.
/*
* 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) {
onClick?.(event);
}
@@ -246,8 +248,10 @@ export function Pane({
});
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 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;
}

View File

@@ -31,8 +31,10 @@ export function Wrapper({
const isWrapped = useContext(StoreContext);
if (isWrapped) {
// we need to wrap it with a fragment because it's not allowed for children to be a ReactNode
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051
/*
* we need to wrap it with a fragment because it's not allowed for children to be a ReactNode
* https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051
*/
return <>{children}</>;
}

View File

@@ -4,6 +4,34 @@ export const NodeIdContext = createContext<string | null>(null);
export const Provider = NodeIdContext.Provider;
export const Consumer = NodeIdContext.Consumer;
/**
* You can use this hook to get the id of the node it is used inside. It is useful
*if you need the node's id deeper in the render tree but don't want to manually
*drill down the id as a prop.
*
* @public
* @returns id of the node
*
* @example
*```jsx
*import { useNodeId } from '@xyflow/react';
*
*export default function CustomNode() {
* return (
* <div>
* <span>This node has an id of </span>
* <NodeIdDisplay />
* </div>
* );
*}
*
*function NodeIdDisplay() {
* const nodeId = useNodeId();
*
* return <span>{nodeId}</span>;
*}
*```
*/
export const useNodeId = (): string | null => {
const nodeId = useContext(NodeIdContext);
return nodeId;

View File

@@ -24,9 +24,28 @@ function getSelector<NodeType extends Node = Node, SelectorReturn = ConnectionSt
return storeSelector;
}
/**
* Hook for accessing the connection state.
* The `useConnection` hook returns the current connection when there is an active
*connection interaction. If no connection interaction is active, it returns null
*for every property. A typical use case for this hook is to colorize handles
*based on a certain condition (e.g. if the connection is valid or not).
*
* @public
* @example
*
* ```tsx
*import { useConnection } from '@xyflow/react';
*
*function App() {
* const connection = useConnection();
*
* return (
* <div> {connection ? `Someone is trying to make a connection from ${connection.fromNode} to this one.` : 'There are currently no incoming connections!'}
*
* </div>
* );
* }
* ```
*
* @returns ConnectionState
*/
export function useConnection<NodeType extends Node = Node, SelectorReturn = ConnectionState<InternalNode<NodeType>>>(

View File

@@ -6,10 +6,22 @@ import type { Edge, ReactFlowState } from '../types';
const edgesSelector = (state: ReactFlowState) => state.edges;
/**
* Hook for getting the current edges from the store.
* This hook returns an array of the current edges. Components that use this hook
*will re-render **whenever any edge changes**.
*
* @public
* @returns An array of edges
*
* @example
* ```tsx
*import { useEdges } from '@xyflow/react';
*
*export default function () {
* const edges = useEdges();
*
* return <div>There are currently {edges.length} edges!</div>;
*}
*```
*/
export function useEdges<EdgeType extends Edge = Edge>(): EdgeType[] {
const edges = useStore(edgesSelector, shallow) as EdgeType[];

View File

@@ -5,11 +5,31 @@ import { useStore } from './useStore';
import type { InternalNode, Node } from '../types';
/**
* Hook for getting an internal node by id
* This hook returns the internal representation of a specific node. Components that use this hook
*will re-render **whenever the node changes**, including when a node is selected
*or moved.
*
* @public
* @param id - id of the node
* @returns array with visible node ids
*
* @example
* ```tsx
*import { useInternalNode } from '@xyflow/react';
*
*export default function () {
* const internalNode = useInternalNode('node-1');
* const absolutePosition = internalNode.internals.positionAbsolute;
*
* return (
* <div>
* The absolute position of the node is at:
* <p>x: {absolutePosition.x}</p>
* <p>y: {absolutePosition.y}</p>
* </div>
* );
*}
*```
*/
export function useInternalNode<NodeType extends Node = Node>(id: string): InternalNode<NodeType> | undefined {
const node = useStore(

View File

@@ -13,18 +13,38 @@ export type UseKeyPressOptions = {
const defaultDoc = typeof document !== 'undefined' ? document : null;
/**
* Hook for handling key events.
* This hook lets you listen for specific key codes and tells you whether they are
*currently pressed or not.
*
* @public
* @param param.keyCode - The key code (string or array of strings) to use
* @param param.options - Options
* @returns boolean
*
* @example
* ```tsx
*import { useKeyPress } from '@xyflow/react';
*
*export default function () {
* const spacePressed = useKeyPress('Space');
* const cmdAndSPressed = useKeyPress(['Meta+s', 'Strg+s']);
*
* return (
* <div>
* {spacePressed && <p>Space pressed!</p>}
* {cmdAndSPressed && <p>Cmd + S pressed!</p>}
* </div>
* );
*}
*```
*/
export 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'
/*
* 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: KeyCode | null = null,
options: UseKeyPressOptions = { target: defaultDoc, actInsideInputWithModifier: true }
): boolean {
@@ -36,20 +56,24 @@ export function useKeyPress(
// we need to remember the pressed keys in order to support combinations
const pressedKeys = useRef<PressedKeys>(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.
/*
* 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<[Array<Keys>, Keys]>(() => {
if (keyCode !== null) {
const keyCodeArr = Array.isArray(keyCode) ? keyCode : [keyCode];
const keys = keyCodeArr
.filter((kc) => typeof kc === 'string')
// we first replace all '+' with '\n' which we will use to split the keys on
// then we replace '\n\n' with '\n+', this way we can also support the combination 'key++'
// in the end we simply split on '\n' to get the key array
/*
* we first replace all '+' with '\n' which we will use to split the keys on
* then we replace '\n\n' with '\n+', this way we can also support the combination 'key++'
* in the end we simply split on '\n' to get the key array
*/
.map((kc) => kc.replace('+', '\n').replace('\n\n', '\n+').split('\n'));
const keysFlat = keys.reduce((res: Keys, item) => res.concat(...item), []);
@@ -133,12 +157,16 @@ export function useKeyPress(
function isMatchingKey(keyCodes: Array<Keys>, pressedKeys: PressedKeys, isUp: boolean): boolean {
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'
/*
* 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
/*
* 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)))
);
}

View File

@@ -22,8 +22,10 @@ export function useMoveSelectedNodes() {
const nodeUpdates = new Map();
const isSelected = selectedAndDraggable(nodesDraggable);
// by default a node moves 5px on each key press
// if snap grid is enabled, we use that for the velocity
/*
* by default a node moves 5px on each key press
* if snap grid is enabled, we use that for the velocity
*/
const xVelo = snapToGrid ? snapGrid[0] : 5;
const yVelo = snapToGrid ? snapGrid[1] : 5;

View File

@@ -22,7 +22,7 @@ type UseNodeConnectionsParams = {
};
/**
* Hook to retrieve all edges connected to a node. Can be filtered by handle type and id.
* This hook returns an array of connections on a specific node, handle type ('source', 'target') or handle ID.
*
* @public
* @param param.id - node id - optional if called inside a custom node
@@ -31,6 +31,22 @@ type UseNodeConnectionsParams = {
* @param param.onConnect - gets called when a connection is established
* @param param.onDisconnect - gets called when a connection is removed
* @returns an array with connections
*
* @example
* ```jsx
*import { useNodeConnections } from '@xyflow/react';
*
*export default function () {
* const connections = useNodeConnections({
* type: 'target',
* handleId: 'my-handle',
* });
*
* return (
* <div>There are currently {connections.length} incoming connections!</div>
* );
*}
*```
*/
export function useNodeConnections({
id,

View File

@@ -6,10 +6,23 @@ import type { Node, ReactFlowState } from '../types';
const nodesSelector = (state: ReactFlowState) => state.nodes;
/**
* Hook for getting the current nodes from the store.
* This hook returns an array of the current nodes. Components that use this hook
*will re-render **whenever any node changes**, including when a node is selected
*or moved.
*
* @public
* @returns An array of nodes
*
* @example
* ```jsx
*import { useNodes } from '@xyflow/react';
*
*export default function() {
* const nodes = useNodes();
*
* return <div>There are currently {nodes.length} nodes!</div>;
*}
*```
*/
export function useNodes<NodeType extends Node = Node>(): NodeType[] {
const nodes = useStore(nodesSelector, shallow) as NodeType[];

View File

@@ -5,12 +5,24 @@ import { useStore } from '../hooks/useStore';
import type { Node } from '../types';
/**
* Hook for receiving data of one or multiple nodes
* This hook lets you subscribe to changes of a specific nodes `data` object.
*
* @public
* @param nodeId - The id (or ids) of the node to get the data from
* @param guard - Optional guard function to narrow down the node type
* @returns An object (or array of object) with {id, type, data} representing each node
*
* @example
*
*```jsx
*import { useNodesData } from '@xyflow/react';
*
*export default function() {
* const nodeData = useNodesData('nodeId-1');
* const nodesData = useNodesData(['nodeId-1', 'nodeId-2']);
*
* return null;
*}
*```
*/
export function useNodesData<NodeType extends Node = Node>(
nodeId: string

View File

@@ -4,11 +4,41 @@ import { applyNodeChanges, applyEdgeChanges } from '../utils/changes';
import type { Node, Edge, OnNodesChange, OnEdgesChange } from '../types';
/**
* Hook for managing the state of nodes - should only be used for prototyping / simple use cases.
* This hook makes it easy to prototype a controlled flow where you manage the
*state of nodes and edges outside the `ReactFlowInstance`. You can think of it
*like React's `useState` hook with an additional helper callback.
*
* @public
* @param initialNodes
* @returns an array [nodes, setNodes, onNodesChange]
* @example
*
*```tsx
*import { ReactFlow, useNodesState, useEdgesState } from '@xyflow/react';
*
*const initialNodes = [];
*const initialEdges = [];
*
*export default function () {
* const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
* const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
*
* return (
* <ReactFlow
* nodes={nodes}
* edges={edges}
* onNodesChange={onNodesChange}
* onEdgesChange={onEdgesChange}
* />
* );
*}
*```
*
*@remarks This hook was created to make prototyping easier and our documentation
*examples clearer. Although it is OK to use this hook in production, in
*practice you may want to use a more sophisticated state management solution
*like Zustand {@link https://reactflow.dev/docs/guides/state-management/} instead.
*
*/
export function useNodesState<NodeType extends Node>(
initialNodes: NodeType[]
@@ -23,11 +53,41 @@ export function useNodesState<NodeType extends Node>(
}
/**
* Hook for managing the state of edges - should only be used for prototyping / simple use cases.
* This hook makes it easy to prototype a controlled flow where you manage the
*state of nodes and edges outside the `ReactFlowInstance`. You can think of it
*like React's `useState` hook with an additional helper callback.
*
* @public
* @param initialEdges
* @returns an array [edges, setEdges, onEdgesChange]
* @example
*
*```tsx
*import { ReactFlow, useNodesState, useEdgesState } from '@xyflow/react';
*
*const initialNodes = [];
*const initialEdges = [];
*
*export default function () {
* const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
* const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
*
* return (
* <ReactFlow
* nodes={nodes}
* edges={edges}
* onNodesChange={onNodesChange}
* onEdgesChange={onEdgesChange}
* />
* );
*}
*```
*
* @remarks This hook was created to make prototyping easier and our documentation
*examples clearer. Although it is OK to use this hook in production, in
*practice you may want to use a more sophisticated state management solution
*like Zustand {@link https://reactflow.dev/docs/guides/state-management/} instead.
*
*/
export function useEdgesState<EdgeType extends Edge = Edge>(
initialEdges: EdgeType[]

View File

@@ -1,6 +1,7 @@
import { nodeHasDimensions } from '@xyflow/system';
import { useStore } from './useStore';
import type { ReactFlowState } from '../types';
import { nodeHasDimensions } from '@xyflow/system';
export type UseNodesInitializedOptions = {
includeHiddenNodes?: boolean;
@@ -22,18 +23,44 @@ const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) =>
return true;
};
const defaultOptions = {
includeHiddenNodes: false,
};
/**
* Hook which returns true when all nodes are initialized.
* This hook tells you whether all the nodes in a flow have been measured and given
*a width and height. When you add a node to the flow, this hook will return
*`false` and then `true` again once the node has been measured.
*
* @public
* @param options.includeHiddenNodes - defaults to false
* @returns boolean indicating whether all nodes are initialized
*
* @example
* ```jsx
*import { useReactFlow, useNodesInitialized } from '@xyflow/react';
*import { useEffect, useState } from 'react';
*
*const options = {
* includeHiddenNodes: false,
*};
*
*export default function useLayout() {
* const { getNodes } = useReactFlow();
* const nodesInitialized = useNodesInitialized(options);
* const [layoutedNodes, setLayoutedNodes] = useState(getNodes());
*
* useEffect(() => {
* if (nodesInitialized) {
* setLayoutedNodes(yourLayoutingFunction(getNodes()));
* }
* }, [nodesInitialized]);
*
* return layoutedNodes;
*}
*```
*/
export function useNodesInitialized(options: UseNodesInitializedOptions = defaultOptions): boolean {
export function useNodesInitialized(
options: UseNodesInitializedOptions = {
includeHiddenNodes: false,
}
): boolean {
const initialized = useStore(selector(options));
return initialized;

View File

@@ -8,10 +8,42 @@ export type UseOnSelectionChangeOptions = {
};
/**
* Hook for registering an onSelectionChange handler.
* This hook lets you listen for changes to both node and edge selection. As the
*name implies, the callback you provide will be called whenever the selection of
*_either_ nodes or edges changes.
*
* @public
* @param params.onChange - The handler to register
*
* @example
* ```jsx
*import { useState } from 'react';
*import { ReactFlow, useOnSelectionChange } from '@xyflow/react';
*
*function SelectionDisplay() {
* const [selectedNodes, setSelectedNodes] = useState([]);
* const [selectedEdges, setSelectedEdges] = useState([]);
*
* // the passed handler has to be memoized, otherwise the hook will not work correctly
* const onChange = useCallback(({ nodes, edges }) => {
* setSelectedNodes(nodes.map((node) => node.id));
* setSelectedEdges(edges.map((edge) => edge.id));
* }, []);
*
* useOnSelectionChange({
* onChange,
* });
*
* return (
* <div>
* <p>Selected nodes: {selectedNodes.join(', ')}</p>
* <p>Selected edges: {selectedEdges.join(', ')}</p>
* </div>
* );
*}
*```
*
* @remarks You need to memoize the passed `onChange` handler, otherwise the hook will not work correctly.
*/
export function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) {
const store = useStoreApi();

View File

@@ -10,12 +10,30 @@ export type UseOnViewportChangeOptions = {
};
/**
* Hook for registering an onViewportChange handler.
* The `useOnViewportChange` hook lets you listen for changes to the viewport such
*as panning and zooming. You can provide a callback for each phase of a viewport
*change: `onStart`, `onChange`, and `onEnd`.
*
* @public
* @param params.onStart - gets called when the viewport starts changing
* @param params.onChange - gets called when the viewport changes
* @param params.onEnd - gets called when the viewport stops changing
*
* @example
* ```jsx
*import { useCallback } from 'react';
*import { useOnViewportChange } from '@xyflow/react';
*
*function ViewportChangeLogger() {
* useOnViewportChange({
* onStart: (viewport: Viewport) => console.log('start', viewport),
* onChange: (viewport: Viewport) => console.log('change', viewport),
* onEnd: (viewport: Viewport) => console.log('end', viewport),
* });
*
* return null;
*}
*```
*/
export function useOnViewportChange({ onStart, onChange, onEnd }: UseOnViewportChangeOptions) {
const store = useStoreApi();

View File

@@ -20,10 +20,33 @@ import type { ReactFlowInstance, Node, Edge, InternalNode, ReactFlowState, Gener
const selector = (s: ReactFlowState) => !!s.panZoom;
/**
* Hook for accessing the ReactFlow instance.
* This hook returns a ReactFlowInstance that can be used to update nodes and edges, manipulate the viewport, or query the current state of the flow.
*
* @public
* @returns ReactFlowInstance
*
* @example
* ```jsx
*import { useCallback, useState } from 'react';
*import { useReactFlow } from '@xyflow/react';
*
*export function NodeCounter() {
* const reactFlow = useReactFlow();
* const [count, setCount] = useState(0);
* const countNodes = useCallback(() => {
* setCount(reactFlow.getNodes().length);
* // you need to pass it as a dependency if you are using it with useEffect or useCallback
* // because at the first render, it's not initialized yet and some functions might not work.
* }, [reactFlow]);
*
* return (
* <div>
* <button onClick={countNodes}>Update count</button>
* <p>There are {count} nodes in the flow.</p>
* </div>
* );
*}
*```
*/
export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(): ReactFlowInstance<
NodeType,

View File

@@ -9,7 +9,9 @@ import type { Edge, Node, ReactFlowState } from '../types';
const zustandErrorMessage = errorMessages['error001']();
/**
* Hook for accessing the internal store. Should only be used in rare cases.
* This hook can be used to subscribe to internal state changes of the React Flow
*component. The `useStore` hook is re-exported from the [Zustand](https://github.com/pmndrs/zustand)
*state management library, so you should check out their docs for more details.
*
* @public
* @param selector
@@ -17,8 +19,13 @@ const zustandErrorMessage = errorMessages['error001']();
* @returns The selected state slice
*
* @example
* const nodes = useStore((state: ReactFlowState<MyNodeType>) => state.nodes);
* ```ts
* const nodes = useStore((state) => state.nodes);
* ```
*
* @remarks This hook should only be used if there is no other way to access the internal
*state. For many of the common use cases, there are dedicated hooks available
*such as {@link useReactFlow}, {@link useViewport}, etc.
*/
function useStore<StateSlice = unknown>(
selector: (state: ReactFlowState) => StateSlice,
@@ -33,6 +40,20 @@ function useStore<StateSlice = unknown>(
return useZustandStore(store, selector, equalityFn);
}
/**
* In some cases, you might need to access the store directly. This hook returns the store object which can be used on demand to access the state or dispatch actions.
*
* @returns The store object
*
* @example
* ```ts
* const store = useStoreApi();
* ```
*
* @remarks This hook should only be used if there is no other way to access the internal
*state. For many of the common use cases, there are dedicated hooks available
*such as {@link useReactFlow}, {@link useViewport}, etc.
*/
function useStoreApi<NodeType extends Node = Node, EdgeType extends Edge = Edge>() {
const store = useContext(StoreContext) as UseBoundStoreWithEqualityFn<
StoreApi<ReactFlowState<NodeType, EdgeType>>

View File

@@ -4,10 +4,48 @@ import type { UpdateNodeInternals, InternalNodeUpdate } from '@xyflow/system';
import { useStoreApi } from '../hooks/useStore';
/**
* Hook for updating node internals.
* When you programmatically add or remove handles to a node or update a node's
*handle position, you need to let React Flow know about it using this hook. This
*will update the internal dimensions of the node and properly reposition handles
*on the canvas if necessary.
*
* @public
* @returns function for updating node internals
*
* @example
* ```jsx
*import { useCallback, useState } from 'react';
*import { Handle, useUpdateNodeInternals } from '@xyflow/react';
*
*export default function RandomHandleNode({ id }) {
* const updateNodeInternals = useUpdateNodeInternals();
* const [handleCount, setHandleCount] = useState(0);
* const randomizeHandleCount = useCallback(() => {
* setHandleCount(Math.floor(Math.random() * 10));
* updateNodeInternals(id);
* }, [id, updateNodeInternals]);
*
* return (
* <>
* {Array.from({ length: handleCount }).map((_, index) => (
* <Handle
* key={index}
* type="target"
* position="left"
* id={`handle-${index}`}
* />
* ))}
*
* <div>
* <button onClick={randomizeHandleCount}>Randomize handle count</button>
* <p>There are {handleCount} handles on this node.</p>
* </div>
* </>
* );
*}
*```
* @remarks This hook can only be used in a component that is a child of a
*{@link ReactFlowProvider} or a {@link ReactFlow} component.
*/
export function useUpdateNodeInternals(): UpdateNodeInternals {
const store = useStoreApi();

View File

@@ -11,10 +11,33 @@ const viewportSelector = (state: ReactFlowState) => ({
});
/**
* Hook for getting the current viewport from the store.
* The `useViewport` hook is a convenient way to read the current state of the
*{@link Viewport} in a component. Components that use this hook
*will re-render **whenever the viewport changes**.
*
* @public
* @returns The current viewport
*
* @example
*
*```jsx
*import { useViewport } from '@xyflow/react';
*
*export default function ViewportDisplay() {
* const { x, y, zoom } = useViewport();
*
* return (
* <div>
* <p>
* The viewport is currently at ({x}, {y}) and zoomed to {zoom}.
* </p>
* </div>
* );
*}
*```
*
* @remarks This hook can only be used in a component that is a child of a
*{@link ReactFlowProvider} or a {@link ReactFlow} component.
*/
export function useViewport(): Viewport {
const viewport = useStore(viewportSelector, shallow);

View File

@@ -8,8 +8,8 @@ import type { Node, ReactFlowState } from '../types';
const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => {
return onlyRenderVisible
? getNodesInside<Node>(s.nodeLookup, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map(
(node) => node.id
)
(node) => node.id
)
: Array.from(s.nodeLookup.keys());
};

View File

@@ -47,12 +47,14 @@ const createStore = ({
...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, nodeExtent, defaultNodes, defaultEdges }),
setNodes: (nodes: Node[]) => {
const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get();
// setNodes() is called exclusively in response to user actions:
// - either when the `<ReactFlow nodes>` prop is updated in the controlled ReactFlow setup,
// - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup.
//
// When this happens, we take the note objects passed by the user and extend them with fields
// relevant for internal React Flow operations.
/*
* setNodes() is called exclusively in response to user actions:
* - either when the `<ReactFlow nodes>` prop is updated in the controlled ReactFlow setup,
* - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup.
*
* When this happens, we take the note objects passed by the user and extend them with fields
* relevant for internal React Flow operations.
*/
adoptUserNodes(nodes, nodeLookup, parentLookup, {
nodeOrigin,
nodeExtent,
@@ -81,9 +83,11 @@ const createStore = ({
set({ hasDefaultEdges: true });
}
},
// Every node gets registerd at a ResizeObserver. Whenever a node
// changes its dimensions, this function is called to measure the
// new dimensions and update the nodes.
/*
* Every node gets registerd at a ResizeObserver. Whenever a node
* changes its dimensions, this function is called to measure the
* new dimensions and update the nodes.
*/
updateNodeInternals: (updates, params = { triggerFitView: true }) => {
const {
triggerNodeChanges,
@@ -125,11 +129,13 @@ const createStore = ({
});
}
// here we are cirmumventing the onNodesChange handler
// in order to be able to display nodes even if the user
// has not provided an onNodesChange handler.
// Nodes are only rendered if they have a width and height
// attribute which they get from this handler.
/*
* here we are cirmumventing the onNodesChange handler
* in order to be able to display nodes even if the user
* has not provided an onNodesChange handler.
* Nodes are only rendered if they have a width and height
* attribute which they get from this handler.
*/
set({ fitViewDone: nextFitViewDone });
} else {
// we always want to trigger useStore calls whenever updateNodeInternals is called
@@ -155,9 +161,9 @@ const createStore = ({
type: 'position',
position: expandParent
? {
x: Math.max(0, dragItem.position.x),
y: Math.max(0, dragItem.position.y),
}
x: Math.max(0, dragItem.position.x),
y: Math.max(0, dragItem.position.y),
}
: dragItem.position,
dragging,
};
@@ -248,8 +254,10 @@ const createStore = ({
const nodeChanges = nodesToUnselect.map((n) => {
const internalNode = nodeLookup.get(n.id);
if (internalNode) {
// we need to unselect the internal node that was selected previously before we
// send the change to the user to prevent it to be selected while dragging the new node
/*
* we need to unselect the internal node that was selected previously before we
* send the change to the user to prevent it to be selected while dragging the new node
*/
internalNode.selected = false;
}
@@ -342,8 +350,10 @@ const createStore = ({
options
);
},
// we can't call an asnychronous function in updateNodeInternals
// for that we created this sync version of fitView
/*
* we can't call an asnychronous function in updateNodeInternals
* for that we created this sync version of fitView
*/
fitViewSync: (options?: FitViewOptions): boolean => {
const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get();

View File

@@ -52,7 +52,8 @@ import type {
*/
export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends Edge = Edge>
extends Omit<HTMLAttributes<HTMLDivElement>, 'onError'> {
/** An array of nodes to render in a controlled flow.
/**
* An array of nodes to render in a controlled flow.
* @example
* const nodes = [
* {
@@ -64,7 +65,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* ];
*/
nodes?: NodeType[];
/** An array of edges to render in a controlled flow.
/**
* An array of edges to render in a controlled flow.
* @example
* const edges = [
* {
@@ -79,7 +81,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
defaultNodes?: NodeType[];
/** The initial edges to render in an uncontrolled flow. */
defaultEdges?: EdgeType[];
/** Defaults to be applied to all new edges that are added to the flow.
/**
* Defaults to be applied to all new edges that are added to the flow.
*
* Properties on a new edge will override these defaults if they exist.
* @example
@@ -132,7 +135,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
onReconnect?: OnReconnect<EdgeType>;
onReconnectStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void;
onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void;
/** This event handler is called when a Node is updated
/**
* This event handler is called when a Node is updated
* @example // Use NodesState hook to create edges and get onNodesChange handler
* import ReactFlow, { useNodesState } from '@xyflow/react';
* const [edges, setNodes, onNodesChange] = useNodesState(initialNodes);
@@ -149,7 +153,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* return (<ReactFlow onNodeChange={onNodeChange} {...rest} />)
*/
onNodesChange?: OnNodesChange<NodeType>;
/** This event handler is called when a Edge is updated
/**
* This event handler is called when a Edge is updated
* @example // Use EdgesState hook to create edges and get onEdgesChange handler
* import ReactFlow, { useEdgesState } from '@xyflow/react';
* const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -181,7 +186,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
onSelectionStart?: (event: ReactMouseEvent) => void;
onSelectionEnd?: (event: ReactMouseEvent) => void;
onSelectionContextMenu?: (event: ReactMouseEvent, nodes: NodeType[]) => void;
/** When a connection line is completed and two nodes are connected by the user, this event fires with the new connection.
/**
* When a connection line is completed and two nodes are connected by the user, this event fires with the new connection.
*
* You can use the addEdge utility to convert the connection to a complete edge.
* @example // Use helper function to update edges onConnect
@@ -223,17 +229,20 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
onPaneMouseMove?: (event: ReactMouseEvent) => void;
/** This event handler gets called when mouse leaves the pane */
onPaneMouseLeave?: (event: ReactMouseEvent) => void;
/** Distance that the mouse can move between mousedown/up that will trigger a click
/**
* Distance that the mouse can move between mousedown/up that will trigger a click
* @default 0
*/
paneClickDistance?: number;
/** Distance that the mouse can move between mousedown/up that will trigger a click
/**
* Distance that the mouse can move between mousedown/up that will trigger a click
* @default 0
*/
nodeClickDistance?: number;
/** This handler gets called before the user deletes nodes or edges and provides a way to abort the deletion by returning false. */
onBeforeDelete?: OnBeforeDelete<NodeType, EdgeType>;
/** Custom node types to be available in a flow.
/**
* Custom node types to be available in a flow.
*
* React Flow matches a node's type to a component in the nodeTypes object.
* @example
@@ -242,7 +251,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* const nodeTypes = { nameOfNodeType: CustomNode };
*/
nodeTypes?: NodeTypes;
/** Custom edge types to be available in a flow.
/**
* Custom edge types to be available in a flow.
*
* React Flow matches an edge's type to a component in the edgeTypes object.
* @example
@@ -251,7 +261,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* const edgeTypes = { nameOfEdgeType: CustomEdge };
*/
edgeTypes?: EdgeTypes;
/** The type of edge path to use for connection lines.
/**
* The type of edge path to use for connection lines.
*
* Although created edges can be of any type, React Flow needs to know what type of path to render for the connection line before the edge is created!
*/
@@ -262,17 +273,20 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
connectionLineComponent?: ConnectionLineComponent;
/** Styles to be applied to the container of the connection line */
connectionLineContainerStyle?: CSSProperties;
/** 'strict' connection mode will only allow you to connect source handles to target handles.
/**
* 'strict' connection mode will only allow you to connect source handles to target handles.
*
* 'loose' connection mode will allow you to connect handles of any type to one another.
* @default 'strict'
*/
connectionMode?: ConnectionMode;
/** Pressing down this key deletes all selected nodes & edges.
/**
* Pressing down this key deletes all selected nodes & edges.
* @default 'Backspace'
*/
deleteKeyCode?: KeyCode | null;
/** If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false.
/**
* If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false.
*
* By setting this prop to null you can disable this functionality.
* @default 'Space'
@@ -280,91 +294,110 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
selectionKeyCode?: KeyCode | null;
/** Select multiple elements with a selection box, without pressing down selectionKey */
selectionOnDrag?: boolean;
/** When set to "partial", when the user creates a selection box by click and dragging nodes that are only partially in the box are still selected.
/**
* When set to "partial", when the user creates a selection box by click and dragging nodes that are only partially in the box are still selected.
* @default 'full'
*/
selectionMode?: SelectionMode;
/** If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false.
/**
* If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false.
*
* By setting this prop to null you can disable this functionality.
* @default 'Space'
*/
panActivationKeyCode?: KeyCode | null;
/** Pressing down this key you can select multiple elements by clicking.
/**
* Pressing down this key you can select multiple elements by clicking.
* @default 'Meta' for macOS, "Ctrl" for other systems
*/
multiSelectionKeyCode?: KeyCode | null;
/** If a key is set, you can zoom the viewport while that key is held down even if panOnScroll is set to false.
/**
* If a key is set, you can zoom the viewport while that key is held down even if panOnScroll is set to false.
*
* By setting this prop to null you can disable this functionality.
* @default 'Meta' for macOS, "Ctrl" for other systems
* */
*
*/
zoomActivationKeyCode?: KeyCode | null;
/** Set this prop to make the flow snap to the grid */
snapToGrid?: boolean;
/** Grid all nodes will snap to
/**
* Grid all nodes will snap to
* @example [20, 20]
*/
snapGrid?: SnapGrid;
/** You can enable this optimisation to instruct Svelte Flow to only render nodes and edges that would be visible in the viewport.
/**
* You can enable this optimisation to instruct Svelte Flow to only render nodes and edges that would be visible in the viewport.
*
* This might improve performance when you have a large number of nodes and edges but also adds an overhead.
* @default false
*/
onlyRenderVisibleElements?: boolean;
/** Controls if all nodes should be draggable
/**
* Controls if all nodes should be draggable
* @default true
*/
nodesDraggable?: boolean;
/** Controls if all nodes should be connectable to each other
/**
* Controls if all nodes should be connectable to each other
* @default true
*/
nodesConnectable?: boolean;
/** Controls if all nodes should be focusable
/**
* Controls if all nodes should be focusable
* @default true
*/
nodesFocusable?: boolean;
/** Defines nodes relative position to its coordinates
/**
* Defines nodes relative position to its coordinates
* @example
* [0, 0] // default, top left
* [0.5, 0.5] // center
* [1, 1] // bottom right
*/
nodeOrigin?: NodeOrigin;
/** Controls if all edges should be focusable
/**
* Controls if all edges should be focusable
* @default true
*/
edgesFocusable?: boolean;
/** Controls if all edges should be updateable
/**
* Controls if all edges should be updateable
* @default true
*/
edgesReconnectable?: boolean;
/** Controls if all elements should (nodes & edges) be selectable
/**
* Controls if all elements should (nodes & edges) be selectable
* @default true
*/
elementsSelectable?: boolean;
/** If true, nodes get selected on drag
/**
* If true, nodes get selected on drag
* @default true
*/
selectNodesOnDrag?: boolean;
/** Enableing this prop allows users to pan the viewport by clicking and dragging.
/**
* Enableing this prop allows users to pan the viewport by clicking and dragging.
*
* You can also set this prop to an array of numbers to limit which mouse buttons can activate panning.
* @example [0, 2] // allows panning with the left and right mouse buttons
* [0, 1, 2, 3, 4] // allows panning with all mouse buttons
*/
panOnDrag?: boolean | number[];
/** Minimum zoom level
/**
* Minimum zoom level
* @default 0.5
*/
minZoom?: number;
/** Maximum zoom level
/**
* Maximum zoom level
* @default 2
*/
maxZoom?: number;
/** Controlled viewport to be used instead of internal one */
viewport?: Viewport;
/** Sets the initial position and zoom of the viewport.
/**
* Sets the initial position and zoom of the viewport.
*
* If a default viewport is provided but fitView is enabled, the default viewport will be ignored.
* @example
@@ -378,23 +411,27 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* Gets called when the viewport changes.
*/
onViewportChange?: (viewport: Viewport) => void;
/** By default the viewport extends infinitely. You can use this prop to set a boundary.
/**
* By default the viewport extends infinitely. You can use this prop to set a boundary.
*
* The first pair of coordinates is the top left boundary and the second pair is the bottom right.
* @example [[-1000, -10000], [1000, 1000]]
*/
translateExtent?: CoordinateExtent;
/** Disabling this prop will allow the user to scroll the page even when their pointer is over the flow.
/**
* Disabling this prop will allow the user to scroll the page even when their pointer is over the flow.
* @default true
*/
preventScrolling?: boolean;
/** By default nodes can be placed on an infinite flow. You can use this prop to set a boundary.
/**
* By default nodes can be placed on an infinite flow. You can use this prop to set a boundary.
*
* The first pair of coordinates is the top left boundary and the second pair is the bottom right.
* @example [[-1000, -10000], [1000, 1000]]
*/
nodeExtent?: CoordinateExtent;
/** Color of edge markers
/**
* Color of edge markers
* @example "#b1b1b7"
*/
defaultMarkerColor?: string;
@@ -402,17 +439,20 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
zoomOnScroll?: boolean;
/** Controls if the viewport should zoom by pinching on a touch screen */
zoomOnPinch?: boolean;
/** Controls if the viewport should pan by scrolling inside the container
/**
* Controls if the viewport should pan by scrolling inside the container
*
* Can be limited to a specific direction with panOnScrollMode
*/
panOnScroll?: boolean;
/** Controls how fast viewport should be panned on scroll.
/**
* Controls how fast viewport should be panned on scroll.
*
* Use togther with panOnScroll prop.
*/
panOnScrollSpeed?: number;
/** This prop is used to limit the direction of panning when panOnScroll is enabled.
/**
* This prop is used to limit the direction of panning when panOnScroll is enabled.
*
* The "free" option allows panning in any direction.
* @default "free"
@@ -427,7 +467,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
noPanClassName?: string;
/** If set, initial viewport will show all nodes & edges */
fitView?: boolean;
/** Options to be used in combination with fitView
/**
* Options to be used in combination with fitView
* @example
* const fitViewOptions = {
* padding: 0.1,
@@ -439,30 +480,35 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* };
*/
fitViewOptions?: FitViewOptions;
/**The connectOnClick option lets you click or tap on a source handle to start a connection
/**
*The connectOnClick option lets you click or tap on a source handle to start a connection
* and then click on a target handle to complete the connection.
*
* If you set this option to false, users will need to drag the connection line to the target
* handle to create a connection.
*/
connectOnClick?: boolean;
/** Set position of the attribution
/**
* Set position of the attribution
* @default 'bottom-right'
* @example 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
*/
attributionPosition?: PanelPosition;
/** By default, we render a small attribution in the corner of your flows that links back to the project.
/**
* By default, we render a small attribution in the corner of your flows that links back to the project.
*
* Anyone is free to remove this attribution whether they're a Pro subscriber or not
* but we ask that you take a quick look at our {@link https://reactflow.dev/learn/troubleshooting/remove-attribution | removing attribution guide}
* before doing so.
*/
proOptions?: ProOptions;
/** Enabling this option will raise the z-index of nodes when they are selected.
/**
* Enabling this option will raise the z-index of nodes when they are selected.
* @default true
*/
elevateNodesOnSelect?: boolean;
/** Enabling this option will raise the z-index of edges when they are selected.
/**
* Enabling this option will raise the z-index of edges when they are selected.
* @default true
*/
elevateEdgesOnSelect?: boolean;
@@ -471,36 +517,43 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* @default false
*/
disableKeyboardA11y?: boolean;
/** You can enable this prop to automatically pan the viewport while dragging a node.
/**
* You can enable this prop to automatically pan the viewport while dragging a node.
* @default true
*/
autoPanOnNodeDrag?: boolean;
/** You can enable this prop to automatically pan the viewport while dragging a node.
/**
* You can enable this prop to automatically pan the viewport while dragging a node.
* @default true
*/
autoPanOnConnect?: boolean;
/** The speed at which the viewport pans while dragging a node or a selection box.
/**
* The speed at which the viewport pans while dragging a node or a selection box.
* @default 15
*/
autoPanSpeed?: number;
/** You can enable this prop to automatically pan the viewport while making a new connection.
/**
* You can enable this prop to automatically pan the viewport while making a new connection.
* @default true
*/
connectionRadius?: number;
/** Ocassionally something may happen that causes Svelte Flow to throw an error.
/**
* Ocassionally something may happen that causes Svelte Flow to throw an error.
*
* Instead of exploding your application, we log a message to the console and then call this event handler.
* You might use it for additional logging or to show a message to the user.
*/
onError?: OnError;
/** This callback can be used to validate a new connection
/**
* This callback can be used to validate a new connection
*
* If you return false, the edge will not be added to your flow.
* If you have custom connection logic its preferred to use this callback over the isValidConnection prop on the handle component for performance reasons.
* @default (connection: Connection) => true
*/
isValidConnection?: IsValidConnection<EdgeType>;
/** With a threshold greater than zero you can control the distinction between node drag and click events.
/**
* With a threshold greater than zero you can control the distinction between node drag and click events.
*
* If threshold equals 1, you need to drag the node 1 pixel before a drag event is fired.
* @default 1
@@ -510,12 +563,14 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
width?: number;
/** Sets a fixed height for the flow */
height?: number;
/** Controls color scheme used for styling the flow
/**
* Controls color scheme used for styling the flow
* @default 'system'
* @example 'system' | 'light' | 'dark'
*/
colorMode?: ColorMode;
/** If set true, some debug information will be logged to the console like which events are fired.
/**
* If set true, some debug information will be logged to the console like which events are fired.
*
* @default undefined
*/

View File

@@ -11,13 +11,17 @@ import {
} from '@xyflow/system';
import type { Node, Edge, InternalNode } from '../types';
// 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.
/*
* 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: any[], elements: any[]): any[] {
const updatedElements: any[] = [];
// By storing a map of changes for each element, we can a quick lookup as we
// iterate over the elements array!
/*
* 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<any, any[]>();
const addItemChanges: any[] = [];
@@ -26,15 +30,19 @@ function applyChanges(changes: any[], elements: any[]): any[] {
addItemChanges.push(change);
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!
/*
* 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.
/*
* 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]);
@@ -45,8 +53,10 @@ function applyChanges(changes: any[], elements: any[]): any[] {
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.
/*
* 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;
@@ -62,9 +72,11 @@ function applyChanges(changes: any[], elements: any[]): any[] {
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.
/**
* 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) {
@@ -74,8 +86,10 @@ function applyChanges(changes: any[], elements: any[]): any[] {
updatedElements.push(updatedElement);
}
// we need to wait for all changes to be applied before adding new items
// to be able to add them at the correct index
/*
* we need to wait for all changes to be applied before adding new items
* to be able to add them at the correct index
*/
if (addItemChanges.length) {
addItemChanges.forEach((change) => {
if (change.index !== undefined) {
@@ -134,21 +148,21 @@ function applyChange(change: any, element: any): any {
* 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.
*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} />
);
* (changes) => {
* setNodes((oldNodes) => applyNodeChanges(changes, oldNodes));
* },
* [setNodes],
* );
*
* return (
* <ReactFLow nodes={nodes} edges={edges} onNodesChange={onNodesChange} />
* );
*/
export function applyNodeChanges<NodeType extends Node = Node>(
changes: NodeChange<NodeType>[],
@@ -161,21 +175,21 @@ export function applyNodeChanges<NodeType extends Node = Node>(
* 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.
*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} />
);
* (changes) => {
* setEdges((oldEdges) => applyEdgeChanges(changes, oldEdges));
* },
* [setEdges],
* );
*
* return (
* <ReactFlow nodes={nodes} edges={edges} onEdgesChange={onEdgesChange} />
* );
*/
export function applyEdgeChanges<EdgeType extends Edge = Edge>(
changes: EdgeChange<EdgeType>[],
@@ -205,9 +219,11 @@ export function getSelectionChanges(
// 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 :/
/*
* 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));

48
pnpm-lock.yaml generated
View File

@@ -428,6 +428,9 @@ importers:
tooling/eslint-config:
devDependencies:
'@stylistic/eslint-plugin':
specifier: ^3.1.0
version: 3.1.0(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/eslint-plugin':
specifier: ^8.23.0
version: 8.23.0(@typescript-eslint/parser@8.23.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
@@ -1706,6 +1709,12 @@ packages:
resolution: {integrity: sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==}
engines: {node: '>=18'}
'@stylistic/eslint-plugin@3.1.0':
resolution: {integrity: sha512-pA6VOrOqk0+S8toJYhQGv2MWpQQR0QpeUo9AhNkC49Y26nxBQ/nH1rta9bUU1rPw2fJ1zZEMV5oCX5AazT7J2g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: '>=8.40.0'
'@svelte-put/shortcut@3.1.1':
resolution: {integrity: sha512-2L5EYTZXiaKvbEelVkg5znxqvfZGZai3m97+cAiUBhLZwXnGtviTDpHxOoZBsqz41szlfRMcamW/8o0+fbW3ZQ==}
peerDependencies:
@@ -2217,6 +2226,11 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
acorn@8.14.0:
resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==}
engines: {node: '>=0.4.0'}
hasBin: true
aggregate-error@3.1.0:
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
engines: {node: '>=8'}
@@ -3290,6 +3304,10 @@ packages:
esm-env@1.0.0:
resolution: {integrity: sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==}
espree@10.3.0:
resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
espree@9.6.1:
resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -4776,6 +4794,10 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
picomatch@4.0.2:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
pify@2.3.0:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
@@ -7959,6 +7981,18 @@ snapshots:
'@sindresorhus/merge-streams@1.0.0': {}
'@stylistic/eslint-plugin@3.1.0(eslint@8.57.0)(typescript@5.4.5)':
dependencies:
'@typescript-eslint/utils': 8.23.0(eslint@8.57.0)(typescript@5.4.5)
eslint: 8.57.0
eslint-visitor-keys: 4.2.0
espree: 10.3.0
estraverse: 5.3.0
picomatch: 4.0.2
transitivePeerDependencies:
- supports-color
- typescript
'@svelte-put/shortcut@3.1.1(svelte@4.2.12)':
dependencies:
svelte: 4.2.12
@@ -8613,12 +8647,18 @@ snapshots:
dependencies:
acorn: 8.10.0
acorn-jsx@5.3.2(acorn@8.14.0):
dependencies:
acorn: 8.14.0
acorn@8.10.0: {}
acorn@8.11.2: {}
acorn@8.11.3: {}
acorn@8.14.0: {}
aggregate-error@3.1.0:
dependencies:
clean-stack: 2.2.0
@@ -10257,6 +10297,12 @@ snapshots:
esm-env@1.0.0: {}
espree@10.3.0:
dependencies:
acorn: 8.14.0
acorn-jsx: 5.3.2(acorn@8.14.0)
eslint-visitor-keys: 4.2.0
espree@9.6.1:
dependencies:
acorn: 8.10.0
@@ -11961,6 +12007,8 @@ snapshots:
picomatch@2.3.1: {}
picomatch@4.0.2: {}
pify@2.3.0: {}
pify@4.0.1: {}

View File

@@ -5,11 +5,12 @@
"license": "MIT",
"main": "src/index.js",
"devDependencies": {
"eslint-config-prettier": "^10.0.1",
"eslint-config-turbo": "^2.4.0",
"eslint-plugin-react": "^7.37.4",
"@stylistic/eslint-plugin": "^3.1.0",
"@typescript-eslint/eslint-plugin": "^8.23.0",
"@typescript-eslint/parser": "^8.23.0",
"eslint-plugin-prettier": "^4.2.1"
"eslint-config-prettier": "^10.0.1",
"eslint-config-turbo": "^2.4.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.37.4"
}
}

View File

@@ -13,7 +13,7 @@ module.exports = {
'turbo',
'prettier',
],
plugins: ['react', '@typescript-eslint'],
plugins: ['react', '@typescript-eslint', '@stylistic'],
parserOptions: {
ecmaFeatures: {
jsx: true,
@@ -28,5 +28,7 @@ module.exports = {
},
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
'@stylistic/indent': ['error', 2],
'@stylistic/multiline-comment-style': ['error', 'starred-block'],
},
};