Merge branch 'main' into middlewares
This commit is contained in:
@@ -1,5 +1,84 @@
|
||||
# @xyflow/react
|
||||
|
||||
## 12.9.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5621](https://github.com/xyflow/xyflow/pull/5621) [`c1304dba7`](https://github.com/xyflow/xyflow/commit/c1304dba7a20bb8d74c7aceb23cd80b56e4c0482) Thanks [@moklick](https://github.com/moklick)! - Set `paneClickDistance` default value to `1`.
|
||||
|
||||
- [#5578](https://github.com/xyflow/xyflow/pull/5578) [`00bcb9f5f`](https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773) Thanks [@peterkogo](https://github.com/peterkogo)! - Pass current pointer position to connection
|
||||
|
||||
- Updated dependencies [[`00bcb9f5f`](https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773)]:
|
||||
- @xyflow/system@0.0.73
|
||||
|
||||
## 12.9.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5593](https://github.com/xyflow/xyflow/pull/5593) [`a8ee089d7`](https://github.com/xyflow/xyflow/commit/a8ee089d7689d9a58113690c8e90e1c1e109602a) Thanks [@moklick](https://github.com/moklick)! - Reset selection box when user selects a node
|
||||
|
||||
## 12.9.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5572](https://github.com/xyflow/xyflow/pull/5572) [`5ec0cac7f`](https://github.com/xyflow/xyflow/commit/5ec0cac7fad21109b74839969c0818f88ddc87d9) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix onPaneClick events being suppressed when selectionOnDrag=true
|
||||
|
||||
- Updated dependencies [[`5ec0cac7f`](https://github.com/xyflow/xyflow/commit/5ec0cac7fad21109b74839969c0818f88ddc87d9)]:
|
||||
- @xyflow/system@0.0.72
|
||||
|
||||
## 12.9.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#5544](https://github.com/xyflow/xyflow/pull/5544) [`c17b49f4c`](https://github.com/xyflow/xyflow/commit/c17b49f4c16167da3f791430163edd592159d27d) Thanks [@0x0f0f0f](https://github.com/0x0f0f0f)! - Add `EdgeToolbar` component
|
||||
|
||||
- [#5550](https://github.com/xyflow/xyflow/pull/5550) [`6ffb9f790`](https://github.com/xyflow/xyflow/commit/6ffb9f7901c32f5b335aee2517f41bf87f274f32) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent child nodes of different parents from overlapping
|
||||
|
||||
- [#5551](https://github.com/xyflow/xyflow/pull/5551) [`6bb64b3ed`](https://github.com/xyflow/xyflow/commit/6bb64b3ed60f26c9ea8bc01c8d62fb9bf74cd634) Thanks [@moklick](https://github.com/moklick)! - Allow to start a selection above a node
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5531](https://github.com/xyflow/xyflow/pull/5531) [`fe2a45ed2`](https://github.com/xyflow/xyflow/commit/fe2a45ed2aa0d96db8e1be0354038be7ce6f824d) Thanks [@Sec-ant](https://github.com/Sec-ant)! - Add `type` property to `EdgeProps` to match `NodeProps` behavior
|
||||
|
||||
- [#5528](https://github.com/xyflow/xyflow/pull/5528) [`d50a963cd`](https://github.com/xyflow/xyflow/commit/d50a963cd0f00cd31236d760dcd7995218d80553) Thanks [@peterkogo](https://github.com/peterkogo)! - Let `NodeResizer` props change during ongoing resize
|
||||
|
||||
- [#5530](https://github.com/xyflow/xyflow/pull/5530) [`3b9951e42`](https://github.com/xyflow/xyflow/commit/3b9951e42f2041276ac8957154598a7af41f81e9) Thanks [@peterkogo](https://github.com/peterkogo)! - Improve performance of adding nodes & edges
|
||||
|
||||
- [#5547](https://github.com/xyflow/xyflow/pull/5547) [`1c8961207`](https://github.com/xyflow/xyflow/commit/1c8961207e8ac326098af18489465b36cdd2d831) Thanks [@moklick](https://github.com/moklick)! - Always call `onMoveEnd` when `onMoveStart` was called
|
||||
|
||||
- Updated dependencies [[`c17b49f4c`](https://github.com/xyflow/xyflow/commit/c17b49f4c16167da3f791430163edd592159d27d), [`6ffb9f790`](https://github.com/xyflow/xyflow/commit/6ffb9f7901c32f5b335aee2517f41bf87f274f32), [`d50a963cd`](https://github.com/xyflow/xyflow/commit/d50a963cd0f00cd31236d760dcd7995218d80553), [`6bb64b3ed`](https://github.com/xyflow/xyflow/commit/6bb64b3ed60f26c9ea8bc01c8d62fb9bf74cd634), [`8a9ee2d83`](https://github.com/xyflow/xyflow/commit/8a9ee2d836776da42b867c6ba90d302fbbc79b37), [`1c8961207`](https://github.com/xyflow/xyflow/commit/1c8961207e8ac326098af18489465b36cdd2d831)]:
|
||||
- @xyflow/system@0.0.71
|
||||
|
||||
## 12.8.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5515](https://github.com/xyflow/xyflow/pull/5515) [`9b4e99029`](https://github.com/xyflow/xyflow/commit/9b4e99029ba87d44c71b33c06259a126db193b43) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix id of static handles not being parsed
|
||||
|
||||
- [#5512](https://github.com/xyflow/xyflow/pull/5512) [`70b672604`](https://github.com/xyflow/xyflow/commit/70b67260470738dd0d88ed8ab558d400097be785) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent native page zoom when pinch zooming on node with nowheel class
|
||||
|
||||
- Updated dependencies [[`9b4e99029`](https://github.com/xyflow/xyflow/commit/9b4e99029ba87d44c71b33c06259a126db193b43), [`70b672604`](https://github.com/xyflow/xyflow/commit/70b67260470738dd0d88ed8ab558d400097be785)]:
|
||||
- @xyflow/system@0.0.70
|
||||
|
||||
## 12.8.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5480](https://github.com/xyflow/xyflow/pull/5480) [`f869808e3`](https://github.com/xyflow/xyflow/commit/f869808e3dfd3d289db034d21d505d62f84cdde3) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent multi-touch events while making a new connection
|
||||
|
||||
- [#5482](https://github.com/xyflow/xyflow/pull/5482) [`56381ae54`](https://github.com/xyflow/xyflow/commit/56381ae546f392b13e7c1390df60e48c9072f128) Thanks [@peterkogo](https://github.com/peterkogo)! - Make isNodeIntersecting behave the same as getIntersectingNodes
|
||||
|
||||
- [#5509](https://github.com/xyflow/xyflow/pull/5509) [`b6ae4cc11`](https://github.com/xyflow/xyflow/commit/b6ae4cc11f1456f843ace7e7f3de41c9196179e0) Thanks [@moklick](https://github.com/moklick)! - Prevent calling onResizeEnd if node was not resized
|
||||
|
||||
- [#5511](https://github.com/xyflow/xyflow/pull/5511) [`d891e5ff1`](https://github.com/xyflow/xyflow/commit/d891e5ff106c6202f69a9b14d261294cc29bbec7) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix regression: elevate edges if connected nodes are selected
|
||||
|
||||
- [#5497](https://github.com/xyflow/xyflow/pull/5497) [`eba087e8b`](https://github.com/xyflow/xyflow/commit/eba087e8bec74e6591e04ebf0e1e61799b94ecd8) Thanks [@peterkogo](https://github.com/peterkogo)! - Skip eagerly rendering nodes when node dimensions and handles are predefined
|
||||
|
||||
- [#5455](https://github.com/xyflow/xyflow/pull/5455) [`d2df1dced`](https://github.com/xyflow/xyflow/commit/d2df1dced7a7de4ed69b003c0069c7d7141bef05) Thanks [@Sec-ant](https://github.com/Sec-ant)! - Fix warning when display is set to none on the wrapper div
|
||||
|
||||
- Updated dependencies [[`f869808e3`](https://github.com/xyflow/xyflow/commit/f869808e3dfd3d289db034d21d505d62f84cdde3), [`2a35c65a0`](https://github.com/xyflow/xyflow/commit/2a35c65a02c6c8aec6b005ac3e72988f996669c4), [`b6ae4cc11`](https://github.com/xyflow/xyflow/commit/b6ae4cc11f1456f843ace7e7f3de41c9196179e0), [`d891e5ff1`](https://github.com/xyflow/xyflow/commit/d891e5ff106c6202f69a9b14d261294cc29bbec7), [`eba087e8b`](https://github.com/xyflow/xyflow/commit/eba087e8bec74e6591e04ebf0e1e61799b94ecd8)]:
|
||||
- @xyflow/system@0.0.69
|
||||
|
||||
## 12.8.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xyflow/react",
|
||||
"version": "12.8.4",
|
||||
"version": "12.9.3",
|
||||
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
|
||||
"keywords": [
|
||||
"react",
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useCallback } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { getEdgeToolbarTransform } from '@xyflow/system';
|
||||
|
||||
import { EdgeLabelRenderer } from '../../components/EdgeLabelRenderer';
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { Edge, ReactFlowState } from '../../types';
|
||||
import type { EdgeToolbarProps } from './types';
|
||||
|
||||
const zoomSelector = (state: ReactFlowState) => state.transform[2];
|
||||
|
||||
/**
|
||||
* This component can render a toolbar or tooltip to one side of a custom edge. This
|
||||
* toolbar doesn't scale with the viewport so that the content stays the same size.
|
||||
*
|
||||
* @public
|
||||
* @example
|
||||
* ```jsx
|
||||
* import { EdgeToolbar, BaseEdge, getBezierPath, type EdgeProps } from "@xyflow/react";
|
||||
*
|
||||
* export function CustomEdge({ id, data, ...props }: EdgeProps) {
|
||||
* const [edgePath, centerX, centerY] = getBezierPath(props);
|
||||
*
|
||||
* return (
|
||||
* <>
|
||||
* <BaseEdge id={id} path={edgePath} />
|
||||
* <EdgeToolbar edgeId={id} x={centerX} y={centerY} isVisible>
|
||||
* <button onClick={() => console.log('edge', id, 'click')}}>Click me</button>
|
||||
* </EdgeToolbar>
|
||||
* </>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function EdgeToolbar({
|
||||
edgeId,
|
||||
x,
|
||||
y,
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
isVisible,
|
||||
alignX = 'center',
|
||||
alignY = 'center',
|
||||
...rest
|
||||
}: EdgeToolbarProps) {
|
||||
const edgeSelector = useCallback((state: ReactFlowState): Edge | undefined => state.edgeLookup.get(edgeId), [edgeId]);
|
||||
const edge = useStore(edgeSelector, shallow);
|
||||
const isActive = typeof isVisible === 'boolean' ? isVisible : edge?.selected;
|
||||
const zoom = useStore(zoomSelector);
|
||||
|
||||
if (!isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const zIndex = (edge?.zIndex ?? 0) + 1;
|
||||
const transform = getEdgeToolbarTransform(x, y, zoom, alignX, alignY);
|
||||
|
||||
return (
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform,
|
||||
zIndex,
|
||||
pointerEvents: 'all',
|
||||
transformOrigin: '0 0',
|
||||
...style,
|
||||
}}
|
||||
className={cc(['react-flow__edge-toolbar', className])}
|
||||
data-id={edge?.id ?? ''}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { EdgeToolbar } from './EdgeToolbar';
|
||||
export type { EdgeToolbarProps } from './types';
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import type { EdgeToolbarBaseProps } from '@xyflow/system';
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type EdgeToolbarProps = EdgeToolbarBaseProps &
|
||||
HTMLAttributes<HTMLDivElement> & {
|
||||
/**
|
||||
* An edge toolbar must be attached to an edge.
|
||||
*/
|
||||
edgeId: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
@@ -3,3 +3,4 @@ export * from './Controls';
|
||||
export * from './MiniMap';
|
||||
export * from './NodeResizer';
|
||||
export * from './NodeToolbar';
|
||||
export * from './EdgeToolbar';
|
||||
|
||||
@@ -69,7 +69,7 @@ const ConnectionLine = <NodeType extends Node = Node>({
|
||||
CustomComponent,
|
||||
isValid,
|
||||
}: ConnectionLineProps<NodeType>) => {
|
||||
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition } =
|
||||
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition, pointer } =
|
||||
useConnection<NodeType>();
|
||||
|
||||
if (!inProgress) {
|
||||
@@ -92,6 +92,7 @@ const ConnectionLine = <NodeType extends Node = Node>({
|
||||
connectionStatus={getConnectionStatus(isValid)}
|
||||
toNode={toNode}
|
||||
toHandle={toHandle}
|
||||
pointer={pointer}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo, useRef, type KeyboardEvent, useCallback, JSX } from 'react';
|
||||
import { useState, useMemo, useRef, type KeyboardEvent, useCallback, JSX, memo } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import {
|
||||
@@ -15,7 +15,7 @@ import { builtinEdgeTypes, nullPosition } from './utils';
|
||||
import { EdgeUpdateAnchors } from './EdgeUpdateAnchors';
|
||||
import type { Edge, EdgeWrapperProps } from '../../types';
|
||||
|
||||
export function EdgeWrapper<EdgeType extends Edge = Edge>({
|
||||
function EdgeWrapper<EdgeType extends Edge = Edge>({
|
||||
id,
|
||||
edgesFocusable,
|
||||
edgesReconnectable,
|
||||
@@ -263,3 +263,5 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(EdgeWrapper) as typeof EdgeWrapper;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type MouseEvent, type KeyboardEvent } from 'react';
|
||||
import { type MouseEvent, type KeyboardEvent, memo } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import {
|
||||
@@ -20,7 +20,7 @@ import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from '.
|
||||
import { useNodeObserver } from './useNodeObserver';
|
||||
import type { InternalNode, Node, NodeWrapperProps } from '../../types';
|
||||
|
||||
export function NodeWrapper<NodeType extends Node>({
|
||||
function NodeWrapper<NodeType extends Node>({
|
||||
id,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
@@ -249,3 +249,5 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(NodeWrapper) as typeof NodeWrapper;
|
||||
|
||||
@@ -69,7 +69,6 @@ const reactFlowFieldsToTrack = [
|
||||
'onBeforeDelete',
|
||||
'debug',
|
||||
'autoPanSpeed',
|
||||
'paneClickDistance',
|
||||
'ariaLabelConfig',
|
||||
] as const;
|
||||
|
||||
@@ -93,7 +92,6 @@ const selector = (s: ReactFlowState) => ({
|
||||
setNodeExtent: s.setNodeExtent,
|
||||
reset: s.reset,
|
||||
setDefaultNodesAndEdges: s.setDefaultNodesAndEdges,
|
||||
setPaneClickDistance: s.setPaneClickDistance,
|
||||
});
|
||||
|
||||
const initPrevValues = {
|
||||
@@ -109,7 +107,6 @@ const initPrevValues = {
|
||||
elementsSelectable: true,
|
||||
noPanClassName: 'nopan',
|
||||
rfId: '1',
|
||||
paneClickDistance: 0,
|
||||
};
|
||||
|
||||
export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
||||
@@ -124,7 +121,6 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
|
||||
setNodeExtent,
|
||||
reset,
|
||||
setDefaultNodesAndEdges,
|
||||
setPaneClickDistance,
|
||||
} = useStore(selector, shallow);
|
||||
const store = useStoreApi<NodeType, EdgeType>();
|
||||
|
||||
@@ -155,7 +151,6 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
|
||||
else if (fieldName === 'maxZoom') setMaxZoom(fieldValue as number);
|
||||
else if (fieldName === 'translateExtent') setTranslateExtent(fieldValue as CoordinateExtent);
|
||||
else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue as CoordinateExtent);
|
||||
else if (fieldName === 'paneClickDistance') setPaneClickDistance(fieldValue as number);
|
||||
else if (fieldName === 'ariaLabelConfig')
|
||||
store.setState({ ariaLabelConfig: mergeAriaLabelConfig(fieldValue as AriaLabelConfig) });
|
||||
// Renamed fields
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useStore } from '../../hooks/useStore';
|
||||
import { useVisibleEdgeIds } from '../../hooks/useVisibleEdgeIds';
|
||||
import MarkerDefinitions from './MarkerDefinitions';
|
||||
import { GraphViewProps } from '../GraphView';
|
||||
import { EdgeWrapper } from '../../components/EdgeWrapper';
|
||||
import EdgeWrapper from '../../components/EdgeWrapper';
|
||||
import type { Edge, ReactFlowState, Node } from '../../types';
|
||||
|
||||
type EdgeRendererProps<EdgeType extends Edge = Edge> = Pick<
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { memo, type ReactNode } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { useGlobalKeyHandler } from '../../hooks/useGlobalKeyHandler';
|
||||
@@ -72,7 +73,7 @@ function FlowRendererComponent<NodeType extends Node = Node>({
|
||||
onViewportChange,
|
||||
isControlledViewport,
|
||||
}: FlowRendererProps<NodeType>) {
|
||||
const { nodesSelectionActive, userSelectionActive } = useStore(selector);
|
||||
const { nodesSelectionActive, userSelectionActive } = useStore(selector, shallow);
|
||||
const selectionKeyPressed = useKeyPress(selectionKeyCode, { target: win });
|
||||
const panActivationKeyPressed = useKeyPress(panActivationKeyCode, { target: win });
|
||||
|
||||
@@ -105,6 +106,7 @@ function FlowRendererComponent<NodeType extends Node = Node>({
|
||||
onViewportChange={onViewportChange}
|
||||
isControlledViewport={isControlledViewport}
|
||||
paneClickDistance={paneClickDistance}
|
||||
selectionOnDrag={_selectionOnDrag}
|
||||
>
|
||||
<Pane
|
||||
onSelectionStart={onSelectionStart}
|
||||
@@ -119,6 +121,7 @@ function FlowRendererComponent<NodeType extends Node = Node>({
|
||||
isSelecting={!!isSelecting}
|
||||
selectionMode={selectionMode}
|
||||
selectionKeyPressed={selectionKeyPressed}
|
||||
paneClickDistance={paneClickDistance}
|
||||
selectionOnDrag={_selectionOnDrag}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useStore } from '../../hooks/useStore';
|
||||
import { containerStyle } from '../../styles/utils';
|
||||
import { GraphViewProps } from '../GraphView';
|
||||
import { useResizeObserver } from './useResizeObserver';
|
||||
import { NodeWrapper } from '../../components/NodeWrapper';
|
||||
import NodeWrapper from '../../components/NodeWrapper';
|
||||
import type { Node, ReactFlowState } from '../../types';
|
||||
|
||||
export type NodeRendererProps<NodeType extends Node> = Pick<
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
/**
|
||||
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift
|
||||
*/
|
||||
|
||||
import {
|
||||
useRef,
|
||||
type MouseEventHandler,
|
||||
type MutableRefObject,
|
||||
type MouseEvent as ReactMouseEvent,
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
type WheelEvent as ReactWheelEvent,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import cc from 'classcat';
|
||||
import {
|
||||
getNodesInside,
|
||||
getEventPosition,
|
||||
SelectionMode,
|
||||
areSetsEqual,
|
||||
type NodeChange,
|
||||
type EdgeChange,
|
||||
} from '@xyflow/system';
|
||||
import { getNodesInside, getEventPosition, SelectionMode, areSetsEqual } from '@xyflow/system';
|
||||
|
||||
import { UserSelection } from '../../components/UserSelection';
|
||||
import { containerStyle } from '../../styles/utils';
|
||||
@@ -29,6 +21,7 @@ type PaneProps = {
|
||||
isSelecting: boolean;
|
||||
selectionKeyPressed: boolean;
|
||||
children: ReactNode;
|
||||
paneClickDistance: number;
|
||||
} & Partial<
|
||||
Pick<
|
||||
ReactFlowProps,
|
||||
@@ -47,9 +40,9 @@ type PaneProps = {
|
||||
>;
|
||||
|
||||
const wrapHandler = (
|
||||
handler: React.MouseEventHandler | undefined,
|
||||
containerRef: React.MutableRefObject<HTMLDivElement | null>
|
||||
): React.MouseEventHandler => {
|
||||
handler: MouseEventHandler | undefined,
|
||||
containerRef: MutableRefObject<HTMLDivElement | null>
|
||||
): MouseEventHandler => {
|
||||
return (event: ReactMouseEvent) => {
|
||||
if (event.target !== containerRef.current) {
|
||||
return;
|
||||
@@ -70,6 +63,7 @@ export function Pane({
|
||||
selectionKeyPressed,
|
||||
selectionMode = SelectionMode.Full,
|
||||
panOnDrag,
|
||||
paneClickDistance,
|
||||
selectionOnDrag,
|
||||
onSelectionStart,
|
||||
onSelectionEnd,
|
||||
@@ -83,17 +77,15 @@ export function Pane({
|
||||
}: PaneProps) {
|
||||
const store = useStoreApi();
|
||||
const { userSelectionActive, elementsSelectable, dragging, connectionInProgress } = useStore(selector, shallow);
|
||||
const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive);
|
||||
const isSelectionEnabled = elementsSelectable && (isSelecting || userSelectionActive);
|
||||
|
||||
const container = useRef<HTMLDivElement | null>(null);
|
||||
const containerBounds = useRef<DOMRect>();
|
||||
|
||||
const selectedNodeIds = useRef<Set<string>>(new Set());
|
||||
const selectedEdgeIds = useRef<Set<string>>(new Set());
|
||||
|
||||
// Used to prevent click events when the user lets go of the selectionKey during a selection
|
||||
const selectionInProgress = useRef<boolean>(false);
|
||||
const selectionStarted = useRef<boolean>(false);
|
||||
|
||||
const onClick = (event: ReactMouseEvent) => {
|
||||
// We prevent click events when the user let go of the selectionKey during a selection
|
||||
@@ -117,31 +109,37 @@ export function Pane({
|
||||
onPaneContextMenu?.(event);
|
||||
};
|
||||
|
||||
const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined;
|
||||
const onWheel = onPaneScroll ? (event: ReactWheelEvent) => onPaneScroll(event) : undefined;
|
||||
|
||||
const onPointerDown = (event: ReactPointerEvent): void => {
|
||||
const { resetSelectedElements, domNode } = store.getState();
|
||||
const onClickCapture = (event: ReactMouseEvent) => {
|
||||
if (selectionInProgress.current) {
|
||||
event.stopPropagation();
|
||||
selectionInProgress.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
// We are using capture here in order to prevent other pointer events
|
||||
// to be able to create a selection above a node or an edge
|
||||
const onPointerDownCapture = (event: ReactPointerEvent): void => {
|
||||
const { domNode } = store.getState();
|
||||
containerBounds.current = domNode?.getBoundingClientRect();
|
||||
if (!containerBounds.current) return;
|
||||
|
||||
if (
|
||||
!elementsSelectable ||
|
||||
!isSelecting ||
|
||||
event.button !== 0 ||
|
||||
event.target !== container.current ||
|
||||
!containerBounds.current
|
||||
) {
|
||||
const eventTargetIsContainer = event.target === container.current;
|
||||
// if a child element has the 'nokey' class, we don't want to swallow the event and don't start a selection
|
||||
const isNoKeyEvent = !eventTargetIsContainer && !!(event.target as HTMLElement).closest('.nokey');
|
||||
const isSelectionActive = (selectionOnDrag && eventTargetIsContainer) || selectionKeyPressed;
|
||||
|
||||
if (isNoKeyEvent || !isSelecting || !isSelectionActive || event.button !== 0 || !event.isPrimary) {
|
||||
return;
|
||||
}
|
||||
|
||||
(event.target as Partial<Element> | null)?.setPointerCapture?.(event.pointerId);
|
||||
(event.target as Partial<Element>)?.setPointerCapture?.(event.pointerId);
|
||||
|
||||
selectionStarted.current = true;
|
||||
selectionInProgress.current = false;
|
||||
|
||||
const { x, y } = getEventPosition(event.nativeEvent, containerBounds.current);
|
||||
|
||||
resetSelectedElements();
|
||||
|
||||
store.setState({
|
||||
userSelectionRect: {
|
||||
width: 0,
|
||||
@@ -153,7 +151,10 @@ export function Pane({
|
||||
},
|
||||
});
|
||||
|
||||
onSelectionStart?.(event);
|
||||
if (!eventTargetIsContainer) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerMove = (event: ReactPointerEvent): void => {
|
||||
@@ -166,17 +167,28 @@ export function Pane({
|
||||
triggerNodeChanges,
|
||||
triggerEdgeChanges,
|
||||
defaultEdgeOptions,
|
||||
resetSelectedElements,
|
||||
} = store.getState();
|
||||
|
||||
if (!containerBounds.current || !userSelectionRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectionInProgress.current = true;
|
||||
|
||||
const { x: mouseX, y: mouseY } = getEventPosition(event.nativeEvent, containerBounds.current);
|
||||
const { startX, startY } = userSelectionRect;
|
||||
|
||||
if (!selectionInProgress.current) {
|
||||
const requiredDistance = selectionKeyPressed ? 0 : paneClickDistance;
|
||||
const distance = Math.hypot(mouseX - startX, mouseY - startY);
|
||||
if (distance <= requiredDistance) {
|
||||
return;
|
||||
}
|
||||
resetSelectedElements();
|
||||
onSelectionStart?.(event);
|
||||
}
|
||||
|
||||
selectionInProgress.current = true;
|
||||
|
||||
const nextUserSelectRect = {
|
||||
startX,
|
||||
startY,
|
||||
@@ -211,12 +223,12 @@ export function Pane({
|
||||
}
|
||||
|
||||
if (!areSetsEqual(prevSelectedNodeIds, selectedNodeIds.current)) {
|
||||
const changes = getSelectionChanges(nodeLookup, selectedNodeIds.current, true) as NodeChange[];
|
||||
const changes = getSelectionChanges(nodeLookup, selectedNodeIds.current, true);
|
||||
triggerNodeChanges(changes);
|
||||
}
|
||||
|
||||
if (!areSetsEqual(prevSelectedEdgeIds, selectedEdgeIds.current)) {
|
||||
const changes = getSelectionChanges(edgeLookup, selectedEdgeIds.current) as EdgeChange[];
|
||||
const changes = getSelectionChanges(edgeLookup, selectedEdgeIds.current);
|
||||
triggerEdgeChanges(changes);
|
||||
}
|
||||
|
||||
@@ -228,37 +240,32 @@ export function Pane({
|
||||
};
|
||||
|
||||
const onPointerUp = (event: ReactPointerEvent) => {
|
||||
if (event.button !== 0 || !selectionStarted.current) {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
(event.target as Partial<Element>)?.releasePointerCapture?.(event.pointerId);
|
||||
const { userSelectionRect } = store.getState();
|
||||
|
||||
/*
|
||||
* We only want to trigger click functions when in selection mode if
|
||||
* the user did not move the mouse.
|
||||
*/
|
||||
if (!userSelectionActive && userSelectionRect && event.target === container.current) {
|
||||
if (!userSelectionActive && event.target === container.current && store.getState().userSelectionRect) {
|
||||
onClick?.(event);
|
||||
}
|
||||
|
||||
store.setState({
|
||||
userSelectionActive: false,
|
||||
userSelectionRect: null,
|
||||
nodesSelectionActive: selectedNodeIds.current.size > 0,
|
||||
});
|
||||
onSelectionEnd?.(event);
|
||||
|
||||
/*
|
||||
* If the user kept holding the selectionKey during the selection,
|
||||
* we need to reset the selectionInProgress, so the next click event is not prevented
|
||||
*/
|
||||
if (selectionKeyPressed || selectionOnDrag) {
|
||||
selectionInProgress.current = false;
|
||||
if (selectionInProgress.current) {
|
||||
onSelectionEnd?.(event);
|
||||
|
||||
store.setState({
|
||||
nodesSelectionActive: selectedNodeIds.current.size > 0,
|
||||
});
|
||||
}
|
||||
|
||||
selectionStarted.current = false;
|
||||
};
|
||||
|
||||
const draggable = panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0));
|
||||
@@ -266,13 +273,14 @@ export function Pane({
|
||||
return (
|
||||
<div
|
||||
className={cc(['react-flow__pane', { draggable, dragging, selection: isSelecting }])}
|
||||
onClick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
|
||||
onClick={isSelectionEnabled ? undefined : wrapHandler(onClick, container)}
|
||||
onContextMenu={wrapHandler(onContextMenu, container)}
|
||||
onWheel={wrapHandler(onWheel, container)}
|
||||
onPointerEnter={hasActiveSelection ? undefined : onPaneMouseEnter}
|
||||
onPointerDown={hasActiveSelection ? onPointerDown : onPaneMouseMove}
|
||||
onPointerMove={hasActiveSelection ? onPointerMove : onPaneMouseMove}
|
||||
onPointerUp={hasActiveSelection ? onPointerUp : undefined}
|
||||
onPointerEnter={isSelectionEnabled ? undefined : onPaneMouseEnter}
|
||||
onPointerMove={isSelectionEnabled ? onPointerMove : onPaneMouseMove}
|
||||
onPointerUp={isSelectionEnabled ? onPointerUp : undefined}
|
||||
onPointerDownCapture={isSelectionEnabled ? onPointerDownCapture : undefined}
|
||||
onClickCapture={isSelectionEnabled ? onClickCapture : undefined}
|
||||
onPointerLeave={onPaneMouseLeave}
|
||||
ref={container}
|
||||
style={containerStyle}
|
||||
|
||||
@@ -104,7 +104,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
||||
onPaneMouseLeave,
|
||||
onPaneScroll,
|
||||
onPaneContextMenu,
|
||||
paneClickDistance = 0,
|
||||
paneClickDistance = 1,
|
||||
nodeClickDistance = 0,
|
||||
children,
|
||||
onReconnect,
|
||||
@@ -309,7 +309,6 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
||||
nodeDragThreshold={nodeDragThreshold}
|
||||
connectionDragThreshold={connectionDragThreshold}
|
||||
onBeforeDelete={onBeforeDelete}
|
||||
paneClickDistance={paneClickDistance}
|
||||
debug={debug}
|
||||
ariaLabelConfig={ariaLabelConfig}
|
||||
/>
|
||||
|
||||
@@ -12,12 +12,7 @@ import type { ReactFlowState } from '../../types';
|
||||
|
||||
type ZoomPaneProps = Omit<
|
||||
FlowRendererProps,
|
||||
| 'deleteKeyCode'
|
||||
| 'selectionKeyCode'
|
||||
| 'multiSelectionKeyCode'
|
||||
| 'noDragClassName'
|
||||
| 'disableKeyboardA11y'
|
||||
| 'selectionOnDrag'
|
||||
'deleteKeyCode' | 'selectionKeyCode' | 'multiSelectionKeyCode' | 'noDragClassName' | 'disableKeyboardA11y'
|
||||
> & {
|
||||
isControlledViewport: boolean;
|
||||
};
|
||||
@@ -25,6 +20,7 @@ type ZoomPaneProps = Omit<
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
userSelectionActive: s.userSelectionActive,
|
||||
lib: s.lib,
|
||||
connectionInProgress: s.connection.inProgress,
|
||||
});
|
||||
|
||||
export function ZoomPane({
|
||||
@@ -48,10 +44,11 @@ export function ZoomPane({
|
||||
onViewportChange,
|
||||
isControlledViewport,
|
||||
paneClickDistance,
|
||||
selectionOnDrag,
|
||||
}: ZoomPaneProps) {
|
||||
const store = useStoreApi();
|
||||
const zoomPane = useRef<HTMLDivElement>(null);
|
||||
const { userSelectionActive, lib } = useStore(selector, shallow);
|
||||
const { userSelectionActive, lib, connectionInProgress } = useStore(selector, shallow);
|
||||
const zoomActivationKeyPressed = useKeyPress(zoomActivationKeyCode);
|
||||
const panZoom = useRef<PanZoomInstance>();
|
||||
|
||||
@@ -76,7 +73,6 @@ export function ZoomPane({
|
||||
maxZoom,
|
||||
translateExtent,
|
||||
viewport: defaultViewport,
|
||||
paneClickDistance,
|
||||
onDraggingChange: (paneDragging: boolean) => store.setState({ paneDragging }),
|
||||
onPanZoomStart: (event, vp) => {
|
||||
const { onViewportChangeStart, onMoveStart } = store.getState();
|
||||
@@ -126,6 +122,9 @@ export function ZoomPane({
|
||||
noWheelClassName,
|
||||
lib,
|
||||
onTransformChange,
|
||||
connectionInProgress,
|
||||
selectionOnDrag,
|
||||
paneClickDistance,
|
||||
});
|
||||
}, [
|
||||
onPaneContextMenu,
|
||||
@@ -143,6 +142,9 @@ export function ZoomPane({
|
||||
noWheelClassName,
|
||||
lib,
|
||||
onTransformChange,
|
||||
connectionInProgress,
|
||||
selectionOnDrag,
|
||||
paneClickDistance,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -202,6 +202,11 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
||||
|
||||
return { deletedNodes: matchingNodes, deletedEdges: matchingEdges };
|
||||
},
|
||||
/**
|
||||
* Partial is defined as "the 2 nodes/areas are intersecting partially".
|
||||
* If a is contained in b or b is contained in a, they are both
|
||||
* considered fully intersecting.
|
||||
*/
|
||||
getIntersectingNodes: (nodeOrRect, partially = true, nodes) => {
|
||||
const isRect = isRectObject(nodeOrRect);
|
||||
const nodeRect = isRect ? nodeOrRect : getNodeRect(nodeOrRect);
|
||||
@@ -240,7 +245,11 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
||||
const overlappingArea = getOverlappingArea(nodeRect, area);
|
||||
const partiallyVisible = partially && overlappingArea > 0;
|
||||
|
||||
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
|
||||
return (
|
||||
partiallyVisible ||
|
||||
overlappingArea >= area.width * area.height ||
|
||||
overlappingArea >= nodeRect.width * nodeRect.height
|
||||
);
|
||||
},
|
||||
updateNode,
|
||||
updateNodeData: (id, dataUpdate, options = { replace: false }) => {
|
||||
|
||||
@@ -13,7 +13,7 @@ export function useResizeHandler(domNode: MutableRefObject<HTMLDivElement | null
|
||||
|
||||
useEffect(() => {
|
||||
const updateDimensions = () => {
|
||||
if (!domNode.current) {
|
||||
if (!domNode.current || !(domNode.current.checkVisibility?.() ?? true)) {
|
||||
return false;
|
||||
}
|
||||
const size = getDimensions(domNode.current);
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
NodeOrigin,
|
||||
CoordinateExtent,
|
||||
fitViewport,
|
||||
getHandlePosition,
|
||||
Position,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
|
||||
@@ -176,7 +178,7 @@ const createStore = ({
|
||||
updateNodePositions: (nodeDragItems, dragging = false) => {
|
||||
const parentExpandChildren: ParentExpandChild[] = [];
|
||||
let changes = [];
|
||||
const { nodeLookup, triggerNodeChanges, onNodesChangeMiddlewareMap } = get();
|
||||
const { nodeLookup, triggerNodeChanges, connection, updateConnection, onNodesChangeMiddlewareMap } = get();
|
||||
|
||||
for (const [id, dragItem] of nodeDragItems) {
|
||||
// we are using the nodelookup to be sure to use the current expandParent and parentId value
|
||||
@@ -195,6 +197,11 @@ const createStore = ({
|
||||
dragging,
|
||||
};
|
||||
|
||||
if (node && connection.inProgress && connection.fromNode.id === node.id) {
|
||||
const updatedFrom = getHandlePosition(node, connection.fromHandle, Position.Left, true);
|
||||
updateConnection({ ...connection, from: updatedFrom });
|
||||
}
|
||||
|
||||
if (expandParent && node.parentId) {
|
||||
parentExpandChildren.push({
|
||||
id,
|
||||
@@ -316,9 +323,6 @@ const createStore = ({
|
||||
|
||||
set({ translateExtent });
|
||||
},
|
||||
setPaneClickDistance: (clickDistance) => {
|
||||
get().panZoom?.setClickDistance(clickDistance);
|
||||
},
|
||||
resetSelectedElements: () => {
|
||||
const { edges, nodes, triggerNodeChanges, triggerEdgeChanges, elementsSelectable } = get();
|
||||
|
||||
|
||||
@@ -79,8 +79,8 @@ const getInitialState = ({
|
||||
|
||||
return {
|
||||
rfId: '1',
|
||||
width: 0,
|
||||
height: 0,
|
||||
width: width ?? 0,
|
||||
height: height ?? 0,
|
||||
transform,
|
||||
nodes: storeNodes,
|
||||
nodesInitialized,
|
||||
|
||||
@@ -612,6 +612,7 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
|
||||
elevateNodesOnSelect?: boolean;
|
||||
/**
|
||||
* Enabling this option will raise the z-index of edges when they are selected.
|
||||
* @default false
|
||||
*/
|
||||
elevateEdgesOnSelect?: boolean;
|
||||
/**
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
OnError,
|
||||
OnReconnect,
|
||||
FinalConnectionState,
|
||||
XYPosition,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { EdgeTypes, InternalNode, Node } from '.';
|
||||
@@ -149,7 +150,7 @@ export type EdgeTextProps = Omit<SVGAttributes<SVGElement>, 'x' | 'y'> &
|
||||
*/
|
||||
export type EdgeProps<EdgeType extends Edge = Edge> = Pick<
|
||||
EdgeType,
|
||||
'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target' | 'selectable' | 'deletable'
|
||||
'id' | 'type' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target' | 'selectable' | 'deletable'
|
||||
> &
|
||||
EdgePosition &
|
||||
EdgeLabelOptions & {
|
||||
@@ -279,6 +280,7 @@ export type ConnectionLineComponentProps<NodeType extends Node = Node> = {
|
||||
connectionStatus: 'valid' | 'invalid' | null;
|
||||
toNode: InternalNode<NodeType> | null;
|
||||
toHandle: Handle | null;
|
||||
pointer: XYPosition;
|
||||
};
|
||||
|
||||
export type ConnectionLineComponent<NodeType extends Node = Node> = ComponentType<
|
||||
|
||||
@@ -228,16 +228,16 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
|
||||
nodeId: string;
|
||||
handleId?: string | null;
|
||||
}) => NodeConnection[];
|
||||
// /**
|
||||
// * Fits the view.
|
||||
// *
|
||||
// * @param options.padding - optional padding
|
||||
// * @param options.includeHiddenNodes - optional includeHiddenNodes
|
||||
// * @param options.minZoom - optional minZoom
|
||||
// * @param options.maxZoom - optional maxZoom
|
||||
// * @param options.duration - optional duration. If set, a transition will be applied
|
||||
// * @param options.nodes - optional nodes to fit the view to
|
||||
// */
|
||||
/**
|
||||
* Fits the view based on the passed params. By default it fits the view to all nodes.
|
||||
*
|
||||
* @param options.padding - optional padding
|
||||
* @param options.nodes - optional nodes to fit the view to
|
||||
* @param options.minZoom - optional minZoom
|
||||
* @param options.maxZoom - optional maxZoom
|
||||
* @param options.duration - optional duration. If set, a transition will be applied
|
||||
* @param options.includeHiddenNodes - optional includeHiddenNodes
|
||||
*/
|
||||
fitView: FitView<NodeType>;
|
||||
};
|
||||
/**
|
||||
|
||||
@@ -178,7 +178,6 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
|
||||
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
|
||||
panBy: PanBy;
|
||||
setCenter: SetCenter;
|
||||
setPaneClickDistance: (distance: number) => void;
|
||||
};
|
||||
|
||||
export type ReactFlowState<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ReactFlowStore<
|
||||
|
||||
@@ -125,9 +125,9 @@ function applyChange(change: any, element: any): any {
|
||||
|
||||
case 'dimensions': {
|
||||
if (typeof change.dimensions !== 'undefined') {
|
||||
element.measured ??= {};
|
||||
element.measured.width = change.dimensions.width;
|
||||
element.measured.height = change.dimensions.height;
|
||||
element.measured = {
|
||||
...change.dimensions,
|
||||
};
|
||||
|
||||
if (change.setAttributes) {
|
||||
if (change.setAttributes === true || change.setAttributes === 'width') {
|
||||
|
||||
@@ -1,5 +1,98 @@
|
||||
# @xyflow/svelte
|
||||
|
||||
## 1.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5603](https://github.com/xyflow/xyflow/pull/5603) [`17a175791`](https://github.com/xyflow/xyflow/commit/17a175791b2420b32d55a8fcbbb35649862b85cf) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix onPaneClick always firing when moving the viewport
|
||||
|
||||
- [#5615](https://github.com/xyflow/xyflow/pull/5615) [`8b35c77eb`](https://github.com/xyflow/xyflow/commit/8b35c77ebe5a4f6cba33c597d94eba1ead64fdfc) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix wrong positions when resizing nodes with a non-default node-origin
|
||||
|
||||
- [#5578](https://github.com/xyflow/xyflow/pull/5578) [`00bcb9f5f`](https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773) Thanks [@peterkogo](https://github.com/peterkogo)! - Pass current pointer position to connection
|
||||
|
||||
- Updated dependencies [[`00bcb9f5f`](https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773)]:
|
||||
- @xyflow/system@0.0.73
|
||||
|
||||
## 1.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5572](https://github.com/xyflow/xyflow/pull/5572) [`2a401ed67`](https://github.com/xyflow/xyflow/commit/2a401ed675b85417da46fbca68832c58d0f9bf02) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix showing selection box when no nodes are selected
|
||||
|
||||
- [#5558](https://github.com/xyflow/xyflow/pull/5558) [`dca0a30f2`](https://github.com/xyflow/xyflow/commit/dca0a30f273a05d592d8b89b3f0f30d829f5bbd0) Thanks [@peterkogo](https://github.com/peterkogo)! - Remove `edgeId` from `EdgeToolbar` props
|
||||
|
||||
- [#5565](https://github.com/xyflow/xyflow/pull/5565) [`cbab3d8be`](https://github.com/xyflow/xyflow/commit/cbab3d8be72b4935ccaaa7a1f89011923b66c57b) Thanks [@0x0f0f0f](https://github.com/0x0f0f0f)! - Pass nodeId to MiniMapNode
|
||||
|
||||
- [#5572](https://github.com/xyflow/xyflow/pull/5572) [`5ec0cac7f`](https://github.com/xyflow/xyflow/commit/5ec0cac7fad21109b74839969c0818f88ddc87d9) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix onPaneClick events being suppressed when selectionOnDrag=true
|
||||
|
||||
- [#5572](https://github.com/xyflow/xyflow/pull/5572) [`9dccac89f`](https://github.com/xyflow/xyflow/commit/9dccac89fb85e51f34f96dc3c18f2f9c7ae5a86a) Thanks [@peterkogo](https://github.com/peterkogo)! - Remove focus outline from selection box
|
||||
|
||||
- Updated dependencies [[`5ec0cac7f`](https://github.com/xyflow/xyflow/commit/5ec0cac7fad21109b74839969c0818f88ddc87d9)]:
|
||||
- @xyflow/system@0.0.72
|
||||
|
||||
## 1.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#5544](https://github.com/xyflow/xyflow/pull/5544) [`c17b49f4c`](https://github.com/xyflow/xyflow/commit/c17b49f4c16167da3f791430163edd592159d27d) Thanks [@0x0f0f0f](https://github.com/0x0f0f0f)! - Add `EdgeToolbar` component
|
||||
|
||||
- [#5550](https://github.com/xyflow/xyflow/pull/5550) [`6ffb9f790`](https://github.com/xyflow/xyflow/commit/6ffb9f7901c32f5b335aee2517f41bf87f274f32) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent child nodes of different parents from overlapping
|
||||
|
||||
- [#5551](https://github.com/xyflow/xyflow/pull/5551) [`6bb64b3ed`](https://github.com/xyflow/xyflow/commit/6bb64b3ed60f26c9ea8bc01c8d62fb9bf74cd634) Thanks [@moklick](https://github.com/moklick)! - Allow to start a selection above a node
|
||||
|
||||
- [#5526](https://github.com/xyflow/xyflow/pull/5526) [`1b0bd9794`](https://github.com/xyflow/xyflow/commit/1b0bd9794f8d610468983a579e53b4633047b465) Thanks [@peterkogo](https://github.com/peterkogo)! - Add `resizeDirection` prop to `ResizeControls`
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5538](https://github.com/xyflow/xyflow/pull/5538) [`87a868958`](https://github.com/xyflow/xyflow/commit/87a8689584d9b29570d186628829324f3bec3b2e) Thanks [@peterkogo](https://github.com/peterkogo)! - Suppress `svelte/prefer-svelte-reactivity` warnings
|
||||
|
||||
- [#5529](https://github.com/xyflow/xyflow/pull/5529) [`da77772de`](https://github.com/xyflow/xyflow/commit/da77772debe6a0e730feacadf53210210178e7e2) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix selected set to false on every node and edge eagerly
|
||||
|
||||
- [#5528](https://github.com/xyflow/xyflow/pull/5528) [`d50a963cd`](https://github.com/xyflow/xyflow/commit/d50a963cd0f00cd31236d760dcd7995218d80553) Thanks [@peterkogo](https://github.com/peterkogo)! - Let `NodeResizer` props change during ongoing resize
|
||||
|
||||
- [#5546](https://github.com/xyflow/xyflow/pull/5546) [`8a9ee2d83`](https://github.com/xyflow/xyflow/commit/8a9ee2d836776da42b867c6ba90d302fbbc79b37) Thanks [@0x0f0f0f](https://github.com/0x0f0f0f)! - Do not crash mini map if all nodes are hidden
|
||||
|
||||
- [#5547](https://github.com/xyflow/xyflow/pull/5547) [`1c8961207`](https://github.com/xyflow/xyflow/commit/1c8961207e8ac326098af18489465b36cdd2d831) Thanks [@moklick](https://github.com/moklick)! - Always call `onMoveEnd` when `onMoveStart` was called
|
||||
|
||||
- Updated dependencies [[`c17b49f4c`](https://github.com/xyflow/xyflow/commit/c17b49f4c16167da3f791430163edd592159d27d), [`6ffb9f790`](https://github.com/xyflow/xyflow/commit/6ffb9f7901c32f5b335aee2517f41bf87f274f32), [`d50a963cd`](https://github.com/xyflow/xyflow/commit/d50a963cd0f00cd31236d760dcd7995218d80553), [`6bb64b3ed`](https://github.com/xyflow/xyflow/commit/6bb64b3ed60f26c9ea8bc01c8d62fb9bf74cd634), [`8a9ee2d83`](https://github.com/xyflow/xyflow/commit/8a9ee2d836776da42b867c6ba90d302fbbc79b37), [`1c8961207`](https://github.com/xyflow/xyflow/commit/1c8961207e8ac326098af18489465b36cdd2d831)]:
|
||||
- @xyflow/system@0.0.71
|
||||
|
||||
## 1.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5515](https://github.com/xyflow/xyflow/pull/5515) [`9b4e99029`](https://github.com/xyflow/xyflow/commit/9b4e99029ba87d44c71b33c06259a126db193b43) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix id of static handles not being parsed
|
||||
|
||||
- [#5512](https://github.com/xyflow/xyflow/pull/5512) [`70b672604`](https://github.com/xyflow/xyflow/commit/70b67260470738dd0d88ed8ab558d400097be785) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent native page zoom when pinch zooming on node with nowheel class
|
||||
|
||||
- Updated dependencies [[`9b4e99029`](https://github.com/xyflow/xyflow/commit/9b4e99029ba87d44c71b33c06259a126db193b43), [`70b672604`](https://github.com/xyflow/xyflow/commit/70b67260470738dd0d88ed8ab558d400097be785)]:
|
||||
- @xyflow/system@0.0.70
|
||||
|
||||
## 1.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#5496](https://github.com/xyflow/xyflow/pull/5496) [`d23c3dfc5`](https://github.com/xyflow/xyflow/commit/d23c3dfc551216ea52183a24e9a9f24ca35cc447) Thanks [@printerscanner](https://github.com/printerscanner)! - Pass a custom `nodeComponent` to Svelteflow's Minimap
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5480](https://github.com/xyflow/xyflow/pull/5480) [`f869808e3`](https://github.com/xyflow/xyflow/commit/f869808e3dfd3d289db034d21d505d62f84cdde3) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent multi-touch events while making a new connection
|
||||
|
||||
- [#5510](https://github.com/xyflow/xyflow/pull/5510) [`cdd671699`](https://github.com/xyflow/xyflow/commit/cdd671699bfe21e0aa69bc5e8585a4d0ce189974) Thanks [@peterkogo](https://github.com/peterkogo)! - Add missing props autoPanSpeed and panOnScrollSpeed
|
||||
|
||||
- [#5482](https://github.com/xyflow/xyflow/pull/5482) [`56381ae54`](https://github.com/xyflow/xyflow/commit/56381ae546f392b13e7c1390df60e48c9072f128) Thanks [@peterkogo](https://github.com/peterkogo)! - Make isNodeIntersecting behave the same as getIntersectingNodes
|
||||
|
||||
- [#5509](https://github.com/xyflow/xyflow/pull/5509) [`b6ae4cc11`](https://github.com/xyflow/xyflow/commit/b6ae4cc11f1456f843ace7e7f3de41c9196179e0) Thanks [@moklick](https://github.com/moklick)! - Prevent calling onResizeEnd if node was not resized
|
||||
|
||||
- [#5511](https://github.com/xyflow/xyflow/pull/5511) [`d891e5ff1`](https://github.com/xyflow/xyflow/commit/d891e5ff106c6202f69a9b14d261294cc29bbec7) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix regression: elevate edges if connected nodes are selected
|
||||
|
||||
- [#5497](https://github.com/xyflow/xyflow/pull/5497) [`eba087e8b`](https://github.com/xyflow/xyflow/commit/eba087e8bec74e6591e04ebf0e1e61799b94ecd8) Thanks [@peterkogo](https://github.com/peterkogo)! - Skip eagerly rendering nodes when node dimensions and handles are predefined
|
||||
|
||||
- [#5455](https://github.com/xyflow/xyflow/pull/5455) [`86e8b4913`](https://github.com/xyflow/xyflow/commit/86e8b49133370b97deeed5269d040b01dc2d3097) Thanks [@Sec-ant](https://github.com/Sec-ant)! - Fix style attribute not being propagated to wrapper
|
||||
|
||||
- Updated dependencies [[`f869808e3`](https://github.com/xyflow/xyflow/commit/f869808e3dfd3d289db034d21d505d62f84cdde3), [`2a35c65a0`](https://github.com/xyflow/xyflow/commit/2a35c65a02c6c8aec6b005ac3e72988f996669c4), [`b6ae4cc11`](https://github.com/xyflow/xyflow/commit/b6ae4cc11f1456f843ace7e7f3de41c9196179e0), [`d891e5ff1`](https://github.com/xyflow/xyflow/commit/d891e5ff106c6202f69a9b14d261294cc29bbec7), [`eba087e8b`](https://github.com/xyflow/xyflow/commit/eba087e8bec74e6591e04ebf0e1e61799b94ecd8)]:
|
||||
- @xyflow/system@0.0.69
|
||||
|
||||
## 1.2.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xyflow/svelte",
|
||||
"version": "1.2.4",
|
||||
"version": "1.4.2",
|
||||
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
|
||||
"keywords": [
|
||||
"svelte",
|
||||
|
||||
@@ -35,8 +35,10 @@ type ZoomParams = {
|
||||
userSelectionActive: boolean;
|
||||
lib: string;
|
||||
paneClickDistance: number;
|
||||
selectionOnDrag?: boolean;
|
||||
onTransformChange: (transform: Transform) => void;
|
||||
onDraggingChange: (dragging: boolean) => void;
|
||||
connectionInProgress: boolean;
|
||||
};
|
||||
|
||||
export default function zoom(domNode: Element, params: ZoomParams) {
|
||||
@@ -48,7 +50,6 @@ export default function zoom(domNode: Element, params: ZoomParams) {
|
||||
onPanZoom,
|
||||
onPanZoomEnd,
|
||||
translateExtent,
|
||||
paneClickDistance,
|
||||
setPanZoomInstance,
|
||||
onDraggingChange,
|
||||
onTransformChange
|
||||
@@ -60,7 +61,6 @@ export default function zoom(domNode: Element, params: ZoomParams) {
|
||||
maxZoom,
|
||||
translateExtent,
|
||||
viewport: initialViewport,
|
||||
paneClickDistance,
|
||||
onPanZoom,
|
||||
onPanZoomStart,
|
||||
onPanZoomEnd,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { hideOnSSR, portal } from '$lib/actions/portal';
|
||||
|
||||
import { hideOnSSR, portal } from '$lib/actions/portal';
|
||||
import { useStore } from '$lib/store';
|
||||
import type { EdgeLabelProps } from './types';
|
||||
import { toPxString } from '$lib/utils';
|
||||
@@ -19,12 +19,13 @@
|
||||
}: EdgeLabelProps = $props();
|
||||
|
||||
const store = useStore();
|
||||
const edgeId = getContext<string>('svelteflow__edge_id');
|
||||
|
||||
const id = getContext<string>('svelteflow__edge_id');
|
||||
if (!edgeId) {
|
||||
throw new Error('EdgeLabel must be used within an edge');
|
||||
}
|
||||
|
||||
let z = $derived.by(() => {
|
||||
return store.visible.edges.get(id)?.zIndex;
|
||||
});
|
||||
let z = $derived(store.visible.edges.get(edgeId)?.zIndex);
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -39,7 +40,7 @@
|
||||
style:z-index={z}
|
||||
tabindex="-1"
|
||||
onclick={() => {
|
||||
if (selectEdgeOnClick && id) store.handleEdgeSelection(id);
|
||||
if (selectEdgeOnClick) store.handleEdgeSelection(edgeId);
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
connectionMode: store.connectionMode,
|
||||
lib: 'svelte',
|
||||
autoPanOnConnect: store.autoPanOnConnect,
|
||||
autoPanSpeed: store.autoPanSpeed,
|
||||
flowId: store.flowId,
|
||||
isValidConnection: isValidConnection ?? store.isValidConnection,
|
||||
updateConnection: store.updateConnection,
|
||||
|
||||
@@ -31,7 +31,12 @@
|
||||
if (store.selectionRectMode === 'nodes') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
store.nodes;
|
||||
return getInternalNodesBounds(store.nodeLookup, { filter: (node) => !!node.selected });
|
||||
const nodeBounds = getInternalNodesBounds(store.nodeLookup, {
|
||||
filter: (node) => !!node.selected
|
||||
});
|
||||
if (nodeBounds.width > 0 && nodeBounds.height > 0) {
|
||||
return nodeBounds;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
@@ -92,4 +97,9 @@
|
||||
z-index: 2000;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.svelte-flow__selection-wrapper:focus,
|
||||
.svelte-flow__selection-wrapper:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
typeof ResizeObserver === 'undefined'
|
||||
? null
|
||||
: new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const updates = new Map();
|
||||
|
||||
entries.forEach((entry: ResizeObserverEntry) => {
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
let {
|
||||
store = $bindable(),
|
||||
panOnDrag = true,
|
||||
paneClickDistance = 1,
|
||||
selectionOnDrag,
|
||||
onpaneclick,
|
||||
onpanecontextmenu,
|
||||
@@ -59,55 +60,53 @@
|
||||
let container: HTMLDivElement;
|
||||
let containerBounds: DOMRect | null = null;
|
||||
|
||||
/* eslint-disable svelte/prefer-svelte-reactivity */
|
||||
let selectedNodeIds: Set<string> = new Set();
|
||||
let selectedEdgeIds: Set<string> = new Set();
|
||||
/* eslint-enable svelte/prefer-svelte-reactivity */
|
||||
|
||||
let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag);
|
||||
let isSelecting = $derived(
|
||||
store.selectionKeyPressed ||
|
||||
store.selectionRect ||
|
||||
!!store.selectionRect ||
|
||||
(selectionOnDrag && panOnDragActive !== true)
|
||||
);
|
||||
let hasActiveSelection = $derived(
|
||||
let isSelectionEnabled = $derived(
|
||||
store.elementsSelectable && (isSelecting || store.selectionRectMode === 'user')
|
||||
);
|
||||
|
||||
// Used to prevent click events when the user lets go of the selectionKey during a selection
|
||||
let selectionInProgress = false;
|
||||
|
||||
function onClick(event: MouseEvent) {
|
||||
// We prevent click events when the user let go of the selectionKey during a selection
|
||||
// We also prevent click events when a connection is in progress
|
||||
if (selectionInProgress || store.connection.inProgress) {
|
||||
selectionInProgress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
onpaneclick?.({ event });
|
||||
store.unselectNodesAndEdges();
|
||||
store.selectionRectMode = null;
|
||||
}
|
||||
|
||||
// We start the selection process when the user clicks down on the pane
|
||||
function onPointerDown(event: PointerEvent) {
|
||||
function onPointerDownCapture(event: PointerEvent) {
|
||||
containerBounds = container?.getBoundingClientRect();
|
||||
if (!containerBounds) return;
|
||||
|
||||
const eventTargetIsContainer = event.target === container;
|
||||
|
||||
const isNoKeyEvent =
|
||||
!eventTargetIsContainer && !!(event.target as HTMLElement).closest('.nokey');
|
||||
|
||||
const isSelectionActive =
|
||||
(selectionOnDrag && eventTargetIsContainer) || store.selectionKeyPressed;
|
||||
|
||||
if (
|
||||
!store.elementsSelectable ||
|
||||
isNoKeyEvent ||
|
||||
!isSelecting ||
|
||||
!isSelectionActive ||
|
||||
event.button !== 0 ||
|
||||
event.target !== container ||
|
||||
!containerBounds
|
||||
!event.isPrimary
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
(event.target as Partial<Element> | null)?.setPointerCapture?.(event.pointerId);
|
||||
(event.target as Partial<Element>)?.setPointerCapture?.(event.pointerId);
|
||||
|
||||
selectionInProgress = false;
|
||||
|
||||
const { x, y } = getEventPosition(event, containerBounds);
|
||||
|
||||
store.unselectNodesAndEdges();
|
||||
|
||||
store.selectionRect = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
@@ -117,7 +116,10 @@
|
||||
y
|
||||
};
|
||||
|
||||
onselectionstart?.(event);
|
||||
if (!eventTargetIsContainer) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerMove(event: PointerEvent) {
|
||||
@@ -125,11 +127,21 @@
|
||||
return;
|
||||
}
|
||||
|
||||
selectionInProgress = true;
|
||||
|
||||
const mousePos = getEventPosition(event, containerBounds);
|
||||
const { startX = 0, startY = 0 } = store.selectionRect;
|
||||
|
||||
if (!selectionInProgress) {
|
||||
const requiredDistance = store.selectionKeyPressed ? 0 : paneClickDistance;
|
||||
const distance = Math.hypot(mousePos.x - startX, mousePos.y - startY);
|
||||
if (distance <= requiredDistance) {
|
||||
return;
|
||||
}
|
||||
store.unselectNodesAndEdges();
|
||||
onselectionstart?.(event);
|
||||
}
|
||||
|
||||
selectionInProgress = true;
|
||||
|
||||
const nextUserSelectRect = {
|
||||
...store.selectionRect,
|
||||
x: mousePos.x < startX ? mousePos.x : startX,
|
||||
@@ -184,26 +196,24 @@
|
||||
return;
|
||||
}
|
||||
|
||||
(event.target as Partial<Element> | null)?.releasePointerCapture?.(event.pointerId);
|
||||
(event.target as Partial<Element>)?.releasePointerCapture?.(event.pointerId);
|
||||
|
||||
// We only want to trigger click functions when in selection mode if
|
||||
// the user did not move the mouse.
|
||||
if (!isSelecting && store.selectionRectMode === 'user' && event.target === container) {
|
||||
|
||||
if (!selectionInProgress && event.target === container) {
|
||||
onClick?.(event);
|
||||
}
|
||||
|
||||
store.selectionRect = null;
|
||||
|
||||
if (selectedNodeIds.size > 0) {
|
||||
store.selectionRectMode = 'nodes';
|
||||
if (selectionInProgress) {
|
||||
store.selectionRectMode = selectedNodeIds.size > 0 ? 'nodes' : null;
|
||||
}
|
||||
|
||||
// If the user kept holding the selectionKey during the selection,
|
||||
// we need to reset the selectionInProgress, so the next click event is not prevented
|
||||
if (store.selectionKeyPressed) {
|
||||
selectionInProgress = false;
|
||||
if (selectionInProgress) {
|
||||
onselectionend?.(event);
|
||||
}
|
||||
|
||||
onselectionend?.(event);
|
||||
}
|
||||
|
||||
const onContextMenu = (event: MouseEvent) => {
|
||||
@@ -214,6 +224,27 @@
|
||||
|
||||
onpanecontextmenu?.({ event });
|
||||
};
|
||||
|
||||
const onClickCapture = (event: MouseEvent) => {
|
||||
if (selectionInProgress) {
|
||||
event.stopPropagation();
|
||||
selectionInProgress = false;
|
||||
}
|
||||
};
|
||||
|
||||
function onClick(event: MouseEvent) {
|
||||
// We prevent click events when the user let go of the selectionKey during a selection
|
||||
// We also prevent click events when a connection is in progress
|
||||
if (selectionInProgress || store.connection.inProgress) {
|
||||
selectionInProgress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
onpaneclick?.({ event });
|
||||
store.unselectNodesAndEdges();
|
||||
store.selectionRectMode = null;
|
||||
store.selectionRect = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
@@ -224,11 +255,12 @@
|
||||
class:draggable={panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0))}
|
||||
class:dragging={store.dragging}
|
||||
class:selection={isSelecting}
|
||||
onclick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
|
||||
onpointerdown={hasActiveSelection ? onPointerDown : undefined}
|
||||
onpointermove={hasActiveSelection ? onPointerMove : undefined}
|
||||
onpointerup={hasActiveSelection ? onPointerUp : undefined}
|
||||
onclick={isSelectionEnabled ? undefined : wrapHandler(onClick, container)}
|
||||
onpointerdowncapture={isSelectionEnabled ? onPointerDownCapture : undefined}
|
||||
onpointermove={isSelectionEnabled ? onPointerMove : undefined}
|
||||
onpointerup={isSelectionEnabled ? onPointerUp : undefined}
|
||||
oncontextmenu={wrapHandler(onContextMenu, container)}
|
||||
onclickcapture={isSelectionEnabled ? onClickCapture : undefined}
|
||||
>
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { SvelteFlowStore } from '$lib/store/types';
|
||||
export type PaneProps<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
||||
store: SvelteFlowStore<NodeType, EdgeType>;
|
||||
panOnDrag?: boolean | number[];
|
||||
paneClickDistance: number;
|
||||
selectionOnDrag?: boolean;
|
||||
onselectionstart?: (event: PointerEvent) => void;
|
||||
onselectionend?: (event: PointerEvent) => void;
|
||||
|
||||
@@ -59,8 +59,9 @@
|
||||
zoomOnDoubleClick = true,
|
||||
zoomOnPinch = true,
|
||||
panOnScroll = false,
|
||||
panOnScrollSpeed = 0.5,
|
||||
panOnDrag = true,
|
||||
selectionOnDrag = true,
|
||||
selectionOnDrag = false,
|
||||
connectionLineComponent,
|
||||
connectionLineStyle,
|
||||
connectionLineContainerStyle,
|
||||
@@ -152,8 +153,10 @@
|
||||
{zoomOnDoubleClick}
|
||||
{zoomOnPinch}
|
||||
{panOnScroll}
|
||||
{panOnScrollSpeed}
|
||||
{panOnDrag}
|
||||
{paneClickDistance}
|
||||
{selectionOnDrag}
|
||||
{onmovestart}
|
||||
{onmove}
|
||||
{onmoveend}
|
||||
@@ -166,6 +169,7 @@
|
||||
{onselectionstart}
|
||||
{onselectionend}
|
||||
{panOnDrag}
|
||||
{paneClickDistance}
|
||||
{selectionOnDrag}
|
||||
>
|
||||
<ViewportComponent bind:store>
|
||||
|
||||
@@ -80,7 +80,6 @@
|
||||
autoPanOnConnect,
|
||||
autoPanOnNodeDrag,
|
||||
colorModeSSR,
|
||||
style,
|
||||
defaultEdgeOptions,
|
||||
elevateNodesOnSelect,
|
||||
elevateEdgesOnSelect,
|
||||
@@ -95,6 +94,8 @@
|
||||
noPanClass,
|
||||
noWheelClass,
|
||||
ariaLabelConfig,
|
||||
autoPanSpeed,
|
||||
panOnScrollSpeed,
|
||||
...divAttributes
|
||||
} = $derived(rest);
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
|
||||
@@ -239,6 +239,11 @@ export type SvelteFlowProps<
|
||||
* @default true
|
||||
*/
|
||||
nodesDraggable?: boolean;
|
||||
/**
|
||||
* The speed at which the viewport pans while dragging a node or a selection box.
|
||||
* @default 15
|
||||
*/
|
||||
autoPanSpeed?: number;
|
||||
/**
|
||||
* When `true`, the viewport will pan when a node is focused.
|
||||
* @default true
|
||||
@@ -305,6 +310,12 @@ export type SvelteFlowProps<
|
||||
* @default false
|
||||
*/
|
||||
panOnScroll?: boolean;
|
||||
/**
|
||||
* Controls how fast viewport should be panned on scroll.
|
||||
* Use together with `panOnScroll` prop.
|
||||
* @default 0.5
|
||||
*/
|
||||
panOnScrollSpeed?: number;
|
||||
/**
|
||||
* This prop is used to limit the direction of panning when panOnScroll is enabled.
|
||||
* The "free" option allows panning in any direction.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
|
||||
import { PanOnScrollMode, type PanZoomInstance, type Transform } from '@xyflow/system';
|
||||
import { type PanZoomInstance, type Transform } from '@xyflow/system';
|
||||
|
||||
import zoom from '$lib/actions/zoom';
|
||||
import type { ZoomProps } from './types';
|
||||
@@ -7,14 +7,16 @@
|
||||
|
||||
let {
|
||||
store = $bindable(),
|
||||
panOnScrollMode = PanOnScrollMode.Free,
|
||||
preventScrolling = true,
|
||||
zoomOnScroll = true,
|
||||
zoomOnDoubleClick = true,
|
||||
zoomOnPinch = true,
|
||||
panOnDrag = true,
|
||||
panOnScroll = false,
|
||||
paneClickDistance = 1,
|
||||
panOnScrollMode,
|
||||
preventScrolling,
|
||||
zoomOnScroll,
|
||||
zoomOnDoubleClick,
|
||||
zoomOnPinch,
|
||||
panOnDrag,
|
||||
panOnScroll,
|
||||
panOnScrollSpeed,
|
||||
paneClickDistance,
|
||||
selectionOnDrag,
|
||||
onmovestart,
|
||||
onmove,
|
||||
onmoveend,
|
||||
@@ -58,8 +60,8 @@
|
||||
zoomOnPinch,
|
||||
panOnScroll: panOnScrollActive,
|
||||
panOnDrag: panOnDragActive,
|
||||
panOnScrollSpeed: 0.5,
|
||||
panOnScrollMode: panOnScrollMode || PanOnScrollMode.Free,
|
||||
panOnScrollSpeed,
|
||||
panOnScrollMode,
|
||||
zoomActivationKeyPressed: store.zoomActivationKeyPressed,
|
||||
preventScrolling: typeof preventScrolling === 'boolean' ? preventScrolling : true,
|
||||
noPanClassName: store.noPanClass,
|
||||
@@ -68,9 +70,11 @@
|
||||
translateExtent: store.translateExtent,
|
||||
lib: 'svelte',
|
||||
paneClickDistance,
|
||||
selectionOnDrag,
|
||||
onTransformChange: (transform: Transform) => {
|
||||
store.viewport = { x: transform[0], y: transform[1], zoom: transform[2] };
|
||||
}
|
||||
},
|
||||
connectionInProgress: store.connection.inProgress
|
||||
}}
|
||||
>
|
||||
{@render children()}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Snippet } from 'svelte';
|
||||
export type ZoomProps<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
||||
store: SvelteFlowStore<NodeType, EdgeType>;
|
||||
panOnScrollMode: PanOnScrollMode;
|
||||
panOnScrollSpeed: number;
|
||||
preventScrolling: boolean;
|
||||
zoomOnScroll: boolean;
|
||||
zoomOnDoubleClick: boolean;
|
||||
@@ -13,6 +14,7 @@ export type ZoomProps<NodeType extends Node = Node, EdgeType extends Edge = Edge
|
||||
panOnScroll: boolean;
|
||||
panOnDrag: boolean | number[];
|
||||
paneClickDistance: number;
|
||||
selectionOnDrag?: boolean;
|
||||
onmove?: OnMove;
|
||||
onmovestart?: OnMoveStart;
|
||||
onmoveend?: OnMoveEnd;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable svelte/prefer-svelte-reactivity */
|
||||
import {
|
||||
areConnectionMapsEqual,
|
||||
handleConnectionChange,
|
||||
@@ -90,3 +91,5 @@ export function useNodeConnections({
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* eslint-enable svelte/prefer-svelte-reactivity */
|
||||
|
||||
@@ -390,6 +390,11 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
|
||||
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
/**
|
||||
* Partial is defined as "the 2 nodes/areas are intersecting partially".
|
||||
* If a is contained in b or b is contained in a, they are both
|
||||
* considered fully intersecting.
|
||||
*/
|
||||
getIntersectingNodes: (
|
||||
nodeOrRect: NodeType | { id: NodeType['id'] } | Rect,
|
||||
partially = true,
|
||||
@@ -434,7 +439,11 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
|
||||
const overlappingArea = getOverlappingArea(nodeRect, area);
|
||||
const partiallyVisible = partially && overlappingArea > 0;
|
||||
|
||||
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
|
||||
return (
|
||||
partiallyVisible ||
|
||||
overlappingArea >= area.width * area.height ||
|
||||
overlappingArea >= nodeRect.width * nodeRect.height
|
||||
);
|
||||
},
|
||||
deleteElements: async ({ nodes: nodesToRemove = [], edges: edgesToRemove = [] }) => {
|
||||
const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove<
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable svelte/prefer-svelte-reactivity */
|
||||
import { useStore } from '$lib/store';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
@@ -38,3 +39,5 @@ export function useUpdateNodeInternals(): (nodeId?: string | string[]) => void {
|
||||
|
||||
return updateInternals;
|
||||
}
|
||||
|
||||
/* eslint-enable svelte/prefer-svelte-reactivity */
|
||||
|
||||
@@ -22,6 +22,7 @@ export * from '$lib/plugins/Controls';
|
||||
export * from '$lib/plugins/Background';
|
||||
export * from '$lib/plugins/Minimap';
|
||||
export * from '$lib/plugins/NodeToolbar';
|
||||
export * from '$lib/plugins/EdgeToolbar';
|
||||
export * from '$lib/plugins/NodeResizer';
|
||||
|
||||
// store
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { getEdgeToolbarTransform } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import { EdgeLabel } from '$lib/components/EdgeLabel';
|
||||
import type { EdgeToolbarProps } from './types';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
let {
|
||||
x,
|
||||
y,
|
||||
alignX = 'center',
|
||||
alignY = 'center',
|
||||
isVisible,
|
||||
selectEdgeOnClick,
|
||||
class: className,
|
||||
children,
|
||||
...rest
|
||||
}: EdgeToolbarProps = $props();
|
||||
|
||||
const store = useStore();
|
||||
const edgeId = getContext<string>('svelteflow__edge_id');
|
||||
|
||||
if (!edgeId) {
|
||||
throw new Error('EdgeToolbar must be used within an edge');
|
||||
}
|
||||
|
||||
const isActive = $derived(
|
||||
typeof isVisible === 'boolean' ? isVisible : store.edgeLookup.get(edgeId)?.selected
|
||||
);
|
||||
const transform = $derived(getEdgeToolbarTransform(x, y, store.viewport.zoom, alignX, alignY));
|
||||
</script>
|
||||
|
||||
{#if isActive}
|
||||
<EdgeLabel {selectEdgeOnClick} transparent>
|
||||
<div
|
||||
style:position="absolute"
|
||||
style:transform
|
||||
style:transform-origin="0 0"
|
||||
class={['svelte-flow__edge-toolbar', className]}
|
||||
data-id={edgeId}
|
||||
{...rest}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</EdgeLabel>
|
||||
{/if}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as EdgeToolbar } from './EdgeToolbar.svelte';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { EdgeToolbarBaseProps } from '@xyflow/system';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
export type EdgeToolbarProps = Omit<EdgeToolbarBaseProps, 'edgeId'> & {
|
||||
selectEdgeOnClick?: boolean;
|
||||
children?: Snippet;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
@@ -27,6 +27,7 @@
|
||||
nodeClass = '',
|
||||
nodeBorderRadius = 5,
|
||||
nodeStrokeWidth = 2,
|
||||
nodeComponent,
|
||||
bgColor,
|
||||
maskColor,
|
||||
maskStrokeColor,
|
||||
@@ -59,14 +60,11 @@
|
||||
width: store.width / store.viewport.zoom,
|
||||
height: store.height / store.viewport.zoom
|
||||
});
|
||||
|
||||
let boundingRect = $derived(
|
||||
store.nodeLookup.size > 0
|
||||
? getBoundsOfRects(
|
||||
getInternalNodesBounds(store.nodeLookup, { filter: (n) => !n.hidden }),
|
||||
viewBB
|
||||
)
|
||||
: viewBB
|
||||
getBoundsOfRects(getInternalNodesBounds(store.nodeLookup, { filter: (n) => !n.hidden }), viewBB)
|
||||
);
|
||||
|
||||
let scaledWidth = $derived(boundingRect.width / width);
|
||||
let scaledHeight = $derived(boundingRect.height / height);
|
||||
let viewScale = $derived(Math.max(scaledWidth, scaledHeight));
|
||||
@@ -120,13 +118,15 @@
|
||||
|
||||
{#each store.nodes as userNode (userNode.id)}
|
||||
{@const node = store.nodeLookup.get(userNode.id)}
|
||||
{#if node && nodeHasDimensions(node)}
|
||||
{#if node && nodeHasDimensions(node) && !node.hidden}
|
||||
{@const nodeDimesions = getNodeDimensions(node)}
|
||||
<MinimapNode
|
||||
id={node.id}
|
||||
x={node.internals.positionAbsolute.x}
|
||||
y={node.internals.positionAbsolute.y}
|
||||
{...nodeDimesions}
|
||||
selected={node.selected}
|
||||
{nodeComponent}
|
||||
color={nodeColorFunc?.(node)}
|
||||
borderRadius={nodeBorderRadius}
|
||||
strokeColor={nodeStrokeColorFunc(node)}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
import type { Component } from 'svelte';
|
||||
import type { MiniMapNodeProps } from './types';
|
||||
|
||||
let {
|
||||
id,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
@@ -12,8 +15,10 @@
|
||||
strokeColor,
|
||||
strokeWidth = 2,
|
||||
selected,
|
||||
class: className
|
||||
class: className,
|
||||
nodeComponent
|
||||
}: {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
@@ -25,20 +30,40 @@
|
||||
strokeWidth?: number;
|
||||
selected?: boolean;
|
||||
class?: ClassValue;
|
||||
nodeComponent?: Component<MiniMapNodeProps>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<rect
|
||||
class={['svelte-flow__minimap-node', className]}
|
||||
class:selected
|
||||
{x}
|
||||
{y}
|
||||
rx={borderRadius}
|
||||
ry={borderRadius}
|
||||
{width}
|
||||
{height}
|
||||
style:fill={color}
|
||||
style:stroke={strokeColor}
|
||||
style:stroke-width={strokeWidth}
|
||||
shape-rendering={shapeRendering}
|
||||
/>
|
||||
{#if nodeComponent}
|
||||
{@const CustomComponent = nodeComponent}
|
||||
|
||||
<CustomComponent
|
||||
{id}
|
||||
{x}
|
||||
{y}
|
||||
{width}
|
||||
{height}
|
||||
{borderRadius}
|
||||
class={className}
|
||||
{color}
|
||||
{shapeRendering}
|
||||
{strokeColor}
|
||||
{strokeWidth}
|
||||
{selected}
|
||||
/>
|
||||
{:else}
|
||||
<rect
|
||||
class={['svelte-flow__minimap-node', className]}
|
||||
class:selected
|
||||
{x}
|
||||
{y}
|
||||
rx={borderRadius}
|
||||
ry={borderRadius}
|
||||
{width}
|
||||
{height}
|
||||
style:fill={color}
|
||||
style:stroke={strokeColor}
|
||||
style:stroke-width={strokeWidth}
|
||||
shape-rendering={shapeRendering}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
import type { PanelPosition } from '@xyflow/system';
|
||||
import type { ClassValue, HTMLAttributes } from 'svelte/elements';
|
||||
import type { Component } from 'svelte';
|
||||
import type { Node } from '$lib/types';
|
||||
|
||||
export type GetMiniMapNodeAttribute = (node: Node) => string;
|
||||
|
||||
/**
|
||||
* The props that are passed to the MiniMapNode component
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type MiniMapNodeProps = {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
borderRadius?: number;
|
||||
class?: ClassValue;
|
||||
color?: string;
|
||||
shapeRendering?: string;
|
||||
strokeColor?: string;
|
||||
strokeWidth?: number;
|
||||
selected?: boolean;
|
||||
};
|
||||
|
||||
export type MiniMapProps = {
|
||||
/** Background color of minimap */
|
||||
bgColor?: string;
|
||||
@@ -17,6 +38,8 @@ export type MiniMapProps = {
|
||||
nodeBorderRadius?: number;
|
||||
/** Stroke width of nodes on the minimap */
|
||||
nodeStrokeWidth?: number;
|
||||
/** A custom component to render the nodes in the minimap. This component must render an SVG element! */
|
||||
nodeComponent?: Component<MiniMapNodeProps>;
|
||||
/** Color of the mask representing viewport */
|
||||
maskColor?: string;
|
||||
/** Stroke color of the mask representing viewport */
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
maxWidth = Number.MAX_VALUE,
|
||||
maxHeight = Number.MAX_VALUE,
|
||||
keepAspectRatio = false,
|
||||
resizeDirection,
|
||||
autoScale = true,
|
||||
shouldResize,
|
||||
onResizeStart,
|
||||
@@ -66,27 +67,28 @@
|
||||
};
|
||||
},
|
||||
onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => {
|
||||
const changes = new Map<string, Partial<Node>>();
|
||||
let position = change.x && change.y ? { x: change.x, y: change.y } : undefined;
|
||||
changes.set(id, { ...change, position });
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const changes = new Map<string, XYResizerChange>();
|
||||
changes.set(id, change);
|
||||
|
||||
for (const childChange of childChanges) {
|
||||
changes.set(childChange.id, {
|
||||
position: childChange.position
|
||||
});
|
||||
changes.set(childChange.id, {x: childChange.position.x, y: childChange.position.y });
|
||||
}
|
||||
|
||||
store.nodes = store.nodes.map((node) => {
|
||||
const change = changes.get(node.id);
|
||||
const horizontal = !resizeDirection || resizeDirection === 'horizontal';
|
||||
const vertical = !resizeDirection || resizeDirection === 'vertical';
|
||||
|
||||
if (change) {
|
||||
return {
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
x: change.position?.x ?? node.position.x,
|
||||
y: change.position?.y ?? node.position.y
|
||||
x: horizontal ? (change.x ?? node.position.x) : node.position.x,
|
||||
y: vertical ? (change.y ?? node.position.y) : node.position.y
|
||||
},
|
||||
width: change.width ?? node.width,
|
||||
height: change.height ?? node.height
|
||||
width: horizontal ? (change.width ?? node.width) : node.width,
|
||||
height: vertical ? (change.height ?? node.height) : node.height
|
||||
};
|
||||
}
|
||||
return node;
|
||||
@@ -109,6 +111,7 @@
|
||||
maxHeight
|
||||
},
|
||||
keepAspectRatio: !!keepAspectRatio,
|
||||
resizeDirection,
|
||||
onResizeStart,
|
||||
onResize,
|
||||
onResizeEnd,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type {
|
||||
ControlPosition,
|
||||
ResizeControlVariant,
|
||||
ResizeControlDirection,
|
||||
ShouldResize,
|
||||
OnResizeStart,
|
||||
OnResize,
|
||||
@@ -46,6 +47,8 @@ export type NodeResizerProps = {
|
||||
onResize?: OnResize;
|
||||
/** Callback called when resizing ends */
|
||||
onResizeEnd?: OnResizeEnd;
|
||||
/** The direction the user can resize the node */
|
||||
resizeDirection?: ResizeControlDirection;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export type ResizeControlProps = Pick<
|
||||
@@ -62,6 +65,7 @@ export type ResizeControlProps = Pick<
|
||||
| 'onResizeStart'
|
||||
| 'onResize'
|
||||
| 'onResizeEnd'
|
||||
| 'resizeDirection'
|
||||
> & {
|
||||
/** Position of control
|
||||
* @example ControlPosition.TopLeft, ControlPosition.TopRight,
|
||||
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
updateAbsolutePositions,
|
||||
snapPosition,
|
||||
calculateNodePosition,
|
||||
type SetCenterOptions
|
||||
type SetCenterOptions,
|
||||
getHandlePosition,
|
||||
Position
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
|
||||
@@ -51,6 +53,15 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
|
||||
|
||||
const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => {
|
||||
store.nodes = store.nodes.map((node) => {
|
||||
if (store.connection.inProgress && store.connection.fromNode.id === node.id) {
|
||||
const internalNode = store.nodeLookup.get(node.id);
|
||||
if (internalNode) {
|
||||
store.connection = {
|
||||
...store.connection,
|
||||
from: getHandlePosition(internalNode, store.connection.fromHandle, Position.Left, true)
|
||||
};
|
||||
}
|
||||
}
|
||||
const dragItem = nodeDragItems.get(node.id);
|
||||
return dragItem ? { ...node, position: dragItem.position, dragging } : node;
|
||||
});
|
||||
@@ -191,10 +202,6 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
|
||||
}
|
||||
}
|
||||
|
||||
function setPaneClickDistance(distance: number) {
|
||||
store.panZoom?.setClickDistance(distance);
|
||||
}
|
||||
|
||||
function deselect<T extends Node | Edge>(
|
||||
elements: T[],
|
||||
elementsToDeselect: Set<string> | null = null
|
||||
@@ -235,12 +242,8 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
|
||||
const nodeWillBeSelected = ids.includes(node.id);
|
||||
const selected = isMultiSelection ? node.selected || nodeWillBeSelected : nodeWillBeSelected;
|
||||
|
||||
if (node.selected !== selected) {
|
||||
// we need to mutate the node here in order to have the correct selected state in the drag handler
|
||||
const internalNode = store.nodeLookup.get(node.id);
|
||||
if (internalNode) internalNode.selected = selected;
|
||||
node.selected = selected;
|
||||
return { ...node };
|
||||
if (!!node.selected !== selected) {
|
||||
return { ...node, selected };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
@@ -257,7 +260,7 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
|
||||
const edgeWillBeSelected = ids.includes(edge.id);
|
||||
const selected = isMultiSelection ? edge.selected || edgeWillBeSelected : edgeWillBeSelected;
|
||||
|
||||
if (edge.selected !== selected) {
|
||||
if (!!edge.selected !== selected) {
|
||||
return { ...edge, selected };
|
||||
}
|
||||
return edge;
|
||||
@@ -397,7 +400,6 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
|
||||
setMinZoom,
|
||||
setMaxZoom,
|
||||
setTranslateExtent,
|
||||
setPaneClickDistance,
|
||||
unselectNodesAndEdges,
|
||||
addSelectedNodes,
|
||||
addSelectedEdges,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable svelte/prefer-svelte-reactivity */
|
||||
|
||||
import {
|
||||
infiniteExtent,
|
||||
SelectionMode,
|
||||
@@ -286,6 +288,7 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
||||
autoPanOnNodeDrag: boolean = $derived(signals.props.autoPanOnNodeDrag ?? true);
|
||||
autoPanOnConnect: boolean = $derived(signals.props.autoPanOnConnect ?? true);
|
||||
autoPanOnNodeFocus: boolean = $derived(signals.props.autoPanOnNodeFocus ?? true);
|
||||
autoPanSpeed: number = $derived(signals.props.autoPanSpeed ?? 15);
|
||||
connectionDragThreshold: number = $derived(signals.props.connectionDragThreshold ?? 1);
|
||||
|
||||
fitViewQueued: boolean = signals.props.fitView ?? false;
|
||||
@@ -476,3 +479,5 @@ function warnIfDeeplyReactive(array: unknown[] | undefined, name: string) {
|
||||
console.warn(`Use $state.raw for ${name} to prevent performance issues.`);
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable svelte/prefer-svelte-reactivity */
|
||||
|
||||
@@ -23,7 +23,6 @@ export type SvelteFlowStoreActions<NodeType extends Node = Node, EdgeType extend
|
||||
setMinZoom: (minZoom: number) => void;
|
||||
setMaxZoom: (maxZoom: number) => void;
|
||||
setTranslateExtent: (extent: CoordinateExtent) => void;
|
||||
setPaneClickDistance: (distance: number) => void;
|
||||
fitView: (options?: FitViewOptions) => Promise<boolean>;
|
||||
setCenter: SetCenter;
|
||||
updateNodePositions: UpdateNodePositions;
|
||||
|
||||
@@ -1,5 +1,55 @@
|
||||
# @xyflow/system
|
||||
|
||||
## 0.0.73
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5578](https://github.com/xyflow/xyflow/pull/5578) [`00bcb9f5f`](https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773) Thanks [@peterkogo](https://github.com/peterkogo)! - Pass current pointer position to connection
|
||||
|
||||
## 0.0.72
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5572](https://github.com/xyflow/xyflow/pull/5572) [`5ec0cac7f`](https://github.com/xyflow/xyflow/commit/5ec0cac7fad21109b74839969c0818f88ddc87d9) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix onPaneClick events being suppressed when selectionOnDrag=true
|
||||
|
||||
## 0.0.71
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5544](https://github.com/xyflow/xyflow/pull/5544) [`c17b49f4c`](https://github.com/xyflow/xyflow/commit/c17b49f4c16167da3f791430163edd592159d27d) Thanks [@0x0f0f0f](https://github.com/0x0f0f0f)! - Add `EdgeToolbar` component
|
||||
|
||||
- [#5550](https://github.com/xyflow/xyflow/pull/5550) [`6ffb9f790`](https://github.com/xyflow/xyflow/commit/6ffb9f7901c32f5b335aee2517f41bf87f274f32) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent child nodes of different parents from overlapping
|
||||
|
||||
- [#5528](https://github.com/xyflow/xyflow/pull/5528) [`d50a963cd`](https://github.com/xyflow/xyflow/commit/d50a963cd0f00cd31236d760dcd7995218d80553) Thanks [@peterkogo](https://github.com/peterkogo)! - Let `NodeResizer` props change during ongoing resize
|
||||
|
||||
- [#5551](https://github.com/xyflow/xyflow/pull/5551) [`6bb64b3ed`](https://github.com/xyflow/xyflow/commit/6bb64b3ed60f26c9ea8bc01c8d62fb9bf74cd634) Thanks [@moklick](https://github.com/moklick)! - Allow to start a selection above a node
|
||||
|
||||
- [#5546](https://github.com/xyflow/xyflow/pull/5546) [`8a9ee2d83`](https://github.com/xyflow/xyflow/commit/8a9ee2d836776da42b867c6ba90d302fbbc79b37) Thanks [@0x0f0f0f](https://github.com/0x0f0f0f)! - Do not crash mini map if all nodes are hidden
|
||||
|
||||
- [#5547](https://github.com/xyflow/xyflow/pull/5547) [`1c8961207`](https://github.com/xyflow/xyflow/commit/1c8961207e8ac326098af18489465b36cdd2d831) Thanks [@moklick](https://github.com/moklick)! - Always call `onMoveEnd` when `onMoveStart` was called
|
||||
|
||||
## 0.0.70
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5515](https://github.com/xyflow/xyflow/pull/5515) [`9b4e99029`](https://github.com/xyflow/xyflow/commit/9b4e99029ba87d44c71b33c06259a126db193b43) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix id of static handles not being parsed
|
||||
|
||||
- [#5512](https://github.com/xyflow/xyflow/pull/5512) [`70b672604`](https://github.com/xyflow/xyflow/commit/70b67260470738dd0d88ed8ab558d400097be785) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent native page zoom when pinch zooming on node with nowheel class
|
||||
|
||||
## 0.0.69
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#5480](https://github.com/xyflow/xyflow/pull/5480) [`f869808e3`](https://github.com/xyflow/xyflow/commit/f869808e3dfd3d289db034d21d505d62f84cdde3) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent multi-touch events while making a new connection
|
||||
|
||||
- [#5489](https://github.com/xyflow/xyflow/pull/5489) [`2a35c65a0`](https://github.com/xyflow/xyflow/commit/2a35c65a02c6c8aec6b005ac3e72988f996669c4) Thanks [@Sec-ant](https://github.com/Sec-ant)! - Fix nodeDragThreshold changing with zoom level
|
||||
|
||||
- [#5509](https://github.com/xyflow/xyflow/pull/5509) [`b6ae4cc11`](https://github.com/xyflow/xyflow/commit/b6ae4cc11f1456f843ace7e7f3de41c9196179e0) Thanks [@moklick](https://github.com/moklick)! - Prevent calling onResizeEnd if node was not resized
|
||||
|
||||
- [#5511](https://github.com/xyflow/xyflow/pull/5511) [`d891e5ff1`](https://github.com/xyflow/xyflow/commit/d891e5ff106c6202f69a9b14d261294cc29bbec7) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix regression: elevate edges if connected nodes are selected
|
||||
|
||||
- [#5497](https://github.com/xyflow/xyflow/pull/5497) [`eba087e8b`](https://github.com/xyflow/xyflow/commit/eba087e8bec74e6591e04ebf0e1e61799b94ecd8) Thanks [@peterkogo](https://github.com/peterkogo)! - Skip eagerly rendering nodes when node dimensions and handles are predefined
|
||||
|
||||
## 0.0.68
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xyflow/system",
|
||||
"version": "0.0.68",
|
||||
"version": "0.0.73",
|
||||
"description": "xyflow core system that powers React Flow and Svelte Flow.",
|
||||
"keywords": [
|
||||
"node-based UI",
|
||||
|
||||
@@ -129,3 +129,30 @@ export type EdgePosition = {
|
||||
};
|
||||
|
||||
export type EdgeLookup<EdgeType extends EdgeBase = EdgeBase> = Map<string, EdgeType>;
|
||||
|
||||
export type EdgeToolbarBaseProps = {
|
||||
/**
|
||||
* The `x` position of the edge toolbar.
|
||||
*/
|
||||
x: number;
|
||||
/**
|
||||
* The `y` position of the edge toolbar.
|
||||
*/
|
||||
y: number;
|
||||
/** If `true`, edge toolbar is visible even if edge is not selected.
|
||||
* @default false
|
||||
*/
|
||||
isVisible?: boolean;
|
||||
/**
|
||||
* Align the vertical toolbar position relative to the passed x position.
|
||||
* @default "center"
|
||||
* @example 'left', 'center', 'right'
|
||||
*/
|
||||
alignX?: 'left' | 'center' | 'right';
|
||||
/**
|
||||
* Align the horizontal toolbar position relative to the passed y position.
|
||||
* @default "center"
|
||||
* @example 'top', 'center', 'bottom'
|
||||
*/
|
||||
alignY?: 'top' | 'center' | 'bottom';
|
||||
};
|
||||
|
||||
@@ -292,6 +292,7 @@ export const initialConnection: NoConnection = {
|
||||
toHandle: null,
|
||||
toPosition: null,
|
||||
toNode: null,
|
||||
pointer: null,
|
||||
};
|
||||
|
||||
export type NoConnection = {
|
||||
@@ -305,6 +306,7 @@ export type NoConnection = {
|
||||
toHandle: null;
|
||||
toPosition: null;
|
||||
toNode: null;
|
||||
pointer: null;
|
||||
};
|
||||
export type ConnectionInProgress<NodeType extends InternalNodeBase = InternalNodeBase> = {
|
||||
/** Indicates whether a connection is currently in progress. */
|
||||
@@ -330,6 +332,8 @@ export type ConnectionInProgress<NodeType extends InternalNodeBase = InternalNod
|
||||
toPosition: Position;
|
||||
/** Returns the end node or `null` if no connection is in progress. */
|
||||
toNode: NodeType | null;
|
||||
/** Returns the pointer position or `null` if no connection is in progress. */
|
||||
pointer: XYPosition;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -95,6 +95,7 @@ export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = Omit<NodeTy
|
||||
internals: {
|
||||
positionAbsolute: XYPosition;
|
||||
z: number;
|
||||
rootParentIndex?: number;
|
||||
/**
|
||||
* Holds a reference to the original node object provided by the user.
|
||||
* Used as an optimization to avoid certain operations.
|
||||
|
||||
@@ -9,7 +9,6 @@ export type PanZoomParams = {
|
||||
domNode: Element;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
paneClickDistance: number;
|
||||
viewport: Viewport;
|
||||
translateExtent: CoordinateExtent;
|
||||
onDraggingChange: OnDraggingChange;
|
||||
@@ -42,6 +41,9 @@ export type PanZoomUpdateOptions = {
|
||||
zoomActivationKeyPressed: boolean;
|
||||
lib: string;
|
||||
onTransformChange: OnTransformChange;
|
||||
connectionInProgress: boolean;
|
||||
paneClickDistance: number;
|
||||
selectionOnDrag?: boolean;
|
||||
};
|
||||
|
||||
export type PanZoomInstance = {
|
||||
|
||||
@@ -43,7 +43,6 @@ export function isInputDOMNode(event: KeyboardEvent): boolean {
|
||||
if (target?.nodeType !== 1 /* Node.ELEMENT_NODE */) return false;
|
||||
|
||||
const isInput = inputTags.includes(target.nodeName) || target.hasAttribute('contenteditable');
|
||||
|
||||
// when an input field is focused we don't want to trigger deletion or movement of nodes
|
||||
return isInput || !!target.closest('.nokey');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
const alignXToPercent: Record<'left' | 'center' | 'right', number> = {
|
||||
left: 0,
|
||||
center: 50,
|
||||
right: 100,
|
||||
};
|
||||
|
||||
const alignYToPercent: Record<'top' | 'center' | 'bottom', number> = {
|
||||
top: 0,
|
||||
center: 50,
|
||||
bottom: 100,
|
||||
};
|
||||
|
||||
export function getEdgeToolbarTransform(
|
||||
x: number,
|
||||
y: number,
|
||||
zoom: number,
|
||||
alignX: 'left' | 'center' | 'right' = 'center',
|
||||
alignY: 'top' | 'center' | 'bottom' = 'center'
|
||||
): string {
|
||||
return `translate(${x}px, ${y}px) scale(${1 / zoom}) translate(${-(alignXToPercent[alignX] ?? 50)}%, ${-(
|
||||
alignYToPercent[alignY] ?? 50
|
||||
)}%)`;
|
||||
}
|
||||
@@ -45,12 +45,11 @@ export function getElevatedEdgeZIndex({
|
||||
if (zIndex !== undefined) {
|
||||
return zIndex;
|
||||
}
|
||||
|
||||
const edgeZ = elevateOnSelect && selected ? 1000 : 0;
|
||||
|
||||
const nodeZ = Math.max(
|
||||
sourceNode.parentId ? sourceNode.internals.z : 0,
|
||||
targetNode.parentId ? targetNode.internals.z : 0
|
||||
sourceNode.parentId || (elevateOnSelect && sourceNode.selected) ? sourceNode.internals.z : 0,
|
||||
targetNode.parentId || (elevateOnSelect && targetNode.selected) ? targetNode.internals.z : 0
|
||||
);
|
||||
|
||||
return edgeZ + nodeZ;
|
||||
@@ -85,7 +84,19 @@ export function isEdgeVisible({ sourceNode, targetNode, width, height, transform
|
||||
return getOverlappingArea(viewRect, boxToRect(edgeBox)) > 0;
|
||||
}
|
||||
|
||||
const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection | EdgeBase): string =>
|
||||
/**
|
||||
* Type for a custom edge ID generator function.
|
||||
* @public
|
||||
*/
|
||||
export type GetEdgeId = (params: Connection | EdgeBase) => string;
|
||||
|
||||
/**
|
||||
* The default edge ID generator function. Generates an ID based on the source, target, and handles.
|
||||
* @public
|
||||
* @param params - The connection or edge to generate an ID for.
|
||||
* @returns The generated edge ID.
|
||||
*/
|
||||
export const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection | EdgeBase): string =>
|
||||
`xy-edge__${source}${sourceHandle || ''}-${target}${targetHandle || ''}`;
|
||||
|
||||
const connectionExists = (edge: EdgeBase, edges: EdgeBase[]) => {
|
||||
@@ -98,11 +109,19 @@ const connectionExists = (edge: EdgeBase, edges: EdgeBase[]) => {
|
||||
);
|
||||
};
|
||||
|
||||
export type AddEdgeOptions = {
|
||||
/**
|
||||
* Custom function to generate edge IDs. If not provided, the default `getEdgeId` function is used.
|
||||
*/
|
||||
getEdgeId?: GetEdgeId;
|
||||
};
|
||||
|
||||
/**
|
||||
* This util is a convenience function to add a new Edge to an array of edges. It also performs some validation to make sure you don't add an invalid edge or duplicate an existing one.
|
||||
* @public
|
||||
* @param edgeParams - Either an `Edge` or a `Connection` you want to add.
|
||||
* @param edges - The array of all current edges.
|
||||
* @param options - Optional configuration object.
|
||||
* @returns A new array of edges with the new edge added.
|
||||
*
|
||||
* @remarks If an edge with the same `target` and `source` already exists (and the same
|
||||
@@ -112,7 +131,8 @@ const connectionExists = (edge: EdgeBase, edges: EdgeBase[]) => {
|
||||
*/
|
||||
export const addEdge = <EdgeType extends EdgeBase>(
|
||||
edgeParams: EdgeType | Connection,
|
||||
edges: EdgeType[]
|
||||
edges: EdgeType[],
|
||||
options: AddEdgeOptions = {}
|
||||
): EdgeType[] => {
|
||||
if (!edgeParams.source || !edgeParams.target) {
|
||||
devWarn('006', errorMessages['error006']());
|
||||
@@ -120,13 +140,15 @@ export const addEdge = <EdgeType extends EdgeBase>(
|
||||
return edges;
|
||||
}
|
||||
|
||||
const edgeIdGenerator = options.getEdgeId || getEdgeId;
|
||||
|
||||
let edge: EdgeType;
|
||||
if (isEdgeBase(edgeParams)) {
|
||||
edge = { ...edgeParams };
|
||||
} else {
|
||||
edge = {
|
||||
...edgeParams,
|
||||
id: getEdgeId(edgeParams),
|
||||
id: edgeIdGenerator(edgeParams),
|
||||
} as EdgeType;
|
||||
}
|
||||
|
||||
@@ -151,6 +173,10 @@ export type ReconnectEdgeOptions = {
|
||||
* @default true
|
||||
*/
|
||||
shouldReplaceId?: boolean;
|
||||
/**
|
||||
* Custom function to generate edge IDs. If not provided, the default `getEdgeId` function is used.
|
||||
*/
|
||||
getEdgeId?: GetEdgeId;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -191,10 +217,12 @@ export const reconnectEdge = <EdgeType extends EdgeBase>(
|
||||
return edges;
|
||||
}
|
||||
|
||||
const edgeIdGenerator = options.getEdgeId || getEdgeId;
|
||||
|
||||
// Remove old edge and create the new edge with parameters of old edge.
|
||||
const edge = {
|
||||
...rest,
|
||||
id: options.shouldReplaceId ? getEdgeId(newConnection) : oldEdgeId,
|
||||
id: options.shouldReplaceId ? edgeIdGenerator(newConnection) : oldEdgeId,
|
||||
source: newConnection.source,
|
||||
target: newConnection.target,
|
||||
sourceHandle: newConnection.sourceHandle,
|
||||
|
||||
@@ -241,20 +241,17 @@ export const getInternalNodesBounds = <NodeType extends InternalNodeBase | NodeD
|
||||
nodeLookup: Map<string, NodeType>,
|
||||
params: GetInternalNodesBoundsParams<NodeType> = {}
|
||||
): Rect => {
|
||||
if (nodeLookup.size === 0) {
|
||||
return { x: 0, y: 0, width: 0, height: 0 };
|
||||
}
|
||||
|
||||
let box = { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity };
|
||||
let hasVisibleNodes = false;
|
||||
|
||||
nodeLookup.forEach((node) => {
|
||||
if (params.filter === undefined || params.filter(node)) {
|
||||
const nodeBox = nodeToBox(node as InternalNodeBase);
|
||||
box = getBoundsOfBoxes(box, nodeBox);
|
||||
box = getBoundsOfBoxes(box, nodeToBox(node as InternalNodeBase));
|
||||
hasVisibleNodes = true;
|
||||
}
|
||||
});
|
||||
|
||||
return boxToRect(box);
|
||||
return hasVisibleNodes ? boxToRect(box) : { x: 0, y: 0, width: 0, height: 0 };
|
||||
};
|
||||
|
||||
export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
|
||||
|
||||
@@ -5,6 +5,7 @@ export * from './graph';
|
||||
export * from './general';
|
||||
export * from './marker';
|
||||
export * from './node-toolbar';
|
||||
export * from './edge-toolbar';
|
||||
export * from './store';
|
||||
export * from './types';
|
||||
export * from './shallow-node-data';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HandleConnection, infiniteExtent } from '..';
|
||||
import { Handle, HandleConnection, infiniteExtent, NodeHandle, NodeHandleBounds } from '..';
|
||||
import {
|
||||
NodeBase,
|
||||
CoordinateExtent,
|
||||
@@ -30,6 +30,9 @@ import {
|
||||
import { getNodePositionWithOrigin } from './graph';
|
||||
import { ParentExpandChild } from './types';
|
||||
|
||||
const SELECTED_NODE_Z = 1000;
|
||||
const ROOT_PARENT_Z_INCREMENT = 10;
|
||||
|
||||
const defaultOptions = {
|
||||
nodeOrigin: [0, 0] as NodeOrigin,
|
||||
nodeExtent: infiniteExtent,
|
||||
@@ -72,6 +75,38 @@ export function updateAbsolutePositions<NodeType extends NodeBase>(
|
||||
}
|
||||
}
|
||||
|
||||
function parseHandles(userNode: NodeBase, internalNode?: InternalNodeBase): NodeHandleBounds | undefined {
|
||||
if (!userNode.handles) {
|
||||
return !userNode.measured ? undefined : internalNode?.internals.handleBounds;
|
||||
}
|
||||
|
||||
const source: Handle[] = [];
|
||||
const target: Handle[] = [];
|
||||
|
||||
for (const handle of userNode.handles) {
|
||||
const handleBounds = {
|
||||
id: handle.id,
|
||||
width: handle.width ?? 1,
|
||||
height: handle.height ?? 1,
|
||||
nodeId: userNode.id,
|
||||
x: handle.x,
|
||||
y: handle.y,
|
||||
position: handle.position,
|
||||
type: handle.type,
|
||||
};
|
||||
if (handle.type === 'source') {
|
||||
source.push(handleBounds);
|
||||
} else if (handle.type === 'target') {
|
||||
target.push(handleBounds);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
source,
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
type UpdateNodesOptions<NodeType extends NodeBase> = {
|
||||
nodeOrigin?: NodeOrigin;
|
||||
nodeExtent?: CoordinateExtent;
|
||||
@@ -88,9 +123,10 @@ export function adoptUserNodes<NodeType extends NodeBase>(
|
||||
): boolean {
|
||||
const _options = mergeObjects(adoptUserNodesDefaultOptions, options);
|
||||
|
||||
let rootParentIndex = { i: -1 };
|
||||
let nodesInitialized = nodes.length > 0;
|
||||
const tmpLookup = new Map(nodeLookup);
|
||||
const selectedNodeZ: number = _options?.elevateNodesOnSelect ? 1000 : 0;
|
||||
const selectedNodeZ: number = _options?.elevateNodesOnSelect ? SELECTED_NODE_Z : 0;
|
||||
|
||||
nodeLookup.clear();
|
||||
parentLookup.clear();
|
||||
@@ -115,7 +151,7 @@ export function adoptUserNodes<NodeType extends NodeBase>(
|
||||
internals: {
|
||||
positionAbsolute: clampedPosition,
|
||||
// if user re-initializes the node or removes `measured` for whatever reason, we reset the handleBounds so that the node gets re-measured
|
||||
handleBounds: !userNode.measured ? undefined : internalNode?.internals.handleBounds,
|
||||
handleBounds: parseHandles(userNode, internalNode),
|
||||
z: calculateZ(userNode, selectedNodeZ),
|
||||
userNode,
|
||||
},
|
||||
@@ -134,7 +170,7 @@ export function adoptUserNodes<NodeType extends NodeBase>(
|
||||
}
|
||||
|
||||
if (userNode.parentId) {
|
||||
updateChildNode(internalNode, nodeLookup, parentLookup, options);
|
||||
updateChildNode(internalNode, nodeLookup, parentLookup, options, rootParentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +201,8 @@ function updateChildNode<NodeType extends NodeBase>(
|
||||
node: InternalNodeBase<NodeType>,
|
||||
nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
|
||||
parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
|
||||
options?: UpdateNodesOptions<NodeType>
|
||||
options?: UpdateNodesOptions<NodeType>,
|
||||
rootParentIndex?: { i: number }
|
||||
) {
|
||||
const { elevateNodesOnSelect, nodeOrigin, nodeExtent } = mergeObjects(defaultOptions, options);
|
||||
const parentId = node.parentId!;
|
||||
@@ -180,7 +217,18 @@ function updateChildNode<NodeType extends NodeBase>(
|
||||
|
||||
updateParentLookup(node, parentLookup);
|
||||
|
||||
const selectedNodeZ = elevateNodesOnSelect ? 1000 : 0;
|
||||
// We just want to set the rootParentIndex for the first child
|
||||
if (rootParentIndex && !parentNode.parentId && parentNode.internals.rootParentIndex === undefined) {
|
||||
parentNode.internals.rootParentIndex = ++rootParentIndex.i;
|
||||
parentNode.internals.z = parentNode.internals.z + rootParentIndex.i * ROOT_PARENT_Z_INCREMENT;
|
||||
}
|
||||
|
||||
// But we need to update rootParentIndex.i also when parent has not been updated
|
||||
if (rootParentIndex && parentNode.internals.rootParentIndex !== undefined) {
|
||||
rootParentIndex.i = parentNode.internals.rootParentIndex;
|
||||
}
|
||||
|
||||
const selectedNodeZ = elevateNodesOnSelect ? SELECTED_NODE_Z : 0;
|
||||
const { x, y, z } = calculateChildXYZ(node, parentNode, nodeOrigin, nodeExtent, selectedNodeZ);
|
||||
const { positionAbsolute } = node.internals;
|
||||
const positionChanged = x !== positionAbsolute.x || y !== positionAbsolute.y;
|
||||
|
||||
@@ -334,8 +334,10 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
||||
}
|
||||
|
||||
if (!dragStarted) {
|
||||
const x = pointerPos.xSnapped - (lastPos.x ?? 0);
|
||||
const y = pointerPos.ySnapped - (lastPos.y ?? 0);
|
||||
// Calculate distance in client coordinates for consistent drag threshold behavior across zoom levels
|
||||
const currentMousePosition = getEventPosition(event.sourceEvent, containerBounds!);
|
||||
const x = currentMousePosition.x - mousePosition.x;
|
||||
const y = currentMousePosition.y - mousePosition.y;
|
||||
const distance = Math.sqrt(x * x + y * y);
|
||||
|
||||
if (distance > nodeDragThreshold) {
|
||||
|
||||
@@ -93,8 +93,8 @@ function onPointerDown(
|
||||
position: fromHandleInternal.position,
|
||||
};
|
||||
|
||||
const fromNodeInternal = nodeLookup.get(nodeId)!;
|
||||
const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true);
|
||||
const fromInternalNode = nodeLookup.get(nodeId)!;
|
||||
const from = getHandlePosition(fromInternalNode, fromHandle, Position.Left, true);
|
||||
|
||||
let previousConnection: ConnectionInProgress = {
|
||||
inProgress: true,
|
||||
@@ -103,12 +103,13 @@ function onPointerDown(
|
||||
from,
|
||||
fromHandle,
|
||||
fromPosition: fromHandle.position,
|
||||
fromNode: fromNodeInternal,
|
||||
fromNode: fromInternalNode,
|
||||
|
||||
to: position,
|
||||
toHandle: null,
|
||||
toPosition: oppositePosition[fromHandle.position],
|
||||
toNode: null,
|
||||
pointer: position,
|
||||
};
|
||||
|
||||
function startConnection() {
|
||||
@@ -171,9 +172,14 @@ function onPointerDown(
|
||||
connection = result.connection;
|
||||
isValid = isConnectionValid(!!closestHandle, result.isValid);
|
||||
|
||||
const fromInternalNode = nodeLookup.get(nodeId);
|
||||
const from = fromInternalNode
|
||||
? getHandlePosition(fromInternalNode, fromHandle, Position.Left, true)
|
||||
: previousConnection.from;
|
||||
|
||||
const newConnection: ConnectionInProgress = {
|
||||
// from stays the same
|
||||
...previousConnection,
|
||||
from,
|
||||
isValid,
|
||||
to:
|
||||
result.toHandle && isValid
|
||||
@@ -182,31 +188,19 @@ function onPointerDown(
|
||||
toHandle: result.toHandle,
|
||||
toPosition: isValid && result.toHandle ? result.toHandle.position : oppositePosition[fromHandle.position],
|
||||
toNode: result.toHandle ? nodeLookup.get(result.toHandle.nodeId)! : null,
|
||||
pointer: position,
|
||||
};
|
||||
|
||||
/*
|
||||
* we don't want to trigger an update when the connection
|
||||
* is snapped to the same handle as before
|
||||
*/
|
||||
if (
|
||||
isValid &&
|
||||
closestHandle &&
|
||||
previousConnection.toHandle &&
|
||||
newConnection.toHandle &&
|
||||
previousConnection.toHandle.type === newConnection.toHandle.type &&
|
||||
previousConnection.toHandle.nodeId === newConnection.toHandle.nodeId &&
|
||||
previousConnection.toHandle.id === newConnection.toHandle.id &&
|
||||
previousConnection.to.x === newConnection.to.x &&
|
||||
previousConnection.to.y === newConnection.to.y
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateConnection(newConnection);
|
||||
previousConnection = newConnection;
|
||||
}
|
||||
|
||||
function onPointerUp(event: MouseEvent | TouchEvent) {
|
||||
// Prevent multi-touch aborting connection
|
||||
if ('touches' in event && event.touches.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionStarted) {
|
||||
if ((closestHandle || resultHandleDomNode) && connection && isValid) {
|
||||
onConnect?.(connection);
|
||||
|
||||
@@ -37,7 +37,6 @@ export function XYPanZoom({
|
||||
domNode,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
paneClickDistance,
|
||||
translateExtent,
|
||||
viewport,
|
||||
onPanZoom,
|
||||
@@ -55,10 +54,7 @@ export function XYPanZoom({
|
||||
isPanScrolling: false,
|
||||
};
|
||||
const bbox = domNode.getBoundingClientRect();
|
||||
const d3ZoomInstance = zoom()
|
||||
.clickDistance(!isNumeric(paneClickDistance) || paneClickDistance < 0 ? 0 : paneClickDistance)
|
||||
.scaleExtent([minZoom, maxZoom])
|
||||
.translateExtent(translateExtent);
|
||||
const d3ZoomInstance = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent);
|
||||
const d3Selection = select(domNode).call(d3ZoomInstance);
|
||||
|
||||
setViewportConstrained(
|
||||
@@ -108,6 +104,9 @@ export function XYPanZoom({
|
||||
zoomActivationKeyPressed,
|
||||
lib,
|
||||
onTransformChange,
|
||||
connectionInProgress,
|
||||
paneClickDistance,
|
||||
selectionOnDrag,
|
||||
}: PanZoomUpdateOptions) {
|
||||
if (userSelectionActive && !zoomPanValues.isZoomingOrPanning) {
|
||||
destroy();
|
||||
@@ -115,6 +114,10 @@ export function XYPanZoom({
|
||||
|
||||
const isPanOnScroll = panOnScroll && !zoomActivationKeyPressed && !userSelectionActive;
|
||||
|
||||
d3ZoomInstance.clickDistance(
|
||||
selectionOnDrag ? Infinity : !isNumeric(paneClickDistance) || paneClickDistance < 0 ? 0 : paneClickDistance
|
||||
);
|
||||
|
||||
const wheelHandler = isPanOnScroll
|
||||
? createPanOnScrollHandler({
|
||||
zoomPanValues,
|
||||
@@ -178,6 +181,7 @@ export function XYPanZoom({
|
||||
noPanClassName,
|
||||
noWheelClassName,
|
||||
lib,
|
||||
connectionInProgress,
|
||||
});
|
||||
d3ZoomInstance.filter(filter);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
type OnDraggingChange,
|
||||
type OnTransformChange,
|
||||
} from '../types';
|
||||
import { isRightClickPan, isWrappedWithClass, transformToViewport, viewChanged, wheelDelta } from './utils';
|
||||
import { isRightClickPan, isWrappedWithClass, transformToViewport, wheelDelta } from './utils';
|
||||
import { isMacOs } from '../utils';
|
||||
import { ZoomPanValues } from './XYPanZoom';
|
||||
|
||||
@@ -72,6 +72,9 @@ export function createPanOnScrollHandler({
|
||||
}: PanOnScrollParams) {
|
||||
return (event: any) => {
|
||||
if (isWrappedWithClass(event, noWheelClassName)) {
|
||||
if (event.ctrlKey) {
|
||||
event.preventDefault(); // stop native page zoom for pinch zooming
|
||||
}
|
||||
return false;
|
||||
}
|
||||
event.preventDefault();
|
||||
@@ -125,9 +128,7 @@ export function createPanOnScrollHandler({
|
||||
zoomPanValues.isPanScrolling = true;
|
||||
|
||||
onPanZoomStart?.(event, nextViewport);
|
||||
}
|
||||
|
||||
if (zoomPanValues.isPanScrolling) {
|
||||
} else {
|
||||
onPanZoom?.(event, nextViewport);
|
||||
|
||||
zoomPanValues.panScrollTimeout = setTimeout(() => {
|
||||
@@ -233,7 +234,7 @@ export function createPanZoomEndHandler({
|
||||
|
||||
onDraggingChange(false);
|
||||
|
||||
if (onPanZoomEnd && viewChanged(zoomPanValues.prevViewport, event.transform)) {
|
||||
if (onPanZoomEnd) {
|
||||
const viewport = transformToViewport(event.transform);
|
||||
zoomPanValues.prevViewport = viewport;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export type FilterParams = {
|
||||
noWheelClassName: string;
|
||||
noPanClassName: string;
|
||||
lib: string;
|
||||
connectionInProgress: boolean;
|
||||
};
|
||||
|
||||
export function createFilter({
|
||||
@@ -25,10 +26,12 @@ export function createFilter({
|
||||
noWheelClassName,
|
||||
noPanClassName,
|
||||
lib,
|
||||
connectionInProgress,
|
||||
}: FilterParams) {
|
||||
return (event: any): boolean => {
|
||||
const zoomScroll = zoomActivationKeyPressed || zoomOnScroll;
|
||||
const pinchZoom = zoomOnPinch && event.ctrlKey;
|
||||
const isWheelEvent = event.type === 'wheel';
|
||||
|
||||
if (
|
||||
event.button === 1 &&
|
||||
@@ -48,20 +51,25 @@ export function createFilter({
|
||||
return false;
|
||||
}
|
||||
|
||||
// we want to disable pinch-zooming while making a connection
|
||||
if (connectionInProgress && !isWheelEvent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the target element is inside an element with the nowheel class, we prevent zooming
|
||||
if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') {
|
||||
if (isWrappedWithClass(event, noWheelClassName) && isWheelEvent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the target element is inside an element with the nopan class, we prevent panning
|
||||
if (
|
||||
isWrappedWithClass(event, noPanClassName) &&
|
||||
(event.type !== 'wheel' || (panOnScroll && event.type === 'wheel' && !zoomActivationKeyPressed))
|
||||
(!isWheelEvent || (panOnScroll && isWheelEvent && !zoomActivationKeyPressed))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') {
|
||||
if (!zoomOnPinch && event.ctrlKey && isWheelEvent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -71,7 +79,7 @@ export function createFilter({
|
||||
}
|
||||
|
||||
// when there is no scroll handling enabled, we prevent all wheel events
|
||||
if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') {
|
||||
if (!zoomScroll && !panOnScroll && !pinchZoom && isWheelEvent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -90,6 +98,6 @@ export function createFilter({
|
||||
(Array.isArray(panOnDrag) && panOnDrag.includes(event.button)) || !event.button || event.button <= 1;
|
||||
|
||||
// default filter for d3-zoom
|
||||
return (!event.ctrlKey || event.type === 'wheel') && buttonAllowed;
|
||||
return (!event.ctrlKey || isWheelEvent) && buttonAllowed;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,9 +6,6 @@ import { transition } from 'd3-transition';
|
||||
import { type D3SelectionInstance, type Viewport } from '../types';
|
||||
import { isMacOs } from '../utils';
|
||||
|
||||
export const viewChanged = (prevViewport: Viewport, eventViewport: any): boolean =>
|
||||
prevViewport.x !== eventViewport.x || prevViewport.y !== eventViewport.y || prevViewport.zoom !== eventViewport.k;
|
||||
|
||||
export const transformToViewport = (transform: ZoomTransform): Viewport => ({
|
||||
x: transform.x,
|
||||
y: transform.y,
|
||||
|
||||
@@ -104,6 +104,18 @@ function nodeToChildExtent(child: NodeBase, parent: NodeBase, nodeOrigin: NodeOr
|
||||
export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: XYResizerParams): XYResizerInstance {
|
||||
const selection = select(domNode);
|
||||
|
||||
let params = {
|
||||
controlDirection: getControlDirection('bottom-right'),
|
||||
boundaries: {
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
maxWidth: Number.MAX_VALUE,
|
||||
maxHeight: Number.MAX_VALUE,
|
||||
},
|
||||
resizeDirection: undefined as ResizeControlDirection | undefined,
|
||||
keepAspectRatio: false,
|
||||
};
|
||||
|
||||
function update({
|
||||
controlPosition,
|
||||
boundaries,
|
||||
@@ -117,7 +129,12 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
|
||||
let prevValues = { ...initPrevValues };
|
||||
let startValues = { ...initStartValues };
|
||||
|
||||
const controlDirection = getControlDirection(controlPosition);
|
||||
params = {
|
||||
boundaries,
|
||||
resizeDirection,
|
||||
keepAspectRatio,
|
||||
controlDirection: getControlDirection(controlPosition),
|
||||
};
|
||||
|
||||
let node: InternalNodeBase | undefined = undefined;
|
||||
let containerBounds: DOMRect | null = null;
|
||||
@@ -125,6 +142,8 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
|
||||
let parentNode: InternalNodeBase | undefined = undefined; // Needed to fix expandParent
|
||||
let parentExtent: CoordinateExtent | undefined = undefined;
|
||||
let childExtent: CoordinateExtent | undefined = undefined;
|
||||
// we only want to trigger onResizeEnd if onResize was actually called
|
||||
let resizeDetected = false;
|
||||
|
||||
const dragHandler = drag<HTMLDivElement, unknown>()
|
||||
.on('start', (event: ResizeDragEvent) => {
|
||||
@@ -210,16 +229,17 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues;
|
||||
const change: XYResizerChange = {};
|
||||
const nodeOrigin = node.origin ?? storeNodeOrigin;
|
||||
|
||||
const { width, height, x, y } = getDimensionsAfterResize(
|
||||
startValues,
|
||||
controlDirection,
|
||||
params.controlDirection,
|
||||
pointerPosition,
|
||||
boundaries,
|
||||
keepAspectRatio,
|
||||
params.boundaries,
|
||||
params.keepAspectRatio,
|
||||
nodeOrigin,
|
||||
parentExtent,
|
||||
childExtent
|
||||
@@ -262,9 +282,13 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
|
||||
|
||||
if (isWidthChange || isHeightChange) {
|
||||
change.width =
|
||||
isWidthChange && (!resizeDirection || resizeDirection === 'horizontal') ? width : prevValues.width;
|
||||
isWidthChange && (!params.resizeDirection || params.resizeDirection === 'horizontal')
|
||||
? width
|
||||
: prevValues.width;
|
||||
change.height =
|
||||
isHeightChange && (!resizeDirection || resizeDirection === 'vertical') ? height : prevValues.height;
|
||||
isHeightChange && (!params.resizeDirection || params.resizeDirection === 'vertical')
|
||||
? height
|
||||
: prevValues.height;
|
||||
prevValues.width = change.width;
|
||||
prevValues.height = change.height;
|
||||
}
|
||||
@@ -289,8 +313,8 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
|
||||
prevWidth,
|
||||
height: prevValues.height,
|
||||
prevHeight,
|
||||
affectsX: controlDirection.affectsX,
|
||||
affectsY: controlDirection.affectsY,
|
||||
affectsX: params.controlDirection.affectsX,
|
||||
affectsY: params.controlDirection.affectsY,
|
||||
});
|
||||
|
||||
const nextValues = { ...prevValues, direction };
|
||||
@@ -300,13 +324,20 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
|
||||
if (callResize === false) {
|
||||
return;
|
||||
}
|
||||
resizeDetected = true;
|
||||
|
||||
onResize?.(event, nextValues);
|
||||
onChange(change, childChanges);
|
||||
})
|
||||
.on('end', (event: ResizeDragEvent) => {
|
||||
if (!resizeDetected) {
|
||||
return;
|
||||
}
|
||||
|
||||
onResizeEnd?.(event, { ...prevValues });
|
||||
onEnd?.({ ...prevValues });
|
||||
|
||||
resizeDetected = false;
|
||||
});
|
||||
selection.call(dragHandler);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { D3DragEvent, SubjectPosition } from 'd3-drag';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @inline
|
||||
*/
|
||||
export type ResizeParams = {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -15,6 +19,7 @@ export type ResizeParamsWithDirection = ResizeParams & {
|
||||
* Used to determine the control line position of the NodeResizer
|
||||
*
|
||||
* @public
|
||||
* @inline
|
||||
*/
|
||||
export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right';
|
||||
|
||||
@@ -22,6 +27,7 @@ export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right';
|
||||
* Used to determine the control position of the NodeResizer
|
||||
*
|
||||
* @public
|
||||
* @inline
|
||||
*/
|
||||
export type ControlPosition = ControlLinePosition | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
||||
|
||||
@@ -38,6 +44,7 @@ export enum ResizeControlVariant {
|
||||
/**
|
||||
* The direction the user can resize the node.
|
||||
* @public
|
||||
* @inline
|
||||
*/
|
||||
export type ResizeControlDirection = 'horizontal' | 'vertical';
|
||||
|
||||
@@ -47,6 +54,12 @@ export const XY_RESIZER_LINE_POSITIONS: ControlLinePosition[] = ['top', 'right',
|
||||
type OnResizeHandler<Params = ResizeParams, Result = void> = (event: ResizeDragEvent, params: Params) => Result;
|
||||
export type ResizeDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
|
||||
|
||||
/**
|
||||
* Callback to determine if node should resize
|
||||
*
|
||||
* @inline
|
||||
* @public
|
||||
*/
|
||||
export type ShouldResize = OnResizeHandler<ResizeParamsWithDirection, boolean>;
|
||||
export type OnResizeStart = OnResizeHandler;
|
||||
export type OnResize = OnResizeHandler<ResizeParamsWithDirection>;
|
||||
|
||||
Reference in New Issue
Block a user