Merge branch 'main' into bug-fix-node-positions
This commit is contained in:
@@ -1,5 +1,65 @@
|
||||
# @xyflow/react
|
||||
|
||||
## 12.8.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`26f2cdd7`](https://github.com/xyflow/xyflow/commit/26f2cdd720fc2c8fb337d3af13b82dab6a90fb60)]:
|
||||
- @xyflow/system@0.0.65
|
||||
|
||||
## 12.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#5361](https://github.com/xyflow/xyflow/pull/5361) [`90e9247a`](https://github.com/xyflow/xyflow/commit/90e9247adbdfa9d06db97e1d0d895e35c960551c) Thanks [@peterkogo](https://github.com/peterkogo)! - Render edges above nodes when they are within a subflow
|
||||
|
||||
- [#5344](https://github.com/xyflow/xyflow/pull/5344) [`2441bf8d`](https://github.com/xyflow/xyflow/commit/2441bf8d97a6b72494f216915d52d5acbeefefde) Thanks [@moklick](https://github.com/moklick)! - Add connectionDragThreshold prop
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5368](https://github.com/xyflow/xyflow/pull/5368) [`445017ed`](https://github.com/xyflow/xyflow/commit/445017ed6fdedee4b39a0eaeb1cbbe0155f9a037) Thanks [@moklick](https://github.com/moklick)! - Cleanup store updater
|
||||
|
||||
- [#5362](https://github.com/xyflow/xyflow/pull/5362) [`72dc1d60`](https://github.com/xyflow/xyflow/commit/72dc1d602110947e3db83c37b9a9125ee85cf4bc) Thanks [@moklick](https://github.com/moklick)! - Remove pointer events from Panel via CSS while a selection gets dragged
|
||||
|
||||
- Updated dependencies [[`72dc1d60`](https://github.com/xyflow/xyflow/commit/72dc1d602110947e3db83c37b9a9125ee85cf4bc), [`90e9247a`](https://github.com/xyflow/xyflow/commit/90e9247adbdfa9d06db97e1d0d895e35c960551c), [`2441bf8d`](https://github.com/xyflow/xyflow/commit/2441bf8d97a6b72494f216915d52d5acbeefefde)]:
|
||||
- @xyflow/system@0.0.64
|
||||
|
||||
## 12.7.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5354](https://github.com/xyflow/xyflow/pull/5354) [`c4312d89`](https://github.com/xyflow/xyflow/commit/c4312d8997ecdc7ef12cfa4efc1fde7131a2b950) Thanks [@moklick](https://github.com/moklick)! - Add TSDoc annotations
|
||||
|
||||
- [#5333](https://github.com/xyflow/xyflow/pull/5333) [`3d7e8b6b`](https://github.com/xyflow/xyflow/commit/3d7e8b6bb10001ee84d79ca4f6a9fd0053c4a276) Thanks [@peterkogo](https://github.com/peterkogo)! - Add missing type exports
|
||||
|
||||
- [#5353](https://github.com/xyflow/xyflow/pull/5353) [`4dd3ae66`](https://github.com/xyflow/xyflow/commit/4dd3ae6684fa3c62ee8cabd64e7631a52173c8f4) Thanks [@aidanbarrett](https://github.com/aidanbarrett)! - Add missing `FinalConnectionState` parameter to `onReconnectEnd` prop types
|
||||
|
||||
- Updated dependencies [[`c4312d89`](https://github.com/xyflow/xyflow/commit/c4312d8997ecdc7ef12cfa4efc1fde7131a2b950), [`3d7e8b6b`](https://github.com/xyflow/xyflow/commit/3d7e8b6bb10001ee84d79ca4f6a9fd0053c4a276), [`9c61000c`](https://github.com/xyflow/xyflow/commit/9c61000cac6277ce97274cc626fa7266f82dec27)]:
|
||||
- @xyflow/system@0.0.63
|
||||
|
||||
## 12.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#5299](https://github.com/xyflow/xyflow/pull/5299) [`848b486b`](https://github.com/xyflow/xyflow/commit/848b486b2201b650ecb3317f367a723edb2458e1) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaRole` prop to nodes and edges
|
||||
|
||||
- [#5280](https://github.com/xyflow/xyflow/pull/5280) [`dba6faf2`](https://github.com/xyflow/xyflow/commit/dba6faf20e7ec2524d5270d177331d3bd260f3ac) Thanks [@moklick](https://github.com/moklick)! - Improve typing for Nodes
|
||||
|
||||
- [#5276](https://github.com/xyflow/xyflow/pull/5276) [`6ce44a05`](https://github.com/xyflow/xyflow/commit/6ce44a05c829068ff5a8416ce3fa4ee6e0eced48) Thanks [@moklick](https://github.com/moklick)! - Add an `ease` and `interpolate` option to all function that alter the viewport
|
||||
|
||||
- [#5277](https://github.com/xyflow/xyflow/pull/5277) [`f59730ce`](https://github.com/xyflow/xyflow/commit/f59730ce3530a91f579f6bbd2ea9335680f552ef) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaLabelConfig` prop for customizing UI text like aria labels and descriptions.
|
||||
|
||||
- [#5317](https://github.com/xyflow/xyflow/pull/5317) [`09458f52`](https://github.com/xyflow/xyflow/commit/09458f52ff57356e03404c58e9bfdbfd50579850) Thanks [@moklick](https://github.com/moklick)! - Add `domAttributes` option for nodes and edges
|
||||
|
||||
- [#5326](https://github.com/xyflow/xyflow/pull/5326) [`050b511c`](https://github.com/xyflow/xyflow/commit/050b511cd6966ba526299f7ca11f9ca4791fd2cf) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent NodeResizer controls to become too small when zooming out
|
||||
|
||||
- [#5308](https://github.com/xyflow/xyflow/pull/5308) [`09fab679`](https://github.com/xyflow/xyflow/commit/09fab6794031410c9e9465281d038c3520afe783) Thanks [@printerscanner](https://github.com/printerscanner)! - Focus nodes on tab if not within the viewport and add a new prop `autoPanOnNodeFocus`
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`848b486b`](https://github.com/xyflow/xyflow/commit/848b486b2201b650ecb3317f367a723edb2458e1), [`dba6faf2`](https://github.com/xyflow/xyflow/commit/dba6faf20e7ec2524d5270d177331d3bd260f3ac), [`6ce44a05`](https://github.com/xyflow/xyflow/commit/6ce44a05c829068ff5a8416ce3fa4ee6e0eced48), [`f59730ce`](https://github.com/xyflow/xyflow/commit/f59730ce3530a91f579f6bbd2ea9335680f552ef), [`09458f52`](https://github.com/xyflow/xyflow/commit/09458f52ff57356e03404c58e9bfdbfd50579850), [`050b511c`](https://github.com/xyflow/xyflow/commit/050b511cd6966ba526299f7ca11f9ca4791fd2cf)]:
|
||||
- @xyflow/system@0.0.62
|
||||
|
||||
## 12.6.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xyflow/react",
|
||||
"version": "12.6.4",
|
||||
"version": "12.8.1",
|
||||
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
|
||||
"keywords": [
|
||||
"react",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useRef, useEffect, memo } from 'react';
|
||||
import { useRef, useEffect, memo, useCallback } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import {
|
||||
XYResizer,
|
||||
ResizeControlVariant,
|
||||
@@ -13,18 +14,28 @@ import {
|
||||
evaluateAbsolutePosition,
|
||||
ParentExpandChild,
|
||||
XYPosition,
|
||||
ControlPosition,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStoreApi } from '../../hooks/useStore';
|
||||
import { useStoreApi, useStore } from '../../hooks/useStore';
|
||||
import { useNodeId } from '../../contexts/NodeIdContext';
|
||||
import type { ResizeControlProps, ResizeControlLineProps } from './types';
|
||||
import { ReactFlowState } from '../../types';
|
||||
|
||||
const scaleSelector = (calculateScale: boolean) => (store: ReactFlowState) =>
|
||||
calculateScale ? `${Math.max(1 / store.transform[2], 1)}` : undefined;
|
||||
|
||||
const defaultPositions: Record<ResizeControlVariant, ControlPosition> = {
|
||||
[ResizeControlVariant.Line]: 'right',
|
||||
[ResizeControlVariant.Handle]: 'bottom-right',
|
||||
};
|
||||
|
||||
function ResizeControl({
|
||||
nodeId,
|
||||
position,
|
||||
variant = ResizeControlVariant.Handle,
|
||||
className,
|
||||
style = {},
|
||||
style = undefined,
|
||||
children,
|
||||
color,
|
||||
minWidth = 10,
|
||||
@@ -33,6 +44,7 @@ function ResizeControl({
|
||||
maxHeight = Number.MAX_VALUE,
|
||||
keepAspectRatio = false,
|
||||
resizeDirection,
|
||||
autoScale = true,
|
||||
shouldResize,
|
||||
onResizeStart,
|
||||
onResize,
|
||||
@@ -42,10 +54,13 @@ function ResizeControl({
|
||||
const id = typeof nodeId === 'string' ? nodeId : contextNodeId;
|
||||
const store = useStoreApi();
|
||||
const resizeControlRef = useRef<HTMLDivElement>(null);
|
||||
const defaultPosition = variant === ResizeControlVariant.Line ? 'right' : 'bottom-right';
|
||||
const controlPosition = position ?? defaultPosition;
|
||||
|
||||
const isHandleControl = variant === ResizeControlVariant.Handle;
|
||||
const scale = useStore(
|
||||
useCallback(scaleSelector(isHandleControl && autoScale), [isHandleControl, autoScale]),
|
||||
shallow
|
||||
);
|
||||
const resizer = useRef<XYResizerInstance | null>(null);
|
||||
const controlPosition = position ?? defaultPositions[variant];
|
||||
|
||||
useEffect(() => {
|
||||
if (!resizeControlRef.current || !id) {
|
||||
@@ -192,14 +207,16 @@ function ResizeControl({
|
||||
]);
|
||||
|
||||
const positionClassNames = controlPosition.split('-');
|
||||
const colorStyleProp = variant === ResizeControlVariant.Line ? 'borderColor' : 'backgroundColor';
|
||||
const controlStyle = color ? { ...style, [colorStyleProp]: color } : style;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cc(['react-flow__resize-control', 'nodrag', ...positionClassNames, variant, className])}
|
||||
ref={resizeControlRef}
|
||||
style={controlStyle}
|
||||
style={{
|
||||
...style,
|
||||
scale,
|
||||
...(color && { [isHandleControl ? 'backgroundColor' : 'borderColor']: color }),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,7 @@ export function NodeResizer({
|
||||
maxWidth = Number.MAX_VALUE,
|
||||
maxHeight = Number.MAX_VALUE,
|
||||
keepAspectRatio = false,
|
||||
autoScale = true,
|
||||
shouldResize,
|
||||
onResizeStart,
|
||||
onResize,
|
||||
@@ -66,6 +67,7 @@ export function NodeResizer({
|
||||
maxHeight={maxHeight}
|
||||
onResizeStart={onResizeStart}
|
||||
keepAspectRatio={keepAspectRatio}
|
||||
autoScale={autoScale}
|
||||
shouldResize={shouldResize}
|
||||
onResize={onResize}
|
||||
onResizeEnd={onResizeEnd}
|
||||
@@ -85,6 +87,7 @@ export function NodeResizer({
|
||||
maxHeight={maxHeight}
|
||||
onResizeStart={onResizeStart}
|
||||
keepAspectRatio={keepAspectRatio}
|
||||
autoScale={autoScale}
|
||||
shouldResize={shouldResize}
|
||||
onResize={onResize}
|
||||
onResizeEnd={onResizeEnd}
|
||||
|
||||
@@ -59,6 +59,11 @@ export type NodeResizerProps = {
|
||||
* @default false
|
||||
*/
|
||||
keepAspectRatio?: boolean;
|
||||
/**
|
||||
* Scale the controls with the zoom level.
|
||||
* @default true
|
||||
*/
|
||||
autoScale?: boolean;
|
||||
/** Callback to determine if node should resize. */
|
||||
shouldResize?: ShouldResize;
|
||||
/** Callback called when resizing starts. */
|
||||
@@ -82,6 +87,7 @@ export type ResizeControlProps = Pick<
|
||||
| 'maxHeight'
|
||||
| 'keepAspectRatio'
|
||||
| 'shouldResize'
|
||||
| 'autoScale'
|
||||
| 'onResizeStart'
|
||||
| 'onResize'
|
||||
| 'onResizeEnd'
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
// Reconnectable edges have a anchors around their handles to reconnect the edge.
|
||||
import { XYHandle, type Connection, EdgePosition, FinalConnectionState, HandleType } from '@xyflow/system';
|
||||
import {
|
||||
XYHandle,
|
||||
type Connection,
|
||||
EdgePosition,
|
||||
FinalConnectionState,
|
||||
HandleType,
|
||||
OnConnectStart,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { EdgeAnchor } from '../Edges/EdgeAnchor';
|
||||
import type { EdgeWrapperProps, Edge } from '../../types/edges';
|
||||
@@ -60,15 +67,17 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
|
||||
} = store.getState();
|
||||
const isTarget = oppositeHandle.type === 'target';
|
||||
|
||||
setReconnecting(true);
|
||||
onReconnectStart?.(event, edge, oppositeHandle.type);
|
||||
|
||||
const _onReconnectEnd = (evt: MouseEvent | TouchEvent, connectionState: FinalConnectionState) => {
|
||||
setReconnecting(false);
|
||||
onReconnectEnd?.(evt, edge, oppositeHandle.type, connectionState);
|
||||
};
|
||||
|
||||
const onConnectEdge = (connection: Connection) => onReconnect?.(edge, connection);
|
||||
const _onConnectStart: OnConnectStart = (_event, params) => {
|
||||
setReconnecting(true);
|
||||
onReconnectStart?.(event, edge, oppositeHandle.type);
|
||||
onConnectStart?.(_event, params);
|
||||
};
|
||||
|
||||
XYHandle.onPointerDown(event.nativeEvent, {
|
||||
autoPanOnConnect,
|
||||
@@ -86,12 +95,13 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
|
||||
panBy,
|
||||
isValidConnection,
|
||||
onConnect: onConnectEdge,
|
||||
onConnectStart,
|
||||
onConnectStart: _onConnectStart,
|
||||
onConnectEnd,
|
||||
onReconnectEnd: _onReconnectEnd,
|
||||
updateConnection,
|
||||
getTransform: () => store.getState().transform,
|
||||
getFromHandle: () => store.getState().connection.fromHandle,
|
||||
dragThreshold: store.getState().connectionDragThreshold,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
|
||||
if (EdgeComponent === undefined) {
|
||||
onError?.('011', errorMessages['error011'](edgeType));
|
||||
edgeType = 'default';
|
||||
EdgeComponent = builtinEdgeTypes.default;
|
||||
EdgeComponent = edgeTypes?.['default'] || builtinEdgeTypes.default;
|
||||
}
|
||||
|
||||
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
|
||||
@@ -199,7 +199,7 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
|
||||
onKeyDown={isFocusable ? onKeyDown : undefined}
|
||||
tabIndex={isFocusable ? 0 : undefined}
|
||||
role={edge.ariaRole ?? (isFocusable ? 'group' : 'img')}
|
||||
aria-roledescription={edge.ariaRoleDescription || 'edge'}
|
||||
aria-roledescription="edge"
|
||||
data-id={id}
|
||||
data-testid={`rf__edge-${id}`}
|
||||
aria-label={
|
||||
@@ -207,6 +207,7 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
|
||||
}
|
||||
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
|
||||
ref={edgeRef}
|
||||
{...edge.domAttributes}
|
||||
>
|
||||
{!reconnecting && (
|
||||
<EdgeComponent
|
||||
|
||||
@@ -149,6 +149,7 @@ function HandleComponent(
|
||||
getTransform: () => store.getState().transform,
|
||||
getFromHandle: () => store.getState().connection.fromHandle,
|
||||
autoPanSpeed: currentStore.autoPanSpeed,
|
||||
dragThreshold: currentStore.connectionDragThreshold,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getNodeDimensions,
|
||||
isInputDOMNode,
|
||||
nodeHasDimensions,
|
||||
getNodesInside,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
@@ -57,7 +58,7 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
if (NodeComponent === undefined) {
|
||||
onError?.('003', errorMessages['error003'](nodeType));
|
||||
nodeType = 'default';
|
||||
NodeComponent = builtinNodeTypes.default;
|
||||
NodeComponent = nodeTypes?.['default'] || builtinNodeTypes.default;
|
||||
}
|
||||
|
||||
const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'));
|
||||
@@ -158,6 +159,28 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
if (disableKeyboardA11y || !nodeRef.current?.matches(':focus-visible')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { transform, width, height, autoPanOnNodeFocus, setCenter } = store.getState();
|
||||
|
||||
if (!autoPanOnNodeFocus) {
|
||||
return;
|
||||
}
|
||||
|
||||
const withinViewport =
|
||||
getNodesInside(new Map([[id, node]]), { x: 0, y: 0, width, height }, transform, true).length > 0;
|
||||
|
||||
if (!withinViewport) {
|
||||
setCenter(node.position.x + nodeDimensions.width / 2, node.position.y + nodeDimensions.height / 2, {
|
||||
zoom: transform[2],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cc([
|
||||
@@ -195,10 +218,12 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
onDoubleClick={onDoubleClickHandler}
|
||||
onKeyDown={isFocusable ? onKeyDown : undefined}
|
||||
tabIndex={isFocusable ? 0 : undefined}
|
||||
onFocus={isFocusable ? onFocus : undefined}
|
||||
role={node.ariaRole ?? (isFocusable ? 'group' : undefined)}
|
||||
aria-roledescription={node.ariaRoleDescription || 'node'}
|
||||
aria-roledescription="node"
|
||||
aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`}
|
||||
aria-label={node.ariaLabel}
|
||||
{...node.domAttributes}
|
||||
>
|
||||
<Provider value={id}>
|
||||
<NodeComponent
|
||||
|
||||
@@ -2,9 +2,6 @@ import { HTMLAttributes, forwardRef } from 'react';
|
||||
import cc from 'classcat';
|
||||
import type { PanelPosition } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import type { ReactFlowState } from '../../types';
|
||||
|
||||
/**
|
||||
* @expand
|
||||
*/
|
||||
@@ -16,8 +13,6 @@ export type PanelProps = HTMLAttributes<HTMLDivElement> & {
|
||||
position?: PanelPosition;
|
||||
};
|
||||
|
||||
const selector = (s: ReactFlowState) => (s.userSelectionActive ? 'none' : 'all');
|
||||
|
||||
/**
|
||||
* The `<Panel />` component helps you position content above the viewport.
|
||||
* It is used internally by the [`<MiniMap />`](/api-reference/components/minimap)
|
||||
@@ -45,16 +40,10 @@ const selector = (s: ReactFlowState) => (s.userSelectionActive ? 'none' : 'all')
|
||||
*/
|
||||
export const Panel = forwardRef<HTMLDivElement, PanelProps>(
|
||||
({ position = 'top-left', children, className, style, ...rest }, ref) => {
|
||||
const pointerEvents = useStore(selector);
|
||||
const positionClasses = `${position}`.split('-');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cc(['react-flow__panel', className, ...positionClasses])}
|
||||
style={{ ...style, pointerEvents }}
|
||||
ref={ref}
|
||||
{...rest}
|
||||
>
|
||||
<div className={cc(['react-flow__panel', className, ...positionClasses])} style={style} ref={ref} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ const reactFlowFieldsToTrack = [
|
||||
'onClickConnectStart',
|
||||
'onClickConnectEnd',
|
||||
'nodesDraggable',
|
||||
'autoPanOnNodeFocus',
|
||||
'nodesConnectable',
|
||||
'nodesFocusable',
|
||||
'edgesFocusable',
|
||||
@@ -64,6 +65,7 @@ const reactFlowFieldsToTrack = [
|
||||
'isValidConnection',
|
||||
'selectNodesOnDrag',
|
||||
'nodeDragThreshold',
|
||||
'connectionDragThreshold',
|
||||
'onBeforeDelete',
|
||||
'debug',
|
||||
'autoPanSpeed',
|
||||
@@ -154,13 +156,11 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
|
||||
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
|
||||
else if (fieldName === 'fitView') store.setState({ fitViewQueued: fieldValue as boolean });
|
||||
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOptions: fieldValue as FitViewOptions });
|
||||
|
||||
if (fieldName === 'ariaLabelConfig') {
|
||||
store.setState({ ariaLabelConfig: mergeAriaLabelConfig(fieldValue as AriaLabelConfig) });
|
||||
}
|
||||
// General case
|
||||
else store.setState({ [fieldName]: fieldValue });
|
||||
}
|
||||
|
||||
@@ -48,13 +48,13 @@ function NodeRendererComponent<NodeType extends Node>(props: NodeRendererProps<N
|
||||
* The split of responsibilities between NodeRenderer and
|
||||
* NodeComponentWrapper may appear weird. However, it’s designed to
|
||||
* minimize the cost of updates when individual nodes change.
|
||||
*
|
||||
*
|
||||
* For example, when you’re 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
|
||||
|
||||
@@ -77,6 +77,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
||||
onlyRenderVisibleElements = false,
|
||||
selectNodesOnDrag,
|
||||
nodesDraggable,
|
||||
autoPanOnNodeFocus,
|
||||
nodesConnectable,
|
||||
nodesFocusable,
|
||||
nodeOrigin = defaultNodeOrigin,
|
||||
@@ -138,6 +139,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
||||
style,
|
||||
id,
|
||||
nodeDragThreshold,
|
||||
connectionDragThreshold,
|
||||
viewport,
|
||||
onViewportChange,
|
||||
width,
|
||||
@@ -261,6 +263,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
||||
onClickConnectStart={onClickConnectStart}
|
||||
onClickConnectEnd={onClickConnectEnd}
|
||||
nodesDraggable={nodesDraggable}
|
||||
autoPanOnNodeFocus={autoPanOnNodeFocus}
|
||||
nodesConnectable={nodesConnectable}
|
||||
nodesFocusable={nodesFocusable}
|
||||
edgesFocusable={edgesFocusable}
|
||||
@@ -304,6 +307,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
||||
isValidConnection={isValidConnection}
|
||||
selectNodesOnDrag={selectNodesOnDrag}
|
||||
nodeDragThreshold={nodeDragThreshold}
|
||||
connectionDragThreshold={connectionDragThreshold}
|
||||
onBeforeDelete={onBeforeDelete}
|
||||
paneClickDistance={paneClickDistance}
|
||||
debug={debug}
|
||||
|
||||
@@ -222,7 +222,11 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
||||
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
|
||||
const partiallyVisible = partially && overlappingArea > 0;
|
||||
|
||||
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
|
||||
return (
|
||||
partiallyVisible ||
|
||||
overlappingArea >= currNodeRect.width * currNodeRect.height ||
|
||||
overlappingArea >= nodeRect.width * nodeRect.height
|
||||
);
|
||||
}) as NodeType[];
|
||||
},
|
||||
isNodeIntersecting: (nodeOrRect, area, partially = true) => {
|
||||
|
||||
@@ -63,25 +63,7 @@ const useViewportHelper = (): ViewportHelperFunctions => {
|
||||
return { x, y, zoom };
|
||||
},
|
||||
setCenter: async (x, y, options) => {
|
||||
const { width, height, maxZoom, panZoom } = store.getState();
|
||||
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom;
|
||||
const centerX = width / 2 - x * nextZoom;
|
||||
const centerY = height / 2 - y * nextZoom;
|
||||
|
||||
if (!panZoom) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
await panZoom.setViewport(
|
||||
{
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
zoom: nextZoom,
|
||||
},
|
||||
{ duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate }
|
||||
);
|
||||
|
||||
return Promise.resolve(true);
|
||||
return store.getState().setCenter(x, y, options);
|
||||
},
|
||||
fitBounds: async (bounds, options) => {
|
||||
const { width, height, minZoom, maxZoom, panZoom } = store.getState();
|
||||
|
||||
@@ -109,6 +109,9 @@ export {
|
||||
type NodeConnection,
|
||||
type OnReconnect,
|
||||
type AriaLabelConfig,
|
||||
type SetCenter,
|
||||
type SetViewport,
|
||||
type FitBounds,
|
||||
} from '@xyflow/system';
|
||||
|
||||
// we need this workaround to prevent a duplicate identifier error
|
||||
|
||||
@@ -360,6 +360,26 @@ const createStore = ({
|
||||
|
||||
return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
|
||||
},
|
||||
setCenter: async (x, y, options) => {
|
||||
const { width, height, maxZoom, panZoom } = get();
|
||||
|
||||
if (!panZoom) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom;
|
||||
|
||||
await panZoom.setViewport(
|
||||
{
|
||||
x: width / 2 - x * nextZoom,
|
||||
y: height / 2 - y * nextZoom,
|
||||
zoom: nextZoom,
|
||||
},
|
||||
{ duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate }
|
||||
);
|
||||
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
cancelConnection: () => {
|
||||
set({
|
||||
connection: { ...initialConnection },
|
||||
|
||||
@@ -107,6 +107,7 @@ const getInitialState = ({
|
||||
noPanClassName: 'nopan',
|
||||
nodeOrigin: storeNodeOrigin,
|
||||
nodeDragThreshold: 1,
|
||||
connectionDragThreshold: 1,
|
||||
|
||||
snapGrid: [15, 15],
|
||||
snapToGrid: false,
|
||||
@@ -134,7 +135,9 @@ const getInitialState = ({
|
||||
ariaLiveMessage: '',
|
||||
autoPanOnConnect: true,
|
||||
autoPanOnNodeDrag: true,
|
||||
autoPanOnNodeFocus: true,
|
||||
autoPanSpeed: 15,
|
||||
|
||||
connectionRadius: 20,
|
||||
onError: devWarn,
|
||||
isValidConnection: undefined,
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
SnapGrid,
|
||||
OnReconnect,
|
||||
AriaLabelConfig,
|
||||
FinalConnectionState,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type {
|
||||
@@ -138,7 +139,7 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
|
||||
* This event fires when the user releases the source or target of an editable edge. It is called
|
||||
* even if an edge update does not occur.
|
||||
*/
|
||||
onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void;
|
||||
onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType, connectionState: FinalConnectionState) => void;
|
||||
/**
|
||||
* Use this event handler to add interactivity to a controlled flow.
|
||||
* It is called on node drag, select, and move.
|
||||
@@ -383,6 +384,11 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
|
||||
* @default true
|
||||
*/
|
||||
nodesDraggable?: boolean;
|
||||
/**
|
||||
* When `true`, the viewport will pan when a node is focused.
|
||||
* @default true
|
||||
*/
|
||||
autoPanOnNodeFocus?: boolean;
|
||||
/**
|
||||
* Controls whether all nodes should be connectable or not. Individual nodes can override this
|
||||
* setting by setting their `connectable` prop.
|
||||
@@ -652,6 +658,12 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
|
||||
* @default 1
|
||||
*/
|
||||
nodeDragThreshold?: number;
|
||||
/**
|
||||
* The threshold in pixels that the mouse must move before a connection line starts to drag.
|
||||
* This is useful to prevent accidental connections when clicking on a handle.
|
||||
* @default 1
|
||||
*/
|
||||
connectionDragThreshold?: number;
|
||||
/** Sets a fixed width for the flow. */
|
||||
width?: number;
|
||||
/** Sets a fixed height for the flow. */
|
||||
|
||||
@@ -69,6 +69,10 @@ export type Edge<
|
||||
* @default "group"
|
||||
*/
|
||||
ariaRole?: AriaRole;
|
||||
/**
|
||||
* General escape hatch for adding custom attributes to the edge's DOM element.
|
||||
*/
|
||||
domAttributes?: Omit<SVGAttributes<SVGGElement>, 'id' | 'style' | 'className' | 'role' | 'aria-label'>;
|
||||
};
|
||||
|
||||
type SmoothStepEdge<EdgeData extends Record<string, unknown> = Record<string, unknown>> = Edge<
|
||||
@@ -180,12 +184,12 @@ export type BaseEdgeProps = Omit<SVGAttributes<SVGPathElement>, 'd' | 'path' | '
|
||||
path: string;
|
||||
/**
|
||||
* The id of the SVG marker to use at the start of the edge. This should be defined in a
|
||||
* `<defs>` element in a separate SVG document or element.
|
||||
* `<defs>` element in a separate SVG document or element. Use the format "url(#markerId)" where markerId is the id of your marker definition.
|
||||
*/
|
||||
markerStart?: string;
|
||||
/**
|
||||
* The id of the SVG marker to use at the end of the edge. This should be defined in a `<defs>`
|
||||
* element in a separate SVG document or element.
|
||||
* element in a separate SVG document or element. Use the format "url(#markerId)" where markerId is the id of your marker definition.
|
||||
*/
|
||||
markerEnd?: string;
|
||||
};
|
||||
|
||||
@@ -99,6 +99,9 @@ export type OnSelectionChangeFunc<NodeType extends Node = Node, EdgeType extends
|
||||
params: OnSelectionChangeParams<NodeType, EdgeType>
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type FitViewParams<NodeType extends Node = Node> = FitViewParamsBase<NodeType>;
|
||||
|
||||
/**
|
||||
@@ -107,9 +110,18 @@ export type FitViewParams<NodeType extends Node = Node> = FitViewParamsBase<Node
|
||||
* transform the viewport smoothly over a given amount of time.
|
||||
*
|
||||
* @public
|
||||
* @inline
|
||||
*/
|
||||
export type FitViewOptions<NodeType extends Node = Node> = FitViewOptionsBase<NodeType>;
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type FitView<NodeType extends Node = Node> = (fitViewOptions?: FitViewOptions<NodeType>) => Promise<boolean>;
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type OnInit<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (
|
||||
reactFlowInstance: ReactFlowInstance<NodeType, EdgeType>
|
||||
) => void;
|
||||
@@ -209,4 +221,8 @@ export type OnBeforeDelete<NodeType extends Node = Node, EdgeType extends Edge =
|
||||
EdgeType
|
||||
>;
|
||||
|
||||
/**
|
||||
* This type can be used to type the `isValidConnection` function.
|
||||
* If the function returns `true`, the connection is valid and can be created.
|
||||
*/
|
||||
export type IsValidConnection<EdgeType extends Edge = Edge> = (edge: EdgeType | Connection) => boolean;
|
||||
|
||||
@@ -105,7 +105,7 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
|
||||
* parameter can be set to `true` to include nodes that are only partially intersecting.
|
||||
*
|
||||
* @param node - the node or rect to check for intersections
|
||||
* @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed node or rect
|
||||
* @param partially - true by default, if set to false, only nodes that are fully intersecting will be returned
|
||||
* @param nodes - optional nodes array to check for intersections
|
||||
*
|
||||
* @returns an array of intersecting nodes
|
||||
@@ -252,10 +252,10 @@ export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edg
|
||||
NodeType,
|
||||
EdgeType
|
||||
> &
|
||||
ViewportHelperFunctions & {
|
||||
/**
|
||||
* React Flow needs to mount the viewport to the DOM and initialize its zoom and pan behavior.
|
||||
* This property tells you when viewport is initialized.
|
||||
*/
|
||||
ViewportHelperFunctions & {
|
||||
/**
|
||||
* React Flow needs to mount the viewport to the DOM and initialize its zoom and pan behavior.
|
||||
* This property tells you when viewport is initialized.
|
||||
*/
|
||||
viewportInitialized: boolean;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent, AriaRole } from 'react';
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent, AriaRole, HTMLAttributes, DOMAttributes } from 'react';
|
||||
import type { CoordinateExtent, NodeBase, OnError, NodeProps as NodePropsBase, InternalNodeBase } from '@xyflow/system';
|
||||
|
||||
import { NodeTypes } from './general';
|
||||
@@ -22,8 +22,15 @@ export type Node<
|
||||
* The ARIA role attribute for the node element, used for accessibility.
|
||||
* @default "group"
|
||||
*/
|
||||
|
||||
ariaRole?: AriaRole;
|
||||
|
||||
/**
|
||||
* General escape hatch for adding custom attributes to the node's DOM element.
|
||||
*/
|
||||
domAttributes?: Omit<
|
||||
HTMLAttributes<HTMLDivElement>,
|
||||
'id' | 'style' | 'className' | 'draggable' | 'role' | 'aria-label' | keyof DOMAttributes<HTMLDivElement>
|
||||
>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
type EdgeChange,
|
||||
type ParentLookup,
|
||||
type AriaLabelConfig,
|
||||
SetCenter,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type {
|
||||
@@ -75,6 +76,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
|
||||
nodeExtent: CoordinateExtent;
|
||||
nodeOrigin: NodeOrigin;
|
||||
nodeDragThreshold: number;
|
||||
connectionDragThreshold: number;
|
||||
|
||||
nodesSelectionActive: boolean;
|
||||
userSelectionActive: boolean;
|
||||
@@ -88,6 +90,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
|
||||
snapGrid: SnapGrid;
|
||||
|
||||
nodesDraggable: boolean;
|
||||
autoPanOnNodeFocus: boolean;
|
||||
nodesConnectable: boolean;
|
||||
nodesFocusable: boolean;
|
||||
edgesFocusable: boolean;
|
||||
@@ -171,6 +174,7 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
|
||||
triggerNodeChanges: (changes: NodeChange<NodeType>[]) => void;
|
||||
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
|
||||
panBy: PanBy;
|
||||
setCenter: SetCenter;
|
||||
setPaneClickDistance: (distance: number) => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,83 @@
|
||||
# @xyflow/svelte
|
||||
|
||||
## 1.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`26f2cdd7`](https://github.com/xyflow/xyflow/commit/26f2cdd720fc2c8fb337d3af13b82dab6a90fb60)]:
|
||||
- @xyflow/system@0.0.65
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#5361](https://github.com/xyflow/xyflow/pull/5361) [`90e9247a`](https://github.com/xyflow/xyflow/commit/90e9247adbdfa9d06db97e1d0d895e35c960551c) Thanks [@peterkogo](https://github.com/peterkogo)! - Render edges above nodes when they are within a subflow
|
||||
|
||||
- [#5344](https://github.com/xyflow/xyflow/pull/5344) [`2441bf8d`](https://github.com/xyflow/xyflow/commit/2441bf8d97a6b72494f216915d52d5acbeefefde) Thanks [@moklick](https://github.com/moklick)! - Add connectionDragThreshold prop
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5362](https://github.com/xyflow/xyflow/pull/5362) [`72dc1d60`](https://github.com/xyflow/xyflow/commit/72dc1d602110947e3db83c37b9a9125ee85cf4bc) Thanks [@moklick](https://github.com/moklick)! - Remove pointer events from Panel via CSS while a selection gets dragged
|
||||
|
||||
- Updated dependencies [[`72dc1d60`](https://github.com/xyflow/xyflow/commit/72dc1d602110947e3db83c37b9a9125ee85cf4bc), [`90e9247a`](https://github.com/xyflow/xyflow/commit/90e9247adbdfa9d06db97e1d0d895e35c960551c), [`2441bf8d`](https://github.com/xyflow/xyflow/commit/2441bf8d97a6b72494f216915d52d5acbeefefde)]:
|
||||
- @xyflow/system@0.0.64
|
||||
|
||||
## 1.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5339](https://github.com/xyflow/xyflow/pull/5339) [`56ebde81`](https://github.com/xyflow/xyflow/commit/56ebde811555775a0a9e2b2f88d35d9511f90c95) Thanks [@jrmajor](https://github.com/jrmajor)! - Fix `onmove`, `onmovestart` and `onmoveend` events not firing
|
||||
|
||||
- [#5354](https://github.com/xyflow/xyflow/pull/5354) [`c4312d89`](https://github.com/xyflow/xyflow/commit/c4312d8997ecdc7ef12cfa4efc1fde7131a2b950) Thanks [@moklick](https://github.com/moklick)! - Add TSDoc annotations
|
||||
|
||||
- [#5335](https://github.com/xyflow/xyflow/pull/5335) [`8474ba49`](https://github.com/xyflow/xyflow/commit/8474ba49cc1f48b6758a728fe4371f38260952e5) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent proxying objects in the store
|
||||
|
||||
- [#5336](https://github.com/xyflow/xyflow/pull/5336) [`d6db97c5`](https://github.com/xyflow/xyflow/commit/d6db97c53597db0eee8edd19beaf20b83f89fabf) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix `useNodeConnections` callbacks firing only when returned signal is used
|
||||
|
||||
- [#5333](https://github.com/xyflow/xyflow/pull/5333) [`3d7e8b6b`](https://github.com/xyflow/xyflow/commit/3d7e8b6bb10001ee84d79ca4f6a9fd0053c4a276) Thanks [@peterkogo](https://github.com/peterkogo)! - Add missing type exports
|
||||
|
||||
- Updated dependencies [[`c4312d89`](https://github.com/xyflow/xyflow/commit/c4312d8997ecdc7ef12cfa4efc1fde7131a2b950), [`3d7e8b6b`](https://github.com/xyflow/xyflow/commit/3d7e8b6bb10001ee84d79ca4f6a9fd0053c4a276), [`9c61000c`](https://github.com/xyflow/xyflow/commit/9c61000cac6277ce97274cc626fa7266f82dec27)]:
|
||||
- @xyflow/system@0.0.63
|
||||
|
||||
## 1.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#5299](https://github.com/xyflow/xyflow/pull/5299) [`848b486b`](https://github.com/xyflow/xyflow/commit/848b486b2201b650ecb3317f367a723edb2458e1) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaRole` prop to nodes and edges
|
||||
|
||||
- [#5280](https://github.com/xyflow/xyflow/pull/5280) [`dba6faf2`](https://github.com/xyflow/xyflow/commit/dba6faf20e7ec2524d5270d177331d3bd260f3ac) Thanks [@moklick](https://github.com/moklick)! - Improve typing for Nodes
|
||||
|
||||
- [#5276](https://github.com/xyflow/xyflow/pull/5276) [`6ce44a05`](https://github.com/xyflow/xyflow/commit/6ce44a05c829068ff5a8416ce3fa4ee6e0eced48) Thanks [@moklick](https://github.com/moklick)! - Add an `ease` and `interpolate` option to all function that alter the viewport
|
||||
|
||||
- [#5277](https://github.com/xyflow/xyflow/pull/5277) [`f59730ce`](https://github.com/xyflow/xyflow/commit/f59730ce3530a91f579f6bbd2ea9335680f552ef) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaLabelConfig` prop for customizing UI text like aria labels and descriptions.
|
||||
|
||||
- [#5317](https://github.com/xyflow/xyflow/pull/5317) [`09458f52`](https://github.com/xyflow/xyflow/commit/09458f52ff57356e03404c58e9bfdbfd50579850) Thanks [@moklick](https://github.com/moklick)! - Add `domAttributes` option for nodes and edges
|
||||
|
||||
- [#5326](https://github.com/xyflow/xyflow/pull/5326) [`050b511c`](https://github.com/xyflow/xyflow/commit/050b511cd6966ba526299f7ca11f9ca4791fd2cf) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent NodeResizer controls to become too small when zooming out
|
||||
|
||||
- [#5308](https://github.com/xyflow/xyflow/pull/5308) [`09fab679`](https://github.com/xyflow/xyflow/commit/09fab6794031410c9e9465281d038c3520afe783) Thanks [@printerscanner](https://github.com/printerscanner)! - Focus nodes on tab if not within the viewport and add a new prop `autoPanOnNodeFocus`
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5327](https://github.com/xyflow/xyflow/pull/5327) [`75ed6dec`](https://github.com/xyflow/xyflow/commit/75ed6decfb3ff408f6136bc1bc712fc4eb3737ef) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent selecting of edges when spacebar is pressed
|
||||
|
||||
- [#5294](https://github.com/xyflow/xyflow/pull/5294) [`4a582e23`](https://github.com/xyflow/xyflow/commit/4a582e2371066170b12f3df947f24fe233b542cd) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix data in `EdgeProps` that was not typed correctly
|
||||
|
||||
- [#5327](https://github.com/xyflow/xyflow/pull/5327) [`d0c36fdb`](https://github.com/xyflow/xyflow/commit/d0c36fdb708b784cbbd87711512b813eb68757ac) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix initial fitView for SSR
|
||||
|
||||
- [#5275](https://github.com/xyflow/xyflow/pull/5275) [`a67bfc09`](https://github.com/xyflow/xyflow/commit/a67bfc09d5b352b5ff3b896c9a9e7138f2c4b20c) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix setting nodesInitialized multiple times
|
||||
|
||||
- [#5271](https://github.com/xyflow/xyflow/pull/5271) [`5224a1a2`](https://github.com/xyflow/xyflow/commit/5224a1a252eee59ffc48fffd74872206e19207f3) Thanks [@leejuyuu](https://github.com/leejuyuu)! - Change a11y description inline styles to classes
|
||||
|
||||
- [#5327](https://github.com/xyflow/xyflow/pull/5327) [`d0c36fdb`](https://github.com/xyflow/xyflow/commit/d0c36fdb708b784cbbd87711512b813eb68757ac) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix `ViewportPortal` not working when used outside of `SvelteFlow` component
|
||||
|
||||
- [#5295](https://github.com/xyflow/xyflow/pull/5295) [`93aefe71`](https://github.com/xyflow/xyflow/commit/93aefe71fb199d0e3463aca38a4564252ab8cbf4) Thanks [@peterkogo](https://github.com/peterkogo)! - Export missing callback types
|
||||
|
||||
- [#5327](https://github.com/xyflow/xyflow/pull/5327) [`d0c36fdb`](https://github.com/xyflow/xyflow/commit/d0c36fdb708b784cbbd87711512b813eb68757ac) Thanks [@peterkogo](https://github.com/peterkogo)! - Display nodes correctly in SSR output
|
||||
|
||||
- Updated dependencies [[`848b486b`](https://github.com/xyflow/xyflow/commit/848b486b2201b650ecb3317f367a723edb2458e1), [`dba6faf2`](https://github.com/xyflow/xyflow/commit/dba6faf20e7ec2524d5270d177331d3bd260f3ac), [`6ce44a05`](https://github.com/xyflow/xyflow/commit/6ce44a05c829068ff5a8416ce3fa4ee6e0eced48), [`f59730ce`](https://github.com/xyflow/xyflow/commit/f59730ce3530a91f579f6bbd2ea9335680f552ef), [`09458f52`](https://github.com/xyflow/xyflow/commit/09458f52ff57356e03404c58e9bfdbfd50579850), [`050b511c`](https://github.com/xyflow/xyflow/commit/050b511cd6966ba526299f7ca11f9ca4791fd2cf)]:
|
||||
- @xyflow/system@0.0.62
|
||||
|
||||
## 1.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xyflow/svelte",
|
||||
"version": "1.0.2",
|
||||
"version": "1.2.1",
|
||||
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
|
||||
"keywords": [
|
||||
"svelte",
|
||||
@@ -55,35 +55,35 @@
|
||||
"@xyflow/system": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.24.0",
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/kit": "^2.20.7",
|
||||
"@eslint/js": "^9.28.0",
|
||||
"@sveltejs/adapter-auto": "^6.0.1",
|
||||
"@sveltejs/kit": "^2.21.4",
|
||||
"@sveltejs/package": "^2.3.11",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.30.1",
|
||||
"@typescript-eslint/parser": "^8.30.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.34.0",
|
||||
"@typescript-eslint/parser": "^8.34.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"cssnano": "^7.0.6",
|
||||
"cssnano": "^7.0.7",
|
||||
"dotenv": "^16.5.0",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-svelte": "^3.5.1",
|
||||
"globals": "^16.0.0",
|
||||
"postcss": "^8.5.3",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-svelte": "^3.9.2",
|
||||
"globals": "^16.2.0",
|
||||
"postcss": "^8.5.4",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"postcss-combine-duplicated-selectors": "^10.0.3",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-nested": "^7.0.2",
|
||||
"postcss-rename": "^0.6.1",
|
||||
"postcss-rename": "^0.8.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"svelte": "^5.27.0",
|
||||
"svelte-check": "^4.1.6",
|
||||
"svelte-eslint-parser": "^1.1.2",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"svelte": "^5.33.19",
|
||||
"svelte-check": "^4.2.1",
|
||||
"svelte-eslint-parser": "^1.2.0",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.30.1"
|
||||
"typescript-eslint": "^8.34.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.25.0"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { portal } from './portal.svelte';
|
||||
export { hideOnSSR } from './utils.svelte';
|
||||
|
||||
@@ -15,25 +15,32 @@ function tryToMount(node: Element, domNode: Element | null, target: Portal | und
|
||||
}
|
||||
|
||||
export function portal(node: Element, target: Portal | undefined) {
|
||||
// TODO: does this work if called outside of SvelteFlow
|
||||
const store = useStore();
|
||||
const { domNode } = $derived(useStore());
|
||||
|
||||
let previousTarget: Portal | undefined = target;
|
||||
|
||||
tryToMount(node, store.domNode, target);
|
||||
let destroyEffect: (() => void) | undefined;
|
||||
// svelte-ignore state_referenced_locally
|
||||
if (domNode) {
|
||||
// if the domNode is already mounted, we can directly try to mount the node
|
||||
tryToMount(node, domNode, target);
|
||||
} else {
|
||||
// if the domNode is not mounted yet, we need to wait for it to be ready
|
||||
destroyEffect = $effect.root(() => {
|
||||
$effect(() => {
|
||||
tryToMount(node, domNode, target);
|
||||
destroyEffect?.();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
async update(target: Portal) {
|
||||
if (target !== previousTarget) {
|
||||
node.parentNode?.removeChild(node);
|
||||
previousTarget = target;
|
||||
}
|
||||
tryToMount(node, store.domNode, target);
|
||||
async update(target: Portal | undefined) {
|
||||
tryToMount(node, domNode, target);
|
||||
},
|
||||
destroy() {
|
||||
if (node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
destroyEffect?.();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
17
packages/svelte/src/lib/actions/portal/utils.svelte.ts
Normal file
17
packages/svelte/src/lib/actions/portal/utils.svelte.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export function hideOnSSR(): { value: boolean } {
|
||||
let hide = $state(typeof window === 'undefined');
|
||||
if (hide) {
|
||||
const destroyEffect = $effect.root(() => {
|
||||
$effect(() => {
|
||||
hide = false;
|
||||
destroyEffect?.();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
get value() {
|
||||
return hide;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -44,6 +44,9 @@ export default function zoom(domNode: Element, params: ZoomParams) {
|
||||
minZoom,
|
||||
maxZoom,
|
||||
initialViewport,
|
||||
onPanZoomStart,
|
||||
onPanZoom,
|
||||
onPanZoomEnd,
|
||||
translateExtent,
|
||||
paneClickDistance,
|
||||
setPanZoomInstance,
|
||||
@@ -58,6 +61,9 @@ export default function zoom(domNode: Element, params: ZoomParams) {
|
||||
translateExtent,
|
||||
viewport: initialViewport,
|
||||
paneClickDistance,
|
||||
onPanZoom,
|
||||
onPanZoomStart,
|
||||
onPanZoomEnd,
|
||||
onDraggingChange
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { portal } from '$lib/actions/portal';
|
||||
import { hideOnSSR, portal } from '$lib/actions/portal';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import type { EdgeLabelProps } from './types';
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
<div
|
||||
use:portal={'edge-labels'}
|
||||
style:display={hideOnSSR().value ? 'none' : undefined}
|
||||
class={['svelte-flow__edge-label', { transparent }, className]}
|
||||
style:cursor={selectEdgeOnClick ? 'pointer' : undefined}
|
||||
style:transform="translate(-50%, -50%) translate({x}px,{y}px)"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { useStore } from '$lib/store';
|
||||
import type { Edge } from '$lib/types';
|
||||
import { XYHandle, type HandleType } from '@xyflow/system';
|
||||
import { XYHandle, type HandleType, type OnConnectStart } from '@xyflow/system';
|
||||
import { getContext } from 'svelte';
|
||||
import { EdgeLabel } from '../EdgeLabel';
|
||||
import type { EdgeReconnectAnchorProps } from './types';
|
||||
@@ -12,6 +12,7 @@
|
||||
position,
|
||||
class: className,
|
||||
size = 25,
|
||||
dragThreshold = 1,
|
||||
children,
|
||||
...rest
|
||||
}: EdgeReconnectAnchorProps = $props();
|
||||
@@ -52,8 +53,11 @@
|
||||
let newEdge: Edge | undefined;
|
||||
let edge = edgeLookup.get(edgeId)!;
|
||||
|
||||
reconnecting = true;
|
||||
onreconnectstart?.(event, edge, type);
|
||||
const _onConnectStart: OnConnectStart = (evt, params) => {
|
||||
reconnecting = true;
|
||||
onreconnectstart?.(event, edge, type);
|
||||
onconnectstart?.(evt, params);
|
||||
};
|
||||
|
||||
const opposite =
|
||||
type === 'target'
|
||||
@@ -79,7 +83,7 @@
|
||||
cancelConnection,
|
||||
panBy,
|
||||
isValidConnection,
|
||||
onConnectStart: onconnectstart,
|
||||
onConnectStart: _onConnectStart,
|
||||
onConnectEnd: onconnectend,
|
||||
onConnect: (connection) => {
|
||||
newEdge = { ...edge, ...connection };
|
||||
@@ -97,7 +101,8 @@
|
||||
},
|
||||
updateConnection,
|
||||
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
|
||||
getFromHandle: () => store.connection.fromHandle
|
||||
getFromHandle: () => store.connection.fromHandle,
|
||||
dragThreshold: dragThreshold ?? store.connectionDragThreshold
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -10,4 +10,5 @@ export type EdgeReconnectAnchorProps = {
|
||||
position?: XYPosition;
|
||||
size?: number;
|
||||
children?: Snippet;
|
||||
dragThreshold?: number;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
@@ -89,8 +89,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
onkeydown = (event: KeyboardEvent) => {
|
||||
// TODO: Possible Svelte Bug? onkeydown is always firing for the last edge
|
||||
function onkeydown(event: KeyboardEvent) {
|
||||
if (!store.disableKeyboardA11y && elementSelectionKeys.includes(event.key) && selectable) {
|
||||
const { unselectNodesAndEdges, addSelectedEdges } = store;
|
||||
const unselect = event.key === 'Escape';
|
||||
@@ -102,7 +101,7 @@
|
||||
addSelectedEdges([id]);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
@@ -138,9 +137,10 @@
|
||||
: `Edge from ${source} to ${target}`}
|
||||
aria-describedby={focusable ? `${ARIA_EDGE_DESC_KEY}-${store.flowId}` : undefined}
|
||||
role={edge.ariaRole ?? (focusable ? 'group' : 'img')}
|
||||
aria-roledescription={edge.ariaRoleDescription || 'edge'}
|
||||
aria-roledescription="edge"
|
||||
onkeydown={focusable ? onkeydown : undefined}
|
||||
tabindex={focusable ? 0 : undefined}
|
||||
{...edge.domAttributes}
|
||||
>
|
||||
<EdgeComponent
|
||||
{id}
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
let store = useStore();
|
||||
let ariaLabelConfig = $derived(store.ariaLabelConfig);
|
||||
|
||||
|
||||
let prevConnections: Map<string, HandleConnection> | null = null;
|
||||
$effect.pre(() => {
|
||||
if (onconnect || ondisconnect) {
|
||||
@@ -140,7 +139,8 @@
|
||||
store.onconnectend?.(event, connectionState);
|
||||
},
|
||||
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
|
||||
getFromHandle: () => store.connection.fromHandle
|
||||
getFromHandle: () => store.connection.fromHandle,
|
||||
dragThreshold: store.connectionDragThreshold
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
errorMessages,
|
||||
isInputDOMNode,
|
||||
nodeHasDimensions,
|
||||
Position
|
||||
Position,
|
||||
getNodesInside
|
||||
} from '@xyflow/system';
|
||||
|
||||
import drag from '$lib/actions/drag';
|
||||
@@ -65,7 +66,9 @@
|
||||
let draggable = $derived(_draggable ?? store.nodesDraggable);
|
||||
let selectable = $derived(_selectable ?? store.elementsSelectable);
|
||||
let connectable = $derived(_connectable ?? store.nodesConnectable);
|
||||
let initialized = $derived(nodeHasDimensions(node) && !!node.internals.handleBounds);
|
||||
let hasDimensions = $derived(nodeHasDimensions(node));
|
||||
let hasHandleBounds = $derived(!!node.internals.handleBounds);
|
||||
let isInitialized = $derived(hasDimensions && hasHandleBounds);
|
||||
let focusable = $derived(_focusable ?? store.nodesFocusable);
|
||||
|
||||
function isInParentLookup(id: string) {
|
||||
@@ -149,7 +152,7 @@
|
||||
|
||||
$effect(() => {
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||
if (resizeObserver && (!initialized || nodeRef !== prevNodeRef)) {
|
||||
if (resizeObserver && (!isInitialized || nodeRef !== prevNodeRef)) {
|
||||
prevNodeRef && resizeObserver.unobserve(prevNodeRef);
|
||||
nodeRef && resizeObserver.observe(nodeRef);
|
||||
prevNodeRef = nodeRef;
|
||||
@@ -197,6 +200,34 @@
|
||||
store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
const onFocus = () => {
|
||||
if (
|
||||
store.disableKeyboardA11y ||
|
||||
!store.autoPanOnNodeFocus ||
|
||||
!nodeRef?.matches(':focus-visible')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { width, height, viewport } = store;
|
||||
|
||||
const withinViewport =
|
||||
getNodesInside(
|
||||
new Map([[id, node]]),
|
||||
{ x: 0, y: 0, width, height },
|
||||
[viewport.x, viewport.y, viewport.zoom],
|
||||
true
|
||||
).length > 0;
|
||||
|
||||
if (!withinViewport) {
|
||||
store.setCenter(
|
||||
node.position.x + (node.measured.width ?? 0) / 2,
|
||||
node.position.y + (node.measured.height ?? 0) / 2,
|
||||
{ zoom: viewport.zoom }
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if !hidden}
|
||||
@@ -236,7 +267,7 @@
|
||||
class:parent={isParent}
|
||||
style:z-index={zIndex}
|
||||
style:transform="translate({positionX}px, {positionY}px)"
|
||||
style:visibility={initialized ? 'visible' : 'hidden'}
|
||||
style:visibility={hasDimensions ? 'visible' : 'hidden'}
|
||||
style={nodeStyle}
|
||||
onclick={onSelectNodeHandler}
|
||||
onpointerenter={onnodepointerenter
|
||||
@@ -252,12 +283,14 @@
|
||||
? (event) => onnodecontextmenu({ node: userNode, event })
|
||||
: undefined}
|
||||
onkeydown={focusable ? onKeyDown : undefined}
|
||||
onfocus={focusable ? onFocus : undefined}
|
||||
tabIndex={focusable ? 0 : undefined}
|
||||
role={node.ariaRole ?? (focusable ? 'group' : undefined)}
|
||||
aria-roledescription={node.ariaRoleDescription || 'node'}
|
||||
aria-roledescription="node"
|
||||
aria-describedby={store.disableKeyboardA11y
|
||||
? undefined
|
||||
: `${ARIA_NODE_DESC_KEY}-${store.flowId}`}
|
||||
{...node.domAttributes}
|
||||
>
|
||||
<NodeComponent
|
||||
{data}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { portal } from '$lib/actions/portal';
|
||||
import { hideOnSSR, portal } from '$lib/actions/portal';
|
||||
import type { ViewportPortalProps } from './types';
|
||||
|
||||
let { target = 'front', children, ...rest }: ViewportPortalProps = $props();
|
||||
</script>
|
||||
|
||||
<div use:portal={`viewport-${target}`} {...rest}>
|
||||
<div
|
||||
use:portal={`viewport-${target}`}
|
||||
style:display={hideOnSSR().value ? 'none' : undefined}
|
||||
{...rest}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type { PanelProps } from './types';
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
let { position = 'top-right', style, class: className, children, ...rest }: PanelProps = $props();
|
||||
|
||||
let store = $derived(useStore());
|
||||
|
||||
let positionClasses = $derived(`${position}`.split('-'));
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={['svelte-flow__panel', className, ...positionClasses]}
|
||||
{style}
|
||||
style:pointer-events={store.selectionRectMode ? 'none' : ''}
|
||||
{...rest}
|
||||
>
|
||||
<div class={['svelte-flow__panel', className, ...positionClasses]} {style} {...rest}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
fitViewOptions,
|
||||
nodeOrigin,
|
||||
nodeDragThreshold,
|
||||
connectionDragThreshold,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
initialViewport,
|
||||
@@ -84,6 +85,7 @@
|
||||
elevateNodesOnSelect,
|
||||
elevateEdgesOnSelect,
|
||||
nodesDraggable,
|
||||
autoPanOnNodeFocus,
|
||||
nodesConnectable,
|
||||
elementsSelectable,
|
||||
nodesFocusable,
|
||||
@@ -100,6 +102,16 @@
|
||||
type OnlyDivAttributes<T> = {
|
||||
[K in keyof T]: K extends keyof HTMLAttributes<HTMLDivElement> ? T[K] : never;
|
||||
};
|
||||
|
||||
// Undo scroll events, preventing viewport from shifting when nodes outside of it are focused
|
||||
function wrapperOnScroll(e: UIEvent & { currentTarget: EventTarget & HTMLDivElement }) {
|
||||
e.currentTarget.scrollTo({ top: 0, left: 0, behavior: 'auto' });
|
||||
|
||||
// Forward the event to any existing onscroll handler if needed
|
||||
if (rest.onscroll) {
|
||||
rest.onscroll(e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -111,6 +123,7 @@
|
||||
class={['svelte-flow', 'svelte-flow__container', className, colorMode]}
|
||||
data-testid="svelte-flow__wrapper"
|
||||
role="application"
|
||||
onscroll={wrapperOnScroll}
|
||||
{...divAttributes satisfies OnlyDivAttributes<typeof divAttributes>}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
@@ -170,6 +170,12 @@ export type SvelteFlowProps<
|
||||
* @default 0
|
||||
*/
|
||||
nodeClickDistance?: number;
|
||||
/**
|
||||
* The threshold in pixels that the mouse must move before a connection line starts to drag.
|
||||
* This is useful to prevent accidental connections when clicking on a handle.
|
||||
* @default 1
|
||||
*/
|
||||
connectionDragThreshold?: number;
|
||||
/** Minimum zoom level
|
||||
* @default 0.5
|
||||
*/
|
||||
@@ -232,6 +238,11 @@ export type SvelteFlowProps<
|
||||
* @default true
|
||||
*/
|
||||
nodesDraggable?: boolean;
|
||||
/**
|
||||
* When `true`, the viewport will pan when a node is focused.
|
||||
* @default true
|
||||
*/
|
||||
autoPanOnNodeFocus?: boolean;
|
||||
/**
|
||||
* Controls if all nodes should be connectable to each other
|
||||
* @default true
|
||||
|
||||
@@ -17,6 +17,8 @@ type UseNodeConnectionsParams = {
|
||||
onDisconnect?: (connections: Connection[]) => void;
|
||||
};
|
||||
|
||||
type ConnectionMap = Map<string, NodeConnection>;
|
||||
|
||||
const initialConnections: NodeConnection[] = [];
|
||||
|
||||
/**
|
||||
@@ -42,26 +44,46 @@ export function useNodeConnections({
|
||||
const contextNodeId = getContext<string>('svelteflow__node_id');
|
||||
const nodeId = id ?? contextNodeId;
|
||||
|
||||
let prevConnections: Map<string, NodeConnection> = new Map();
|
||||
let connectionMaps: { previous: ConnectionMap; next: ConnectionMap } = {
|
||||
previous: new Map(),
|
||||
next: new Map()
|
||||
};
|
||||
let connectionsArray: NodeConnection[] = initialConnections;
|
||||
|
||||
const connections = $derived.by(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
edges;
|
||||
|
||||
const prevConnections = connectionMaps.next;
|
||||
const nextConnections =
|
||||
connectionLookup.get(
|
||||
`${nodeId}${handleType ? (handleId ? `-${handleType}-${handleId}` : `-${handleType}`) : ''}`
|
||||
) ?? new Map();
|
||||
if (!areConnectionMapsEqual(nextConnections, prevConnections)) {
|
||||
if (onConnect) handleConnectionChange(nextConnections, prevConnections, onConnect);
|
||||
if (onDisconnect) handleConnectionChange(prevConnections, nextConnections, onDisconnect);
|
||||
|
||||
prevConnections = nextConnections;
|
||||
connectionMaps = {
|
||||
previous: prevConnections,
|
||||
next: nextConnections
|
||||
};
|
||||
connectionsArray = Array.from(nextConnections.values() || initialConnections);
|
||||
}
|
||||
return connectionsArray;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
// We subscribe to changes to the connections only when onConnect/onDisconnect are provided
|
||||
if (onConnect) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
connections;
|
||||
handleConnectionChange(connectionMaps.next, connectionMaps.previous, onConnect);
|
||||
}
|
||||
|
||||
if (onDisconnect) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
connections;
|
||||
handleConnectionChange(connectionMaps.previous, connectionMaps.next, onDisconnect);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
get current() {
|
||||
return connections;
|
||||
|
||||
@@ -125,7 +125,7 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
|
||||
* Returns all nodes that intersect with the given node or rect.
|
||||
*
|
||||
* @param node - the node or rect to check for intersections
|
||||
* @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed node or rect
|
||||
* @param partially - true by default, if set to false, only nodes that are fully intersecting will be returned
|
||||
* @param nodes - optional nodes array to check for intersections
|
||||
*
|
||||
* @returns an array of intersecting nodes
|
||||
@@ -366,28 +366,8 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
getViewport: () => $state.snapshot(store.viewport),
|
||||
setCenter: async (x, y, options) => {
|
||||
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : store.maxZoom;
|
||||
const currentPanZoom = store.panZoom;
|
||||
|
||||
if (!currentPanZoom) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
await currentPanZoom.setViewport(
|
||||
{
|
||||
x: store.width / 2 - x * nextZoom,
|
||||
y: store.height / 2 - y * nextZoom,
|
||||
zoom: nextZoom
|
||||
},
|
||||
{ duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate }
|
||||
);
|
||||
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
fitView: (options?: FitViewOptions) => {
|
||||
return store.fitView(options);
|
||||
},
|
||||
setCenter: async (x, y, options) => store.setCenter(x, y, options),
|
||||
fitView: (options?: FitViewOptions) => store.fitView(options),
|
||||
fitBounds: async (bounds: Rect, options?: FitBoundsOptions) => {
|
||||
if (!store.panZoom) {
|
||||
return Promise.resolve(false);
|
||||
@@ -432,7 +412,11 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
|
||||
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
|
||||
const partiallyVisible = partially && overlappingArea > 0;
|
||||
|
||||
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
|
||||
return (
|
||||
partiallyVisible ||
|
||||
overlappingArea >= currNodeRect.width * currNodeRect.height ||
|
||||
overlappingArea >= nodeRect.width * nodeRect.height
|
||||
);
|
||||
});
|
||||
},
|
||||
isNodeIntersecting: (
|
||||
|
||||
@@ -105,6 +105,9 @@ export {
|
||||
type OnResizeStart,
|
||||
type OnResize,
|
||||
type OnResizeEnd,
|
||||
type OnReconnect,
|
||||
type OnReconnectStart,
|
||||
type OnReconnectEnd,
|
||||
type ControlPosition,
|
||||
type ControlLinePosition,
|
||||
ResizeControlVariant,
|
||||
@@ -113,7 +116,10 @@ export {
|
||||
type ResizeDragEvent,
|
||||
type IsValidConnection,
|
||||
type NodeConnection,
|
||||
type AriaLabelConfig
|
||||
type AriaLabelConfig,
|
||||
type SetCenter,
|
||||
type SetViewport,
|
||||
type FitBounds
|
||||
} from '@xyflow/system';
|
||||
|
||||
// system utils
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
buttonColor,
|
||||
buttonColorHover,
|
||||
buttonBorderColor,
|
||||
'aria-label': ariaLabel,
|
||||
fitViewOptions,
|
||||
children,
|
||||
before,
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
handleStyle,
|
||||
lineClass,
|
||||
lineStyle,
|
||||
autoScale = true,
|
||||
...rest
|
||||
}: NodeResizerProps = $props();
|
||||
</script>
|
||||
@@ -25,11 +26,19 @@
|
||||
style={lineStyle}
|
||||
{nodeId}
|
||||
{position}
|
||||
{autoScale}
|
||||
variant={ResizeControlVariant.Line}
|
||||
{...rest}
|
||||
/>
|
||||
{/each}
|
||||
{#each XY_RESIZER_HANDLE_POSITIONS as position (position)}
|
||||
<ResizeControl class={handleClass} style={handleStyle} {nodeId} {position} {...rest} />
|
||||
<ResizeControl
|
||||
class={handleClass}
|
||||
style={handleStyle}
|
||||
{nodeId}
|
||||
{position}
|
||||
{autoScale}
|
||||
{...rest}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
maxWidth = Number.MAX_VALUE,
|
||||
maxHeight = Number.MAX_VALUE,
|
||||
keepAspectRatio = false,
|
||||
autoScale = true,
|
||||
shouldResize,
|
||||
onResizeStart,
|
||||
onResize,
|
||||
@@ -40,10 +41,10 @@
|
||||
let resizeControlRef: HTMLDivElement;
|
||||
let resizer: XYResizerInstance | null = $state(null);
|
||||
|
||||
let isLineVariant = $derived(variant === ResizeControlVariant.Line);
|
||||
|
||||
let controlPosition = $derived.by(() => {
|
||||
let defaultPosition = (
|
||||
variant === ResizeControlVariant.Line ? 'right' : 'bottom-right'
|
||||
) as ControlPosition;
|
||||
let defaultPosition = (isLineVariant ? 'right' : 'bottom-right') as ControlPosition;
|
||||
return position ?? defaultPosition;
|
||||
});
|
||||
|
||||
@@ -119,8 +120,9 @@
|
||||
<div
|
||||
class={['svelte-flow__resize-control', store.noDragClass, ...positionClasses, variant, className]}
|
||||
bind:this={resizeControlRef}
|
||||
style:border-color={variant === ResizeControlVariant.Line ? color : undefined}
|
||||
style:background-color={variant === ResizeControlVariant.Line ? undefined : color}
|
||||
style:border-color={isLineVariant ? color : undefined}
|
||||
style:background-color={isLineVariant ? undefined : color}
|
||||
style:scale={isLineVariant || !autoScale ? undefined : Math.max(1 / store.viewport.zoom, 1)}
|
||||
{...rest}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
@@ -36,6 +36,8 @@ export type NodeResizerProps = {
|
||||
maxHeight?: number;
|
||||
/** Keep aspect ratio when resizing */
|
||||
keepAspectRatio?: boolean;
|
||||
/** Automatically scale the node when resizing */
|
||||
autoScale?: boolean;
|
||||
/** Callback to determine if node should resize */
|
||||
shouldResize?: ShouldResize;
|
||||
/** Callback called when resizing starts */
|
||||
@@ -55,6 +57,7 @@ export type ResizeControlProps = Pick<
|
||||
| 'maxWidth'
|
||||
| 'maxHeight'
|
||||
| 'keepAspectRatio'
|
||||
| 'autoScale'
|
||||
| 'shouldResize'
|
||||
| 'onResizeStart'
|
||||
| 'onResize'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { getContext } from 'svelte';
|
||||
import { Position, getNodeToolbarTransform } from '@xyflow/system';
|
||||
|
||||
import { portal } from '$lib/actions/portal';
|
||||
import { hideOnSSR, portal } from '$lib/actions/portal';
|
||||
import { useStore } from '$lib/store';
|
||||
import { useSvelteFlow } from '$lib/hooks/useSvelteFlow.svelte';
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
{#if store.domNode && isActive && toolbarNodes}
|
||||
<div
|
||||
use:portal={'root'}
|
||||
style:display={hideOnSSR().value ? 'none' : undefined}
|
||||
class="svelte-flow__node-toolbar"
|
||||
data-id={toolbarNodes.reduce((acc, node) => `${acc}${node.id} `, '').trim()}
|
||||
style:position="absolute"
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
type ConnectionState,
|
||||
updateAbsolutePositions,
|
||||
snapPosition,
|
||||
calculateNodePosition
|
||||
calculateNodePosition,
|
||||
type SetCenterOptions
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
|
||||
@@ -126,6 +127,26 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
|
||||
return fitViewResolver.promise;
|
||||
}
|
||||
|
||||
async function setCenter(x: number, y: number, options?: SetCenterOptions) {
|
||||
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : store.maxZoom;
|
||||
const currentPanZoom = store.panZoom;
|
||||
|
||||
if (!currentPanZoom) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
await currentPanZoom.setViewport(
|
||||
{
|
||||
x: store.width / 2 - x * nextZoom,
|
||||
y: store.height / 2 - y * nextZoom,
|
||||
zoom: nextZoom
|
||||
},
|
||||
{ duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate }
|
||||
);
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) {
|
||||
const panZoom = store.panZoom;
|
||||
if (!panZoom) {
|
||||
@@ -372,6 +393,7 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
fitView,
|
||||
setCenter,
|
||||
setMinZoom,
|
||||
setMaxZoom,
|
||||
setTranslateExtent,
|
||||
|
||||
@@ -84,6 +84,25 @@ export const initialEdgeTypes = {
|
||||
step: StepEdgeInternal
|
||||
};
|
||||
|
||||
function getInitialViewport(
|
||||
// This is just used to make sure adoptUserNodes is called before we calculate the viewport
|
||||
_nodesInitialized: boolean,
|
||||
fitView: boolean | undefined,
|
||||
initialViewport: Viewport | undefined,
|
||||
width: number,
|
||||
height: number,
|
||||
nodeLookup: NodeLookup
|
||||
) {
|
||||
if (fitView && !initialViewport && width && height) {
|
||||
const bounds = getInternalNodesBounds(nodeLookup, {
|
||||
filter: (node) => !!((node.width || node.initialWidth) && (node.height || node.initialHeight))
|
||||
});
|
||||
return getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
|
||||
} else {
|
||||
return initialViewport ?? { x: 0, y: 0, zoom: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
export function getInitialStore<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
||||
signals: StoreSignals<NodeType, EdgeType>
|
||||
) {
|
||||
@@ -91,10 +110,10 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
||||
// Inline classes have some performance implications but we just call it once (max twice).
|
||||
class SvelteFlowStore {
|
||||
flowId: string = $derived(signals.props.id ?? '1');
|
||||
domNode = $state<HTMLDivElement | null>(null);
|
||||
panZoom: PanZoomInstance | null = $state(null);
|
||||
width = $state<number>(signals.width ?? 0);
|
||||
height = $state<number>(signals.height ?? 0);
|
||||
domNode = $state.raw<HTMLDivElement | null>(null);
|
||||
panZoom: PanZoomInstance | null = $state.raw(null);
|
||||
width = $state.raw<number>(signals.width ?? 0);
|
||||
height = $state.raw<number>(signals.height ?? 0);
|
||||
|
||||
nodesInitialized: boolean = $derived.by(() => {
|
||||
const nodesInitialized = adoptUserNodes(signals.nodes, this.nodeLookup, this.parentLookup, {
|
||||
@@ -266,6 +285,8 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
||||
nodeDragThreshold: number = $derived(signals.props.nodeDragThreshold ?? 1);
|
||||
autoPanOnNodeDrag: boolean = $derived(signals.props.autoPanOnNodeDrag ?? true);
|
||||
autoPanOnConnect: boolean = $derived(signals.props.autoPanOnConnect ?? true);
|
||||
autoPanOnNodeFocus: boolean = $derived(signals.props.autoPanOnNodeFocus ?? true);
|
||||
connectionDragThreshold: number = $derived(signals.props.connectionDragThreshold ?? 1);
|
||||
|
||||
fitViewQueued: boolean = signals.props.fitView ?? false;
|
||||
fitViewOptions: FitViewOptions | undefined = signals.props.fitViewOptions;
|
||||
@@ -273,16 +294,16 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
||||
|
||||
snapGrid: SnapGrid | null = $derived(signals.props.snapGrid ?? null);
|
||||
|
||||
dragging: boolean = $state(false);
|
||||
selectionRect: SelectionRect | null = $state(null);
|
||||
dragging: boolean = $state.raw(false);
|
||||
selectionRect: SelectionRect | null = $state.raw(null);
|
||||
|
||||
selectionKeyPressed: boolean = $state(false);
|
||||
multiselectionKeyPressed: boolean = $state(false);
|
||||
deleteKeyPressed: boolean = $state(false);
|
||||
panActivationKeyPressed: boolean = $state(false);
|
||||
zoomActivationKeyPressed: boolean = $state(false);
|
||||
selectionRectMode: string | null = $state(null);
|
||||
ariaLiveMessage = $state<string>('');
|
||||
selectionKeyPressed: boolean = $state.raw(false);
|
||||
multiselectionKeyPressed: boolean = $state.raw(false);
|
||||
deleteKeyPressed: boolean = $state.raw(false);
|
||||
panActivationKeyPressed: boolean = $state.raw(false);
|
||||
zoomActivationKeyPressed: boolean = $state.raw(false);
|
||||
selectionRectMode: string | null = $state.raw(null);
|
||||
ariaLiveMessage = $state.raw<string>('');
|
||||
selectionMode: SelectionMode = $derived(signals.props.selectionMode ?? SelectionMode.Partial);
|
||||
|
||||
nodeTypes: NodeTypes = $derived({ ...initialNodeTypes, ...signals.props.nodeTypes });
|
||||
@@ -297,7 +318,16 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
||||
|
||||
// _viewport is the internal viewport.
|
||||
// when binding to viewport, we operate on signals.viewport instead
|
||||
_viewport: Viewport = $state(signals.props.initialViewport ?? { x: 0, y: 0, zoom: 1 });
|
||||
_viewport: Viewport = $state.raw(
|
||||
getInitialViewport(
|
||||
this.nodesInitialized,
|
||||
signals.props.fitView,
|
||||
signals.props.initialViewport,
|
||||
this.width,
|
||||
this.height,
|
||||
this.nodeLookup
|
||||
)
|
||||
);
|
||||
get viewport() {
|
||||
return signals.viewport ?? this._viewport;
|
||||
}
|
||||
@@ -309,21 +339,21 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
||||
}
|
||||
|
||||
// _connection is viewport independent and originating from XYHandle
|
||||
_connection: ConnectionState = $state(initialConnection);
|
||||
_connection: ConnectionState = $state.raw(initialConnection);
|
||||
// We derive a viewport dependent connection here
|
||||
connection: ConnectionState = $derived.by(() => {
|
||||
if (this._connection.inProgress) {
|
||||
return {
|
||||
...this._connection,
|
||||
to: pointToRendererPoint(this._connection.to, [
|
||||
this.viewport.x,
|
||||
this.viewport.y,
|
||||
this.viewport.zoom
|
||||
])
|
||||
};
|
||||
} else {
|
||||
if (!this._connection.inProgress) {
|
||||
return this._connection;
|
||||
}
|
||||
|
||||
return {
|
||||
...this._connection,
|
||||
to: pointToRendererPoint(this._connection.to, [
|
||||
this.viewport.x,
|
||||
this.viewport.y,
|
||||
this.viewport.zoom
|
||||
])
|
||||
};
|
||||
});
|
||||
connectionMode: ConnectionMode = $derived(
|
||||
signals.props.connectionMode ?? ConnectionMode.Strict
|
||||
@@ -363,7 +393,7 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
||||
clickConnect?: boolean = $derived(signals.props.clickConnect ?? true);
|
||||
onclickconnectstart?: OnConnectStart = $derived(signals.props.onclickconnectstart);
|
||||
onclickconnectend?: OnConnectEnd = $derived(signals.props.onclickconnectend);
|
||||
clickConnectStartHandle: Pick<Handle, 'id' | 'nodeId' | 'type'> | null = $state(null);
|
||||
clickConnectStartHandle: Pick<Handle, 'id' | 'nodeId' | 'type'> | null = $state.raw(null);
|
||||
|
||||
onselectiondrag?: OnSelectionDrag<NodeType> = $derived(signals.props.onselectiondrag);
|
||||
onselectiondragstart?: OnSelectionDrag<NodeType> = $derived(signals.props.onselectiondragstart);
|
||||
@@ -409,15 +439,6 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
||||
);
|
||||
|
||||
constructor() {
|
||||
// Process intial fitView here
|
||||
if (signals.props.fitView && !signals.props.initialViewport && this.width && this.height) {
|
||||
const bounds = getInternalNodesBounds(this.nodeLookup, {
|
||||
filter: (node) =>
|
||||
!!((node.width || node.initialWidth) && (node.height || node.initialHeight))
|
||||
});
|
||||
this.viewport = getViewportForBounds(bounds, this.width, this.height, 0.5, 2, 0.1);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
warnIfDeeplyReactive(signals.nodes, 'nodes');
|
||||
warnIfDeeplyReactive(signals.edges, 'edges');
|
||||
|
||||
@@ -6,7 +6,8 @@ import type {
|
||||
UpdateNodePositions,
|
||||
CoordinateExtent,
|
||||
UpdateConnection,
|
||||
Viewport
|
||||
Viewport,
|
||||
SetCenter
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type { getInitialStore } from './initial-store.svelte';
|
||||
@@ -24,6 +25,7 @@ export type SvelteFlowStoreActions<NodeType extends Node = Node, EdgeType extend
|
||||
setTranslateExtent: (extent: CoordinateExtent) => void;
|
||||
setPaneClickDistance: (distance: number) => void;
|
||||
fitView: (options?: FitViewOptions) => Promise<boolean>;
|
||||
setCenter: SetCenter;
|
||||
updateNodePositions: UpdateNodePositions;
|
||||
updateNodeInternals: (updates: Map<string, InternalNodeUpdate>) => void;
|
||||
unselectNodesAndEdges: (params?: { nodes?: NodeType[]; edges?: EdgeType[] }) => void;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Component } from 'svelte';
|
||||
import type { ClassValue, HTMLAttributes, SVGAttributes } from 'svelte/elements';
|
||||
import type {
|
||||
EdgeBase,
|
||||
BezierPathOptions,
|
||||
@@ -9,7 +10,6 @@ import type {
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type { Node } from '$lib/types';
|
||||
import type { ClassValue, HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
/**
|
||||
* An `Edge` is the complete description with everything Svelte Flow needs to know in order to
|
||||
@@ -30,6 +30,13 @@ export type Edge<
|
||||
* @default "group"
|
||||
*/
|
||||
ariaRole?: HTMLAttributes<HTMLElement>['role'];
|
||||
/**
|
||||
* General escape hatch for adding custom attributes to the edge's DOM element.
|
||||
*/
|
||||
domAttributes?: Omit<
|
||||
SVGAttributes<SVGGElement>,
|
||||
'id' | 'style' | 'class' | 'role' | 'aria-label'
|
||||
>;
|
||||
};
|
||||
|
||||
export type BaseEdgeProps = Pick<
|
||||
@@ -43,11 +50,15 @@ export type BaseEdgeProps = Pick<
|
||||
labelX?: number;
|
||||
/** The y coordinate of the label */
|
||||
labelY?: number;
|
||||
/** Marker at start of edge
|
||||
/**
|
||||
* The id of the SVG marker to use at the start of the edge. This should be defined in a
|
||||
* `<defs>` element in a separate SVG document or element. Use the format "url(#markerId)" where markerId is the id of your marker definition.
|
||||
* @example 'url(#arrow)'
|
||||
*/
|
||||
markerStart?: string;
|
||||
/** Marker at end of edge
|
||||
/**
|
||||
* The id of the SVG marker to use at the end of the edge. This should be defined in a `<defs>`
|
||||
* element in a separate SVG document or element. Use the format "url(#markerId)" where markerId is the id of your marker definition.
|
||||
* @example 'url(#arrow)'
|
||||
*/
|
||||
markerEnd?: string;
|
||||
|
||||
@@ -21,8 +21,16 @@ export type ConnectionData = {
|
||||
connectionStatus: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type FitViewOptions<NodeType extends Node = Node> = FitViewOptionsBase<NodeType>;
|
||||
|
||||
/**
|
||||
* This type can be used to type the `onDelete` function with a custom node and edge type.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type OnDelete<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (params: {
|
||||
nodes: NodeType[];
|
||||
edges: EdgeType[];
|
||||
@@ -40,6 +48,10 @@ export type OnBeforeDelete<
|
||||
EdgeType extends Edge = Edge
|
||||
> = OnBeforeDeleteBase<NodeType, EdgeType>;
|
||||
|
||||
/**
|
||||
* This type can be used to type the `isValidConnection` function.
|
||||
* If the function returns `true`, the connection is valid and can be created.
|
||||
*/
|
||||
export type IsValidConnection<EdgeType extends Edge = Edge> = (
|
||||
edge: EdgeType | Connection
|
||||
) => boolean;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Component } from 'svelte';
|
||||
import type { ClassValue, HTMLAttributes } from 'svelte/elements';
|
||||
import type { ClassValue, HTMLAttributes, DOMAttributes } from 'svelte/elements';
|
||||
import type { InternalNodeBase, NodeBase, NodeProps as NodePropsBase } from '@xyflow/system';
|
||||
|
||||
/**
|
||||
@@ -25,7 +25,21 @@ export type Node<
|
||||
* The ARIA role attribute for the node element, used for accessibility.
|
||||
* @default "group"
|
||||
*/
|
||||
ariaRole?: HTMLAttributes<HTMLElement>['role'];
|
||||
ariaRole?: HTMLAttributes<HTMLDivElement>['role'];
|
||||
|
||||
/**
|
||||
* General escape hatch for adding custom attributes to the node's DOM element.
|
||||
*/
|
||||
domAttributes?: Omit<
|
||||
HTMLAttributes<HTMLDivElement>,
|
||||
| 'id'
|
||||
| 'style'
|
||||
| 'class'
|
||||
| 'draggable'
|
||||
| 'role'
|
||||
| 'aria-label'
|
||||
| keyof DOMAttributes<HTMLDivElement>
|
||||
>;
|
||||
};
|
||||
|
||||
// @todo: currently generics for nodes are not really supported
|
||||
|
||||
@@ -1,5 +1,47 @@
|
||||
# @xyflow/system
|
||||
|
||||
## 0.0.65
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5370](https://github.com/xyflow/xyflow/pull/5370) [`26f2cdd7`](https://github.com/xyflow/xyflow/commit/26f2cdd720fc2c8fb337d3af13b82dab6a90fb60) Thanks [@moklick](https://github.com/moklick)! - Only fire connection end events if connection was started
|
||||
|
||||
## 0.0.64
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5362](https://github.com/xyflow/xyflow/pull/5362) [`72dc1d60`](https://github.com/xyflow/xyflow/commit/72dc1d602110947e3db83c37b9a9125ee85cf4bc) Thanks [@moklick](https://github.com/moklick)! - Remove pointer events from Panel via CSS while a selection gets dragged
|
||||
|
||||
- [#5361](https://github.com/xyflow/xyflow/pull/5361) [`90e9247a`](https://github.com/xyflow/xyflow/commit/90e9247adbdfa9d06db97e1d0d895e35c960551c) Thanks [@peterkogo](https://github.com/peterkogo)! - Render edges above nodes when they are within a subflow
|
||||
|
||||
- [#5344](https://github.com/xyflow/xyflow/pull/5344) [`2441bf8d`](https://github.com/xyflow/xyflow/commit/2441bf8d97a6b72494f216915d52d5acbeefefde) Thanks [@moklick](https://github.com/moklick)! - Add connectionDragThreshold prop
|
||||
|
||||
## 0.0.63
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5354](https://github.com/xyflow/xyflow/pull/5354) [`c4312d89`](https://github.com/xyflow/xyflow/commit/c4312d8997ecdc7ef12cfa4efc1fde7131a2b950) Thanks [@moklick](https://github.com/moklick)! - Add TSDoc annotations
|
||||
|
||||
- [#5333](https://github.com/xyflow/xyflow/pull/5333) [`3d7e8b6b`](https://github.com/xyflow/xyflow/commit/3d7e8b6bb10001ee84d79ca4f6a9fd0053c4a276) Thanks [@peterkogo](https://github.com/peterkogo)! - Add missing type exports
|
||||
|
||||
- [#5350](https://github.com/xyflow/xyflow/pull/5350) [`9c61000c`](https://github.com/xyflow/xyflow/commit/9c61000cac6277ce97274cc626fa7266f82dec27) Thanks [@moklick](https://github.com/moklick)! - Only send node position updates if positions changed
|
||||
|
||||
## 0.0.62
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5299](https://github.com/xyflow/xyflow/pull/5299) [`848b486b`](https://github.com/xyflow/xyflow/commit/848b486b2201b650ecb3317f367a723edb2458e1) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaRole` prop to nodes and edges
|
||||
|
||||
- [#5280](https://github.com/xyflow/xyflow/pull/5280) [`dba6faf2`](https://github.com/xyflow/xyflow/commit/dba6faf20e7ec2524d5270d177331d3bd260f3ac) Thanks [@moklick](https://github.com/moklick)! - Improve typing for Nodes
|
||||
|
||||
- [#5276](https://github.com/xyflow/xyflow/pull/5276) [`6ce44a05`](https://github.com/xyflow/xyflow/commit/6ce44a05c829068ff5a8416ce3fa4ee6e0eced48) Thanks [@moklick](https://github.com/moklick)! - Add an `ease` and `interpolate` option to all function that alter the viewport
|
||||
|
||||
- [#5277](https://github.com/xyflow/xyflow/pull/5277) [`f59730ce`](https://github.com/xyflow/xyflow/commit/f59730ce3530a91f579f6bbd2ea9335680f552ef) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaLabelConfig` prop for customizing UI text like aria labels and descriptions.
|
||||
|
||||
- [#5317](https://github.com/xyflow/xyflow/pull/5317) [`09458f52`](https://github.com/xyflow/xyflow/commit/09458f52ff57356e03404c58e9bfdbfd50579850) Thanks [@moklick](https://github.com/moklick)! - Add `domAttributes` option for nodes and edges
|
||||
|
||||
- [#5326](https://github.com/xyflow/xyflow/pull/5326) [`050b511c`](https://github.com/xyflow/xyflow/commit/050b511cd6966ba526299f7ca11f9ca4791fd2cf) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent NodeResizer controls to become too small when zooming out
|
||||
|
||||
## 0.0.61
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,47 +1,62 @@
|
||||
# @xyflow/system
|
||||
|
||||
Core system that powers React Flow and Svelte Flow.
|
||||
Core system utilities powering [React Flow](https://reactflow.dev) and [Svelte Flow](https://svelteflow.dev).
|
||||
|
||||
## Installation
|
||||
> **Note:** This package is designed as a shared vanilla utility layer for React Flow and Svelte Flow. It is not intended for use with unrelated libraries.
|
||||
|
||||
```sh
|
||||
npm install @xyflow/system
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @xyflow/system
|
||||
```
|
||||
|
||||
## What is this package about?
|
||||
## What is this package?
|
||||
|
||||
The @xyflow/system package was created to have a place for vanilla utils for React Flow and Svelte Flow. The package exports helpers for edge creation, pan and zoom, dragging of nodes, general utils and lots of types. All the helpers are specifically built for React Flow and Svelte Flow so it's probably not too interesting to use them with other libraries.
|
||||
`@xyflow/system` provides core, framework-agnostic helpers and types for building node-based editors and flow diagrams. It contains the logic and utilities that are shared between React Flow and Svelte Flow, such as edge path calculations, pan/zoom, node dragging, and more.
|
||||
|
||||
### XYPanZoom
|
||||
## Features
|
||||
|
||||
Adds zoom and pan for the pane.
|
||||
- **Pan & Zoom (`XYPanZoom`)**: Utilities for adding interactive pan and zoom to your canvas.
|
||||
- **Dragging (`XYDrag`)**: Helpers for node and selection dragging.
|
||||
- **Handles/Connections (`XYHandle`)**: Logic for drawing and managing connection lines between nodes.
|
||||
- **Minimap (`XYMiniMap`)**: Interactive minimap utilities for overview and navigation.
|
||||
- **Edge Utilities**: Functions for SVG edge path creation (bezier, straight, step, smoothstep, etc.).
|
||||
- **Store Utilities**: Helpers for managing and updating flow state.
|
||||
- **DOM Utilities**: Functions for DOM measurements and interactions.
|
||||
- **Marker Utilities**: Helpers for SVG markers on edges.
|
||||
- **Graph Utilities**: Functions for working with nodes, edges, and graph structure.
|
||||
- **General Utilities**: Miscellaneous helpers used throughout the system.
|
||||
- **Types & Constants**: Shared types, enums, and constants for consistent data structures.
|
||||
|
||||
### XYDrag
|
||||
## Usage
|
||||
|
||||
Adds drag for nodes and selection.
|
||||
You can import any utility, type, or helper directly from the package:
|
||||
|
||||
### XYHandle
|
||||
```ts
|
||||
import { getBezierPath, getConnectedEdges, Position, XYPanZoom } from '@xyflow/system';
|
||||
```
|
||||
|
||||
Adds connection line drawing.
|
||||
### Example: Bezier Edge Path Creation
|
||||
|
||||
### XYMinimap
|
||||
```ts
|
||||
import { getBezierPath, Position } from '@xyflow/system';
|
||||
|
||||
Adds interactive mini map (zoom and pan).
|
||||
const [path, labelX, labelY] = getBezierPath({
|
||||
sourceX: 0,
|
||||
sourceY: 20,
|
||||
sourcePosition: Position.Right,
|
||||
targetX: 150,
|
||||
targetY: 100,
|
||||
targetPosition: Position.Left,
|
||||
});
|
||||
```
|
||||
|
||||
### Edge utils
|
||||
## API Reference
|
||||
|
||||
Util function for SVG edge path creating.
|
||||
There is currently no dedicated API documentation for this package. For details on available utilities, types, and helpers, please refer to the [source code](./src) or check out the [React Flow documentation](https://reactflow.dev/api-reference/) where we are documenting a lot of stuff from this package.
|
||||
|
||||
### Store utils
|
||||
## Contributing
|
||||
|
||||
Helpers for store functions.
|
||||
|
||||
### Dom utils
|
||||
|
||||
### Marker utils
|
||||
|
||||
### Graph utils
|
||||
|
||||
### General utils
|
||||
See the main [xyflow repository](https://github.com/xyflow/xyflow) for contribution guidelines.
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xyflow/system",
|
||||
"version": "0.0.61",
|
||||
"version": "0.0.65",
|
||||
"description": "xyflow core system that powers React Flow and Svelte Flow.",
|
||||
"keywords": [
|
||||
"node-based UI",
|
||||
|
||||
@@ -265,6 +265,10 @@ svg.xy-flow__connectionline {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.xy-flow__pane.selection .xy-flow__panel {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xy-flow__panel {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
|
||||
/* handle styles */
|
||||
.xy-flow__resize-control.handle {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 1px;
|
||||
background-color: var(--xy-resize-background-color, var(--xy-resize-background-color-default));
|
||||
transform: translate(-50%, -50%);
|
||||
translate: -50% -50%;
|
||||
}
|
||||
|
||||
.xy-flow__resize-control.handle.left {
|
||||
|
||||
@@ -40,11 +40,6 @@ export type EdgeBase<
|
||||
* This property sets the width of that invisible path.
|
||||
*/
|
||||
interactionWidth?: number;
|
||||
/**
|
||||
* A description of the edge's, used for accessibility.
|
||||
* @default "edge"
|
||||
*/
|
||||
ariaRoleDescription?: string;
|
||||
};
|
||||
|
||||
export type SmoothStepPathOptions = {
|
||||
|
||||
@@ -13,16 +13,56 @@ import { EdgeBase } from '..';
|
||||
|
||||
export type Project = (position: XYPosition) => XYPosition;
|
||||
|
||||
/**
|
||||
* This type is used to define the `onMove` handler.
|
||||
*/
|
||||
export type OnMove = (event: MouseEvent | TouchEvent | null, viewport: Viewport) => void;
|
||||
export type OnMoveStart = OnMove;
|
||||
export type OnMoveEnd = OnMove;
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type ZoomInOut = (options?: ViewportHelperFunctionOptions) => Promise<boolean>;
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type ZoomTo = (zoomLevel: number, options?: ViewportHelperFunctionOptions) => Promise<boolean>;
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type GetZoom = () => number;
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type GetViewport = () => Viewport;
|
||||
|
||||
/**
|
||||
* The `SetViewport` function is used to set the viewport of the flow.
|
||||
*
|
||||
* @inline
|
||||
* @param viewport - The viewport to set.
|
||||
* @param options - Optional parameters to control the animation and easing of the viewport change.
|
||||
*/
|
||||
export type SetViewport = (viewport: Viewport, options?: ViewportHelperFunctionOptions) => Promise<boolean>;
|
||||
|
||||
/**
|
||||
* The `SetCenter` function is used to set the center of the flow viewport to a specific position
|
||||
*
|
||||
* @inline
|
||||
* @param x - x coordinate
|
||||
* @param y - y coordinate
|
||||
* @param options - Optional parameters to control the animation and easing of the viewport change.
|
||||
*/
|
||||
export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => Promise<boolean>;
|
||||
|
||||
/**
|
||||
* The `FitBounds` function is used to fit the flow viewport to the bounds of the nodes.
|
||||
*
|
||||
* @inline
|
||||
* @param bounds - The bounds to fit the viewport to.
|
||||
* @param options - Optional parameters to control the animation and easing of the viewport change.
|
||||
*/
|
||||
export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => Promise<boolean>;
|
||||
|
||||
/**
|
||||
@@ -179,10 +219,16 @@ export type ViewportHelperFunctionOptions = {
|
||||
interpolate?: 'smooth' | 'linear';
|
||||
};
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type SetCenterOptions = ViewportHelperFunctionOptions & {
|
||||
zoom?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type FitBoundsOptions = ViewportHelperFunctionOptions & {
|
||||
padding?: number;
|
||||
};
|
||||
|
||||
@@ -73,11 +73,6 @@ export type NodeBase<
|
||||
*/
|
||||
origin?: NodeOrigin;
|
||||
handles?: NodeHandle[];
|
||||
/**
|
||||
* A description of the node's role, used for accessibility.
|
||||
* @default "node"
|
||||
*/
|
||||
ariaRoleDescription?: string;
|
||||
measured?: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
|
||||
@@ -30,21 +30,30 @@ export type GetEdgeZIndexParams = {
|
||||
elevateOnSelect?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the z-index for an edge based on the node it connects and whether it is selected.
|
||||
* By default, edges are rendered below nodes. This behaviour is different for edges that are
|
||||
* connected to nodes with a parent, as they are rendered above the parent node.
|
||||
*/
|
||||
export function getElevatedEdgeZIndex({
|
||||
sourceNode,
|
||||
targetNode,
|
||||
selected = false,
|
||||
zIndex = 0,
|
||||
zIndex,
|
||||
elevateOnSelect = false,
|
||||
}: GetEdgeZIndexParams): number {
|
||||
if (!elevateOnSelect) {
|
||||
if (zIndex !== undefined) {
|
||||
return zIndex;
|
||||
}
|
||||
|
||||
const edgeOrConnectedNodeSelected = selected || targetNode.selected || sourceNode.selected;
|
||||
const selectedZIndex = Math.max(sourceNode.internals.z || 0, targetNode.internals.z || 0, 1000);
|
||||
const edgeZ = elevateOnSelect && selected ? 1000 : 0;
|
||||
|
||||
return zIndex + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
|
||||
const nodeZ = Math.max(
|
||||
sourceNode.parentId ? sourceNode.internals.z : 0,
|
||||
targetNode.parentId ? targetNode.internals.z : 0
|
||||
);
|
||||
|
||||
return edgeZ + nodeZ;
|
||||
}
|
||||
|
||||
type IsEdgeVisibleParams = {
|
||||
|
||||
@@ -232,7 +232,7 @@ function calculateChildXYZ<NodeType extends NodeBase>(
|
||||
return {
|
||||
x: absolutePosition.x,
|
||||
y: absolutePosition.y,
|
||||
z: parentZ > childZ ? parentZ : childZ,
|
||||
z: parentZ >= childZ ? parentZ + 1 : childZ,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
||||
let dragStarted = false;
|
||||
let d3Selection: Selection<Element, unknown, null, undefined> | null = null;
|
||||
let abortDrag = false; // prevents unintentional dragging on multitouch
|
||||
let nodePositionsChanged = false;
|
||||
|
||||
// public functions
|
||||
function update({
|
||||
@@ -191,6 +192,8 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
||||
dragItem.internals.positionAbsolute = positionAbsolute;
|
||||
}
|
||||
|
||||
nodePositionsChanged = nodePositionsChanged || hasChange;
|
||||
|
||||
if (!hasChange) {
|
||||
return;
|
||||
}
|
||||
@@ -294,6 +297,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
||||
containerBounds = domNode?.getBoundingClientRect() || null;
|
||||
|
||||
abortDrag = false;
|
||||
nodePositionsChanged = false;
|
||||
|
||||
if (nodeDragThreshold === 0) {
|
||||
startDrag(event);
|
||||
@@ -354,7 +358,10 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
||||
if (dragItems.size > 0) {
|
||||
const { nodeLookup, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems();
|
||||
|
||||
updateNodePositions(dragItems, false);
|
||||
if (nodePositionsChanged) {
|
||||
updateNodePositions(dragItems, false);
|
||||
nodePositionsChanged = false;
|
||||
}
|
||||
|
||||
if (onDragStop || onNodeDragStop || (!nodeId && onSelectionDragStop)) {
|
||||
const [currentNode, currentNodes] = getEventHandlerParams({
|
||||
|
||||
@@ -45,6 +45,7 @@ function onPointerDown(
|
||||
getTransform,
|
||||
getFromHandle,
|
||||
autoPanSpeed,
|
||||
dragThreshold = 1,
|
||||
}: OnPointerDownParams
|
||||
) {
|
||||
// when xyflow is used inside a shadow root we can't use document
|
||||
@@ -56,6 +57,7 @@ function onPointerDown(
|
||||
const clickedHandle = doc?.elementFromPoint(x, y);
|
||||
const handleType = getHandleType(edgeUpdaterType, clickedHandle);
|
||||
const containerBounds = domNode?.getBoundingClientRect();
|
||||
let connectionStarted = false;
|
||||
|
||||
if (!containerBounds || !handleType) {
|
||||
return;
|
||||
@@ -92,10 +94,9 @@ function onPointerDown(
|
||||
};
|
||||
|
||||
const fromNodeInternal = nodeLookup.get(nodeId)!;
|
||||
|
||||
const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true);
|
||||
|
||||
const newConnection: ConnectionInProgress = {
|
||||
let previousConnection: ConnectionInProgress = {
|
||||
inProgress: true,
|
||||
isValid: null,
|
||||
|
||||
@@ -110,12 +111,30 @@ function onPointerDown(
|
||||
toNode: null,
|
||||
};
|
||||
|
||||
updateConnection(newConnection);
|
||||
let previousConnection: ConnectionInProgress = newConnection;
|
||||
function startConnection() {
|
||||
connectionStarted = true;
|
||||
updateConnection(previousConnection);
|
||||
onConnectStart?.(event, { nodeId, handleId, handleType });
|
||||
}
|
||||
|
||||
onConnectStart?.(event, { nodeId, handleId, handleType });
|
||||
if (dragThreshold === 0) {
|
||||
startConnection();
|
||||
}
|
||||
|
||||
function onPointerMove(event: MouseEvent | TouchEvent) {
|
||||
if (!connectionStarted) {
|
||||
const { x: evtX, y: evtY } = getEventPosition(event);
|
||||
const dx = evtX - x;
|
||||
const dy = evtY - y;
|
||||
const nextConnectionStarted = dx * dx + dy * dy > dragThreshold * dragThreshold;
|
||||
|
||||
if (!nextConnectionStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
startConnection();
|
||||
}
|
||||
|
||||
if (!getFromHandle() || !fromHandle) {
|
||||
onPointerUp(event);
|
||||
return;
|
||||
@@ -188,24 +207,27 @@ function onPointerDown(
|
||||
}
|
||||
|
||||
function onPointerUp(event: MouseEvent | TouchEvent) {
|
||||
if ((closestHandle || handleDomNode) && connection && isValid) {
|
||||
onConnect?.(connection);
|
||||
}
|
||||
if (connectionStarted) {
|
||||
if ((closestHandle || handleDomNode) && connection && isValid) {
|
||||
onConnect?.(connection);
|
||||
}
|
||||
|
||||
/*
|
||||
* it's important to get a fresh reference from the store here
|
||||
* in order to get the latest state of onConnectEnd
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { inProgress, ...connectionState } = previousConnection;
|
||||
const finalConnectionState = {
|
||||
...connectionState,
|
||||
toPosition: previousConnection.toHandle ? previousConnection.toPosition : null,
|
||||
};
|
||||
onConnectEnd?.(event, finalConnectionState);
|
||||
/*
|
||||
* it's important to get a fresh reference from the store here
|
||||
* in order to get the latest state of onConnectEnd
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { inProgress, ...connectionState } = previousConnection;
|
||||
const finalConnectionState = {
|
||||
...connectionState,
|
||||
toPosition: previousConnection.toHandle ? previousConnection.toPosition : null,
|
||||
};
|
||||
|
||||
if (edgeUpdaterType) {
|
||||
onReconnectEnd?.(event, finalConnectionState);
|
||||
onConnectEnd?.(event, finalConnectionState);
|
||||
|
||||
if (edgeUpdaterType) {
|
||||
onReconnectEnd?.(event, finalConnectionState);
|
||||
}
|
||||
}
|
||||
|
||||
cancelConnection();
|
||||
|
||||
@@ -37,6 +37,7 @@ export type OnPointerDownParams = {
|
||||
getTransform: () => Transform;
|
||||
getFromHandle: () => Handle | null;
|
||||
autoPanSpeed?: number;
|
||||
dragThreshold?: number;
|
||||
};
|
||||
|
||||
export type IsValidParams = {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
createZoomOnScrollHandler,
|
||||
} from './eventhandler';
|
||||
import { createFilter } from './filter';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { transition } from 'd3-transition';
|
||||
|
||||
export type ZoomPanValues = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { type ZoomTransform, zoomIdentity } from 'd3-zoom';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { transition } from 'd3-transition';
|
||||
|
||||
import { type D3SelectionInstance, type Viewport } from '../types';
|
||||
|
||||
Reference in New Issue
Block a user