@@ -21,7 +21,7 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"zustand": "^4.3.1"
|
"zustand": "^4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cypress/skip-test": "^2.6.1",
|
"@cypress/skip-test": "^2.6.1",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ReactFlow, Background, BackgroundVariant, Node, Edge, SelectionMode } from '@xyflow/react';
|
import { ReactFlow, Background, BackgroundVariant, Node, Edge, SelectionMode, Controls, Panel } from '@xyflow/react';
|
||||||
|
|
||||||
const MULTI_SELECT_KEY = ['Meta', 'Shift'];
|
const MULTI_SELECT_KEY = ['Meta', 'Shift'];
|
||||||
|
|
||||||
@@ -21,6 +21,10 @@ const onPaneContextMenu = (e: any) => {
|
|||||||
|
|
||||||
const panOnDrag = [1, 2];
|
const panOnDrag = [1, 2];
|
||||||
|
|
||||||
|
const onMoveStart = (e: any) => console.log('move start', e);
|
||||||
|
const onMove = (e: any) => console.log('move', e);
|
||||||
|
const onMoveEnd = (e: any) => console.log('move end', e);
|
||||||
|
|
||||||
const BasicFlow = () => {
|
const BasicFlow = () => {
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
@@ -36,11 +40,15 @@ const BasicFlow = () => {
|
|||||||
fitView
|
fitView
|
||||||
selectNodesOnDrag={false}
|
selectNodesOnDrag={false}
|
||||||
onSelectionContextMenu={onPaneContextMenu}
|
onSelectionContextMenu={onPaneContextMenu}
|
||||||
|
onMoveStart={onMoveStart}
|
||||||
|
onMove={onMove}
|
||||||
|
onMoveEnd={onMoveEnd}
|
||||||
>
|
>
|
||||||
<Background variant={BackgroundVariant.Cross} />
|
<Background variant={BackgroundVariant.Cross} />
|
||||||
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
<Controls />
|
||||||
|
<Panel position="top-right">
|
||||||
<input type={'text'} placeholder={'name'} />
|
<input type={'text'} placeholder={'name'} />
|
||||||
</div>
|
</Panel>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,8 +16,17 @@
|
|||||||
],
|
],
|
||||||
"source": "src/index.ts",
|
"source": "src/index.ts",
|
||||||
"main": "dist/umd/index.js",
|
"main": "dist/umd/index.js",
|
||||||
"module": "dist/esm/index.js",
|
"module": "dist/esm/index.mjs",
|
||||||
"types": "dist/esm/index.d.ts",
|
"types": "dist/esm/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/esm/index.d.ts",
|
||||||
|
"import": "./dist/esm/index.mjs",
|
||||||
|
"require": "./dist/umd/index.js"
|
||||||
|
},
|
||||||
|
"./dist/base.css": "./dist/base.css",
|
||||||
|
"./dist/style.css": "./dist/style.css"
|
||||||
|
},
|
||||||
"sideEffects": [
|
"sideEffects": [
|
||||||
"*.css"
|
"*.css"
|
||||||
],
|
],
|
||||||
@@ -48,7 +57,7 @@
|
|||||||
"d3-drag": "^3.0.0",
|
"d3-drag": "^3.0.0",
|
||||||
"d3-selection": "^3.0.0",
|
"d3-selection": "^3.0.0",
|
||||||
"d3-zoom": "^3.0.0",
|
"d3-zoom": "^3.0.0",
|
||||||
"zustand": "^4.3.3"
|
"zustand": "^4.4.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=17",
|
"react": ">=17",
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ function MiniMap({
|
|||||||
zoomable = false,
|
zoomable = false,
|
||||||
ariaLabel = 'React Flow mini map',
|
ariaLabel = 'React Flow mini map',
|
||||||
inversePan,
|
inversePan,
|
||||||
zoomStep,
|
zoomStep = 10,
|
||||||
|
offsetScale = 5,
|
||||||
}: MiniMapProps) {
|
}: MiniMapProps) {
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const svg = useRef<SVGSVGElement>(null);
|
const svg = useRef<SVGSVGElement>(null);
|
||||||
@@ -70,7 +71,7 @@ function MiniMap({
|
|||||||
const viewScale = Math.max(scaledWidth, scaledHeight);
|
const viewScale = Math.max(scaledWidth, scaledHeight);
|
||||||
const viewWidth = viewScale * elementWidth;
|
const viewWidth = viewScale * elementWidth;
|
||||||
const viewHeight = viewScale * elementHeight;
|
const viewHeight = viewScale * elementHeight;
|
||||||
const offset = 5 * viewScale;
|
const offset = offsetScale * viewScale;
|
||||||
const x = boundingRect.x - (viewWidth - boundingRect.width) / 2 - offset;
|
const x = boundingRect.x - (viewWidth - boundingRect.width) / 2 - offset;
|
||||||
const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset;
|
const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset;
|
||||||
const width = viewWidth + offset * 2;
|
const width = viewWidth + offset * 2;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ function MiniMapNode({
|
|||||||
className,
|
className,
|
||||||
borderRadius,
|
borderRadius,
|
||||||
shapeRendering,
|
shapeRendering,
|
||||||
|
selected,
|
||||||
onClick,
|
onClick,
|
||||||
}: MiniMapNodeProps) {
|
}: MiniMapNodeProps) {
|
||||||
const { background, backgroundColor } = style || {};
|
const { background, backgroundColor } = style || {};
|
||||||
@@ -23,7 +24,7 @@ function MiniMapNode({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<rect
|
<rect
|
||||||
className={cc(['react-flow__minimap-node', className])}
|
className={cc(['react-flow__minimap-node', { selected }, className])}
|
||||||
x={x}
|
x={x}
|
||||||
y={y}
|
y={y}
|
||||||
rx={borderRadius}
|
rx={borderRadius}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ function MiniMapNodes({
|
|||||||
width={node.width!}
|
width={node.width!}
|
||||||
height={node.height!}
|
height={node.height!}
|
||||||
style={node.style}
|
style={node.style}
|
||||||
|
selected={!!node.selected}
|
||||||
className={nodeClassNameFunc(node)}
|
className={nodeClassNameFunc(node)}
|
||||||
color={nodeColorFunc(node)}
|
color={nodeColorFunc(node)}
|
||||||
borderRadius={nodeBorderRadius}
|
borderRadius={nodeBorderRadius}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export type MiniMapProps<NodeData = any> = Omit<HTMLAttributes<SVGSVGElement>, '
|
|||||||
ariaLabel?: string | null;
|
ariaLabel?: string | null;
|
||||||
inversePan?: boolean;
|
inversePan?: boolean;
|
||||||
zoomStep?: number;
|
zoomStep?: number;
|
||||||
|
offsetScale?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MiniMapNodes = Pick<
|
export type MiniMapNodes = Pick<
|
||||||
@@ -46,5 +47,6 @@ export type MiniMapNodeProps = {
|
|||||||
strokeColor: string;
|
strokeColor: string;
|
||||||
strokeWidth: number;
|
strokeWidth: number;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
|
selected: boolean;
|
||||||
onClick?: (event: MouseEvent, id: string) => void;
|
onClick?: (event: MouseEvent, id: string) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
|||||||
onMouseLeave={onEdgeMouseLeave}
|
onMouseLeave={onEdgeMouseLeave}
|
||||||
onKeyDown={isFocusable ? onKeyDown : undefined}
|
onKeyDown={isFocusable ? onKeyDown : undefined}
|
||||||
tabIndex={isFocusable ? 0 : undefined}
|
tabIndex={isFocusable ? 0 : undefined}
|
||||||
role={isFocusable ? 'button' : undefined}
|
role={isFocusable ? 'button' : 'img'}
|
||||||
data-testid={`rf__edge-${id}`}
|
data-testid={`rf__edge-${id}`}
|
||||||
aria-label={ariaLabel === null ? undefined : ariaLabel ? ariaLabel : `Edge from ${source} to ${target}`}
|
aria-label={ariaLabel === null ? undefined : ariaLabel ? ariaLabel : `Edge from ${source} to ${target}`}
|
||||||
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
|
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { MouseEvent, RefObject } from 'react';
|
|||||||
import type { StoreApi } from 'zustand';
|
import type { StoreApi } from 'zustand';
|
||||||
|
|
||||||
import type { Node, ReactFlowState } from '../../types';
|
import type { Node, ReactFlowState } from '../../types';
|
||||||
|
import { errorMessages } from '@xyflow/system';
|
||||||
|
|
||||||
export function getMouseHandler(
|
export function getMouseHandler(
|
||||||
id: string,
|
id: string,
|
||||||
@@ -34,9 +35,14 @@ export function handleNodeClick({
|
|||||||
unselect?: boolean;
|
unselect?: boolean;
|
||||||
nodeRef?: RefObject<HTMLDivElement>;
|
nodeRef?: RefObject<HTMLDivElement>;
|
||||||
}) {
|
}) {
|
||||||
const { addSelectedNodes, unselectNodesAndEdges, multiSelectionActive, nodes } = store.getState();
|
const { addSelectedNodes, unselectNodesAndEdges, multiSelectionActive, nodes, onError } = store.getState();
|
||||||
const node = nodes.find((n) => n.id === id)!;
|
const node = nodes.find((n) => n.id === id)!;
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
onError?.('012', errorMessages['error012'](id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
store.setState({ nodesSelectionActive: false });
|
store.setState({ nodesSelectionActive: false });
|
||||||
|
|
||||||
if (!node.selected) {
|
if (!node.selected) {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useRef, type FC, type PropsWithChildren } from 'react';
|
import { useRef, type FC, type PropsWithChildren } from 'react';
|
||||||
import { type StoreApi } from 'zustand';
|
import { type StoreApi } from 'zustand';
|
||||||
|
import { UseBoundStoreWithEqualityFn } from 'zustand/traditional';
|
||||||
|
|
||||||
import { Provider } from '../../contexts/RFStoreContext';
|
import { Provider } from '../../contexts/RFStoreContext';
|
||||||
import { createRFStore } from '../../store';
|
import { createRFStore } from '../../store';
|
||||||
import type { ReactFlowState } from '../../types';
|
import type { ReactFlowState } from '../../types';
|
||||||
|
|
||||||
const ReactFlowProvider: FC<PropsWithChildren<unknown>> = ({ children }) => {
|
const ReactFlowProvider: FC<PropsWithChildren<unknown>> = ({ children }) => {
|
||||||
const storeRef = useRef<StoreApi<ReactFlowState> | null>(null);
|
const storeRef = useRef<UseBoundStoreWithEqualityFn<StoreApi<ReactFlowState>> | null>(null);
|
||||||
|
|
||||||
if (!storeRef.current) {
|
if (!storeRef.current) {
|
||||||
storeRef.current = createRFStore();
|
storeRef.current = createRFStore();
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ import { useStore } from '../../hooks/useStore';
|
|||||||
import useVisibleEdges from '../../hooks/useVisibleEdges';
|
import useVisibleEdges from '../../hooks/useVisibleEdges';
|
||||||
import MarkerDefinitions from './MarkerDefinitions';
|
import MarkerDefinitions from './MarkerDefinitions';
|
||||||
import { GraphViewProps } from '../GraphView';
|
import { GraphViewProps } from '../GraphView';
|
||||||
import type { ReactFlowState } from '../../types';
|
import type { EdgeTypesWrapped, ReactFlowState } from '../../types';
|
||||||
|
|
||||||
type EdgeRendererProps = Pick<
|
type EdgeRendererProps = Pick<
|
||||||
GraphViewProps,
|
GraphViewProps,
|
||||||
| 'edgeTypes'
|
|
||||||
| 'onEdgeClick'
|
| 'onEdgeClick'
|
||||||
| 'onEdgeDoubleClick'
|
| 'onEdgeDoubleClick'
|
||||||
| 'defaultMarkerColor'
|
| 'defaultMarkerColor'
|
||||||
@@ -29,6 +28,7 @@ type EdgeRendererProps = Pick<
|
|||||||
| 'rfId'
|
| 'rfId'
|
||||||
| 'disableKeyboardA11y'
|
| 'disableKeyboardA11y'
|
||||||
> & {
|
> & {
|
||||||
|
edgeTypes: EdgeTypesWrapped;
|
||||||
elevateEdgesOnSelect: boolean;
|
elevateEdgesOnSelect: boolean;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import EdgeRenderer from '../EdgeRenderer';
|
|||||||
import ViewportWrapper from '../Viewport';
|
import ViewportWrapper from '../Viewport';
|
||||||
import useOnInitHandler from '../../hooks/useOnInitHandler';
|
import useOnInitHandler from '../../hooks/useOnInitHandler';
|
||||||
import ConnectionLine from '../../components/ConnectionLine';
|
import ConnectionLine from '../../components/ConnectionLine';
|
||||||
import type { EdgeTypesWrapped, NodeTypesWrapped, ReactFlowProps } from '../../types';
|
import type { NodeTypesWrapped, ReactFlowProps } from '../../types';
|
||||||
|
import { createNodeTypes } from '../NodeRenderer/utils';
|
||||||
|
import { createEdgeTypes } from '../EdgeRenderer/utils';
|
||||||
|
import { useNodeOrEdgeTypes } from './utils';
|
||||||
|
|
||||||
export type GraphViewProps = Omit<
|
export type GraphViewProps = Omit<
|
||||||
ReactFlowProps,
|
ReactFlowProps,
|
||||||
@@ -15,6 +18,8 @@ export type GraphViewProps = Omit<
|
|||||||
Required<
|
Required<
|
||||||
Pick<
|
Pick<
|
||||||
ReactFlowProps,
|
ReactFlowProps,
|
||||||
|
| 'nodeTypes'
|
||||||
|
| 'edgeTypes'
|
||||||
| 'selectionKeyCode'
|
| 'selectionKeyCode'
|
||||||
| 'deleteKeyCode'
|
| 'deleteKeyCode'
|
||||||
| 'multiSelectionKeyCode'
|
| 'multiSelectionKeyCode'
|
||||||
@@ -33,8 +38,6 @@ export type GraphViewProps = Omit<
|
|||||||
| 'nodeOrigin'
|
| 'nodeOrigin'
|
||||||
>
|
>
|
||||||
> & {
|
> & {
|
||||||
nodeTypes: NodeTypesWrapped;
|
|
||||||
edgeTypes: EdgeTypesWrapped;
|
|
||||||
rfId: string;
|
rfId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,6 +105,9 @@ const GraphView = ({
|
|||||||
nodeExtent,
|
nodeExtent,
|
||||||
rfId,
|
rfId,
|
||||||
}: GraphViewProps) => {
|
}: GraphViewProps) => {
|
||||||
|
const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes);
|
||||||
|
const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes);
|
||||||
|
|
||||||
useOnInitHandler(onInit);
|
useOnInitHandler(onInit);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -142,7 +148,7 @@ const GraphView = ({
|
|||||||
>
|
>
|
||||||
<ViewportWrapper>
|
<ViewportWrapper>
|
||||||
<EdgeRenderer
|
<EdgeRenderer
|
||||||
edgeTypes={edgeTypes}
|
edgeTypes={edgeTypesWrapped}
|
||||||
onEdgeClick={onEdgeClick}
|
onEdgeClick={onEdgeClick}
|
||||||
onEdgeDoubleClick={onEdgeDoubleClick}
|
onEdgeDoubleClick={onEdgeDoubleClick}
|
||||||
onEdgeUpdate={onEdgeUpdate}
|
onEdgeUpdate={onEdgeUpdate}
|
||||||
@@ -170,7 +176,7 @@ const GraphView = ({
|
|||||||
<div className="react-flow__edgelabel-renderer" />
|
<div className="react-flow__edgelabel-renderer" />
|
||||||
|
|
||||||
<NodeRenderer
|
<NodeRenderer
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypesWrapped}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
onNodeDoubleClick={onNodeDoubleClick}
|
onNodeDoubleClick={onNodeDoubleClick}
|
||||||
onNodeMouseEnter={onNodeMouseEnter}
|
onNodeMouseEnter={onNodeMouseEnter}
|
||||||
|
|||||||
+4
-2
@@ -1,22 +1,24 @@
|
|||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
import { errorMessages, devWarn } from '@xyflow/system';
|
import { errorMessages } from '@xyflow/system';
|
||||||
|
|
||||||
import { CreateEdgeTypes } from '../EdgeRenderer/utils';
|
import { CreateEdgeTypes } from '../EdgeRenderer/utils';
|
||||||
import { CreateNodeTypes } from '../NodeRenderer/utils';
|
import { CreateNodeTypes } from '../NodeRenderer/utils';
|
||||||
import type { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types';
|
import type { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types';
|
||||||
|
import { useStoreApi } from '../../hooks/useStore';
|
||||||
|
|
||||||
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypesWrapped;
|
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypesWrapped;
|
||||||
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: EdgeTypes, createTypes: CreateEdgeTypes): EdgeTypesWrapped;
|
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: EdgeTypes, createTypes: CreateEdgeTypes): EdgeTypesWrapped;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any {
|
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any {
|
||||||
const typesKeysRef = useRef<string[] | null>(null);
|
const typesKeysRef = useRef<string[] | null>(null);
|
||||||
|
const store = useStoreApi();
|
||||||
|
|
||||||
const typesParsed = useMemo(() => {
|
const typesParsed = useMemo(() => {
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
const typeKeys = Object.keys(nodeOrEdgeTypes);
|
const typeKeys = Object.keys(nodeOrEdgeTypes);
|
||||||
if (shallow(typesKeysRef.current, typeKeys)) {
|
if (shallow(typesKeysRef.current, typeKeys)) {
|
||||||
devWarn('002', errorMessages['error002']());
|
store.getState().onError?.('002', errorMessages['error002']());
|
||||||
}
|
}
|
||||||
|
|
||||||
typesKeysRef.current = typeKeys;
|
typesKeysRef.current = typeKeys;
|
||||||
@@ -6,11 +6,10 @@ import useVisibleNodes from '../../hooks/useVisibleNodes';
|
|||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
import { containerStyle } from '../../styles';
|
import { containerStyle } from '../../styles';
|
||||||
import { GraphViewProps } from '../GraphView';
|
import { GraphViewProps } from '../GraphView';
|
||||||
import type { ReactFlowState, WrapNodeProps } from '../../types';
|
import type { NodeTypesWrapped, ReactFlowState, WrapNodeProps } from '../../types';
|
||||||
|
|
||||||
type NodeRendererProps = Pick<
|
type NodeRendererProps = Pick<
|
||||||
GraphViewProps,
|
GraphViewProps,
|
||||||
| 'nodeTypes'
|
|
||||||
| 'onNodeClick'
|
| 'onNodeClick'
|
||||||
| 'onNodeDoubleClick'
|
| 'onNodeDoubleClick'
|
||||||
| 'onNodeMouseEnter'
|
| 'onNodeMouseEnter'
|
||||||
@@ -24,7 +23,9 @@ type NodeRendererProps = Pick<
|
|||||||
| 'disableKeyboardA11y'
|
| 'disableKeyboardA11y'
|
||||||
| 'nodeOrigin'
|
| 'nodeOrigin'
|
||||||
| 'nodeExtent'
|
| 'nodeExtent'
|
||||||
>;
|
> & {
|
||||||
|
nodeTypes: NodeTypesWrapped;
|
||||||
|
};
|
||||||
|
|
||||||
const selector = (s: ReactFlowState) => ({
|
const selector = (s: ReactFlowState) => ({
|
||||||
nodesDraggable: s.nodesDraggable,
|
nodesDraggable: s.nodesDraggable,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
infiniteExtent,
|
infiniteExtent,
|
||||||
type NodeOrigin,
|
type NodeOrigin,
|
||||||
type Viewport,
|
type Viewport,
|
||||||
|
isMacOs,
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import Attribution from '../../components/Attribution';
|
import Attribution from '../../components/Attribution';
|
||||||
@@ -19,19 +20,9 @@ import GroupNode from '../../components/Nodes/GroupNode';
|
|||||||
import SelectionListener from '../../components/SelectionListener';
|
import SelectionListener from '../../components/SelectionListener';
|
||||||
import StoreUpdater from '../../components/StoreUpdater';
|
import StoreUpdater from '../../components/StoreUpdater';
|
||||||
import A11yDescriptions from '../../components/A11yDescriptions';
|
import A11yDescriptions from '../../components/A11yDescriptions';
|
||||||
import { createEdgeTypes } from '../EdgeRenderer/utils';
|
|
||||||
import { createNodeTypes } from '../NodeRenderer/utils';
|
|
||||||
import GraphView from '../GraphView';
|
import GraphView from '../GraphView';
|
||||||
import Wrapper from './Wrapper';
|
import Wrapper from './Wrapper';
|
||||||
import { useNodeOrEdgeTypes } from './utils';
|
import type { EdgeTypes, NodeTypes, ReactFlowProps, ReactFlowRefType } from '../../types';
|
||||||
import type {
|
|
||||||
EdgeTypes,
|
|
||||||
EdgeTypesWrapped,
|
|
||||||
NodeTypes,
|
|
||||||
NodeTypesWrapped,
|
|
||||||
ReactFlowProps,
|
|
||||||
ReactFlowRefType,
|
|
||||||
} from '../../types';
|
|
||||||
|
|
||||||
const defaultNodeTypes: NodeTypes = {
|
const defaultNodeTypes: NodeTypes = {
|
||||||
input: InputNode,
|
input: InputNode,
|
||||||
@@ -108,8 +99,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
|||||||
selectionOnDrag = false,
|
selectionOnDrag = false,
|
||||||
selectionMode = SelectionMode.Full,
|
selectionMode = SelectionMode.Full,
|
||||||
panActivationKeyCode = 'Space',
|
panActivationKeyCode = 'Space',
|
||||||
multiSelectionKeyCode = 'Meta',
|
multiSelectionKeyCode = isMacOs() ? 'Meta' : 'Control',
|
||||||
zoomActivationKeyCode = 'Meta',
|
zoomActivationKeyCode = isMacOs() ? 'Meta' : 'Control',
|
||||||
snapToGrid = false,
|
snapToGrid = false,
|
||||||
snapGrid = initSnapGrid,
|
snapGrid = initSnapGrid,
|
||||||
onlyRenderVisibleElements = false,
|
onlyRenderVisibleElements = false,
|
||||||
@@ -176,8 +167,6 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
|||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes) as NodeTypesWrapped;
|
|
||||||
const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes) as EdgeTypesWrapped;
|
|
||||||
const rfId = id || '1';
|
const rfId = id || '1';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -199,8 +188,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
|||||||
onNodeMouseLeave={onNodeMouseLeave}
|
onNodeMouseLeave={onNodeMouseLeave}
|
||||||
onNodeContextMenu={onNodeContextMenu}
|
onNodeContextMenu={onNodeContextMenu}
|
||||||
onNodeDoubleClick={onNodeDoubleClick}
|
onNodeDoubleClick={onNodeDoubleClick}
|
||||||
nodeTypes={nodeTypesWrapped}
|
nodeTypes={nodeTypes}
|
||||||
edgeTypes={edgeTypesWrapped}
|
edgeTypes={edgeTypes}
|
||||||
connectionLineType={connectionLineType}
|
connectionLineType={connectionLineType}
|
||||||
connectionLineStyle={connectionLineStyle}
|
connectionLineStyle={connectionLineStyle}
|
||||||
connectionLineComponent={connectionLineComponent}
|
connectionLineComponent={connectionLineComponent}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useContext, useMemo } from 'react';
|
import { useContext, useMemo } from 'react';
|
||||||
import { useStore as useZustandStore, type StoreApi } from 'zustand';
|
import { useStoreWithEqualityFn as useZustandStore } from 'zustand/traditional';
|
||||||
|
import type { StoreApi } from 'zustand';
|
||||||
import { errorMessages } from '@xyflow/system';
|
import { errorMessages } from '@xyflow/system';
|
||||||
|
|
||||||
import StoreContext from '../contexts/RFStoreContext';
|
import StoreContext from '../contexts/RFStoreContext';
|
||||||
|
|||||||
+248
-245
@@ -1,4 +1,4 @@
|
|||||||
import { createStore } from 'zustand';
|
import { createWithEqualityFn } from 'zustand/traditional';
|
||||||
import {
|
import {
|
||||||
clampPosition,
|
clampPosition,
|
||||||
fitView as fitViewSystem,
|
fitView as fitViewSystem,
|
||||||
@@ -25,261 +25,264 @@ import type {
|
|||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
const createRFStore = () =>
|
const createRFStore = () =>
|
||||||
createStore<ReactFlowState>((set, get) => ({
|
createWithEqualityFn<ReactFlowState>(
|
||||||
...initialState,
|
(set, get) => ({
|
||||||
setNodes: (nodes: Node[]) => {
|
...initialState,
|
||||||
const { nodes: storeNodes, nodeOrigin, elevateNodesOnSelect } = get();
|
setNodes: (nodes: Node[]) => {
|
||||||
const nextNodes = updateNodes(nodes, storeNodes, { nodeOrigin, elevateNodesOnSelect });
|
const { nodes: storeNodes, nodeOrigin, elevateNodesOnSelect } = get();
|
||||||
|
const nextNodes = updateNodes(nodes, storeNodes, { nodeOrigin, elevateNodesOnSelect });
|
||||||
|
|
||||||
set({ nodes: nextNodes });
|
set({ nodes: nextNodes });
|
||||||
},
|
},
|
||||||
getNodes: () => {
|
getNodes: () => {
|
||||||
return get().nodes;
|
return get().nodes;
|
||||||
},
|
},
|
||||||
setEdges: (edges: Edge[]) => {
|
setEdges: (edges: Edge[]) => {
|
||||||
const { defaultEdgeOptions = {} } = get();
|
const { defaultEdgeOptions = {} } = get();
|
||||||
set({ edges: edges.map((e) => ({ ...defaultEdgeOptions, ...e })) });
|
set({ edges: edges.map((e) => ({ ...defaultEdgeOptions, ...e })) });
|
||||||
},
|
},
|
||||||
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => {
|
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => {
|
||||||
const hasDefaultNodes = typeof nodes !== 'undefined';
|
const hasDefaultNodes = typeof nodes !== 'undefined';
|
||||||
const hasDefaultEdges = typeof edges !== 'undefined';
|
const hasDefaultEdges = typeof edges !== 'undefined';
|
||||||
|
|
||||||
const nextNodes = hasDefaultNodes
|
const nextNodes = hasDefaultNodes
|
||||||
? updateNodes(nodes, [], {
|
? updateNodes(nodes, [], {
|
||||||
nodeOrigin: get().nodeOrigin,
|
nodeOrigin: get().nodeOrigin,
|
||||||
elevateNodesOnSelect: get().elevateNodesOnSelect,
|
elevateNodesOnSelect: get().elevateNodesOnSelect,
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
const nextEdges = hasDefaultEdges ? edges : [];
|
const nextEdges = hasDefaultEdges ? edges : [];
|
||||||
|
|
||||||
set({ nodes: nextNodes, edges: nextEdges, hasDefaultNodes, hasDefaultEdges });
|
set({ nodes: nextNodes, edges: nextEdges, hasDefaultNodes, hasDefaultEdges });
|
||||||
},
|
},
|
||||||
updateNodeDimensions: (updates) => {
|
updateNodeDimensions: (updates) => {
|
||||||
const { onNodesChange, fitView, nodes, fitViewOnInit, fitViewDone, fitViewOnInitOptions, domNode, nodeOrigin } =
|
const { onNodesChange, fitView, nodes, fitViewOnInit, fitViewDone, fitViewOnInitOptions, domNode, nodeOrigin } =
|
||||||
get();
|
get();
|
||||||
const changes: NodeDimensionChange[] = [];
|
const changes: NodeDimensionChange[] = [];
|
||||||
|
|
||||||
const updatedNodes = updateNodeDimensionsSystem(
|
const updatedNodes = updateNodeDimensionsSystem(
|
||||||
updates,
|
updates,
|
||||||
nodes,
|
nodes,
|
||||||
domNode,
|
domNode,
|
||||||
nodeOrigin,
|
nodeOrigin,
|
||||||
(id: string, dimensions: Dimensions) => {
|
(id: string, dimensions: Dimensions) => {
|
||||||
changes.push({
|
changes.push({
|
||||||
id: id,
|
id: id,
|
||||||
type: 'dimensions',
|
type: 'dimensions',
|
||||||
dimensions,
|
dimensions,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updatedNodes) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
if (!updatedNodes) {
|
const nextNodes = updateAbsolutePositions(updatedNodes, nodeOrigin);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextNodes = updateAbsolutePositions(updatedNodes, nodeOrigin);
|
const nextFitViewDone =
|
||||||
|
fitViewDone ||
|
||||||
|
(fitViewOnInit &&
|
||||||
|
fitView({
|
||||||
|
...fitViewOnInitOptions,
|
||||||
|
nodes: nextNodes,
|
||||||
|
}));
|
||||||
|
|
||||||
const nextFitViewDone =
|
set({ nodes: nextNodes, fitViewDone: nextFitViewDone });
|
||||||
fitViewDone ||
|
|
||||||
(fitViewOnInit &&
|
|
||||||
fitView({
|
|
||||||
...fitViewOnInitOptions,
|
|
||||||
nodes: nextNodes,
|
|
||||||
}));
|
|
||||||
|
|
||||||
set({ nodes: nextNodes, fitViewDone: nextFitViewDone });
|
if (changes?.length > 0) {
|
||||||
|
onNodesChange?.(changes);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateNodePositions: (nodeDragItems, positionChanged = true, dragging = false) => {
|
||||||
|
const changes = nodeDragItems.map((node) => {
|
||||||
|
const change: NodePositionChange = {
|
||||||
|
id: node.id,
|
||||||
|
type: 'position',
|
||||||
|
dragging,
|
||||||
|
};
|
||||||
|
|
||||||
if (changes?.length > 0) {
|
if (positionChanged) {
|
||||||
onNodesChange?.(changes);
|
change.positionAbsolute = node.positionAbsolute;
|
||||||
}
|
change.position = node.position;
|
||||||
},
|
}
|
||||||
updateNodePositions: (nodeDragItems, positionChanged = true, dragging = false) => {
|
|
||||||
const changes = nodeDragItems.map((node) => {
|
return change;
|
||||||
const change: NodePositionChange = {
|
});
|
||||||
id: node.id,
|
|
||||||
type: 'position',
|
get().triggerNodeChanges(changes);
|
||||||
dragging,
|
},
|
||||||
|
|
||||||
|
triggerNodeChanges: (changes) => {
|
||||||
|
const { onNodesChange, nodes, hasDefaultNodes, nodeOrigin, elevateNodesOnSelect } = get();
|
||||||
|
|
||||||
|
if (changes?.length) {
|
||||||
|
if (hasDefaultNodes) {
|
||||||
|
const updatedNodes = applyNodeChanges(changes, nodes);
|
||||||
|
const nextNodes = updateNodes(updatedNodes, nodes, {
|
||||||
|
nodeOrigin,
|
||||||
|
elevateNodesOnSelect,
|
||||||
|
});
|
||||||
|
set({ nodes: nextNodes });
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodesChange?.(changes);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addSelectedNodes: (selectedNodeIds) => {
|
||||||
|
const { multiSelectionActive, edges, nodes } = get();
|
||||||
|
let changedNodes: NodeSelectionChange[];
|
||||||
|
let changedEdges: EdgeSelectionChange[] | null = null;
|
||||||
|
|
||||||
|
if (multiSelectionActive) {
|
||||||
|
changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)) as NodeSelectionChange[];
|
||||||
|
} else {
|
||||||
|
changedNodes = getSelectionChanges(nodes, selectedNodeIds);
|
||||||
|
changedEdges = getSelectionChanges(edges, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNodesAndEdgesSelections({
|
||||||
|
changedNodes,
|
||||||
|
changedEdges,
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addSelectedEdges: (selectedEdgeIds) => {
|
||||||
|
const { multiSelectionActive, edges, nodes } = get();
|
||||||
|
let changedEdges: EdgeSelectionChange[];
|
||||||
|
let changedNodes: NodeSelectionChange[] | null = null;
|
||||||
|
|
||||||
|
if (multiSelectionActive) {
|
||||||
|
changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)) as EdgeSelectionChange[];
|
||||||
|
} else {
|
||||||
|
changedEdges = getSelectionChanges(edges, selectedEdgeIds);
|
||||||
|
changedNodes = getSelectionChanges(nodes, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNodesAndEdgesSelections({
|
||||||
|
changedNodes,
|
||||||
|
changedEdges,
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => {
|
||||||
|
const { edges: storeEdges, nodes: storeNodes } = get();
|
||||||
|
const nodesToUnselect = nodes ? nodes : storeNodes;
|
||||||
|
const edgesToUnselect = edges ? edges : storeEdges;
|
||||||
|
|
||||||
|
const changedNodes = nodesToUnselect.map((n) => {
|
||||||
|
n.selected = false;
|
||||||
|
return createSelectionChange(n.id, false);
|
||||||
|
}) as NodeSelectionChange[];
|
||||||
|
const changedEdges = edgesToUnselect.map((edge) =>
|
||||||
|
createSelectionChange(edge.id, false)
|
||||||
|
) as EdgeSelectionChange[];
|
||||||
|
|
||||||
|
updateNodesAndEdgesSelections({
|
||||||
|
changedNodes,
|
||||||
|
changedEdges,
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setMinZoom: (minZoom) => {
|
||||||
|
const { panZoom, maxZoom } = get();
|
||||||
|
panZoom?.setScaleExtent([minZoom, maxZoom]);
|
||||||
|
|
||||||
|
set({ minZoom });
|
||||||
|
},
|
||||||
|
setMaxZoom: (maxZoom) => {
|
||||||
|
const { panZoom, minZoom } = get();
|
||||||
|
panZoom?.setScaleExtent([minZoom, maxZoom]);
|
||||||
|
|
||||||
|
set({ maxZoom });
|
||||||
|
},
|
||||||
|
setTranslateExtent: (translateExtent) => {
|
||||||
|
get().panZoom?.setTranslateExtent(translateExtent);
|
||||||
|
|
||||||
|
set({ translateExtent });
|
||||||
|
},
|
||||||
|
resetSelectedElements: () => {
|
||||||
|
const { edges, nodes } = get();
|
||||||
|
|
||||||
|
const nodesToUnselect = nodes
|
||||||
|
.filter((e) => e.selected)
|
||||||
|
.map((n) => createSelectionChange(n.id, false)) as NodeSelectionChange[];
|
||||||
|
const edgesToUnselect = edges
|
||||||
|
.filter((e) => e.selected)
|
||||||
|
.map((e) => createSelectionChange(e.id, false)) as EdgeSelectionChange[];
|
||||||
|
|
||||||
|
updateNodesAndEdgesSelections({
|
||||||
|
changedNodes: nodesToUnselect,
|
||||||
|
changedEdges: edgesToUnselect,
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setNodeExtent: (nodeExtent) => {
|
||||||
|
const { nodes } = get();
|
||||||
|
|
||||||
|
set({
|
||||||
|
nodeExtent,
|
||||||
|
nodes: nodes.map((node) => {
|
||||||
|
const positionAbsolute = clampPosition(node.position, nodeExtent);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
positionAbsolute,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
panBy: (delta): boolean => {
|
||||||
|
const { transform, width, height, panZoom, translateExtent } = get();
|
||||||
|
return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
|
||||||
|
},
|
||||||
|
fitView: (options?: FitViewOptions): boolean => {
|
||||||
|
const { panZoom, nodes, width, height, minZoom, maxZoom, nodeOrigin } = get();
|
||||||
|
const fitViewNodes = options?.nodes || nodes;
|
||||||
|
|
||||||
|
if (!panZoom) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fitViewSystem(
|
||||||
|
{
|
||||||
|
nodes: fitViewNodes as Node[],
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
panZoom,
|
||||||
|
minZoom,
|
||||||
|
maxZoom,
|
||||||
|
nodeOrigin,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cancelConnection: () =>
|
||||||
|
set({
|
||||||
|
connectionStatus: initialState.connectionStatus,
|
||||||
|
connectionStartHandle: initialState.connectionStartHandle,
|
||||||
|
connectionEndHandle: initialState.connectionEndHandle,
|
||||||
|
}),
|
||||||
|
updateConnection: (params) => {
|
||||||
|
const { connectionStatus, connectionStartHandle, connectionEndHandle, connectionPosition } = get();
|
||||||
|
|
||||||
|
const currentConnection = {
|
||||||
|
connectionPosition: params.connectionPosition ?? connectionPosition,
|
||||||
|
connectionStatus: params.connectionStatus ?? connectionStatus,
|
||||||
|
connectionStartHandle: params.connectionStartHandle ?? connectionStartHandle,
|
||||||
|
connectionEndHandle: params.connectionEndHandle ?? connectionEndHandle,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (positionChanged) {
|
set(currentConnection);
|
||||||
change.positionAbsolute = node.positionAbsolute;
|
},
|
||||||
change.position = node.position;
|
reset: () => set({ ...initialState }),
|
||||||
}
|
}),
|
||||||
|
Object.is
|
||||||
return change;
|
);
|
||||||
});
|
|
||||||
|
|
||||||
get().triggerNodeChanges(changes);
|
|
||||||
},
|
|
||||||
|
|
||||||
triggerNodeChanges: (changes) => {
|
|
||||||
const { onNodesChange, nodes, hasDefaultNodes, nodeOrigin, elevateNodesOnSelect } = get();
|
|
||||||
|
|
||||||
if (changes?.length) {
|
|
||||||
if (hasDefaultNodes) {
|
|
||||||
const updatedNodes = applyNodeChanges(changes, nodes);
|
|
||||||
const nextNodes = updateNodes(updatedNodes, nodes, {
|
|
||||||
nodeOrigin,
|
|
||||||
elevateNodesOnSelect,
|
|
||||||
});
|
|
||||||
set({ nodes: nextNodes });
|
|
||||||
}
|
|
||||||
|
|
||||||
onNodesChange?.(changes);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addSelectedNodes: (selectedNodeIds) => {
|
|
||||||
const { multiSelectionActive, edges, nodes } = get();
|
|
||||||
let changedNodes: NodeSelectionChange[];
|
|
||||||
let changedEdges: EdgeSelectionChange[] | null = null;
|
|
||||||
|
|
||||||
if (multiSelectionActive) {
|
|
||||||
changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)) as NodeSelectionChange[];
|
|
||||||
} else {
|
|
||||||
changedNodes = getSelectionChanges(nodes, selectedNodeIds);
|
|
||||||
changedEdges = getSelectionChanges(edges, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateNodesAndEdgesSelections({
|
|
||||||
changedNodes,
|
|
||||||
changedEdges,
|
|
||||||
get,
|
|
||||||
set,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
addSelectedEdges: (selectedEdgeIds) => {
|
|
||||||
const { multiSelectionActive, edges, nodes } = get();
|
|
||||||
let changedEdges: EdgeSelectionChange[];
|
|
||||||
let changedNodes: NodeSelectionChange[] | null = null;
|
|
||||||
|
|
||||||
if (multiSelectionActive) {
|
|
||||||
changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)) as EdgeSelectionChange[];
|
|
||||||
} else {
|
|
||||||
changedEdges = getSelectionChanges(edges, selectedEdgeIds);
|
|
||||||
changedNodes = getSelectionChanges(nodes, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateNodesAndEdgesSelections({
|
|
||||||
changedNodes,
|
|
||||||
changedEdges,
|
|
||||||
get,
|
|
||||||
set,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => {
|
|
||||||
const { edges: storeEdges, nodes: storeNodes } = get();
|
|
||||||
const nodesToUnselect = nodes ? nodes : storeNodes;
|
|
||||||
const edgesToUnselect = edges ? edges : storeEdges;
|
|
||||||
|
|
||||||
const changedNodes = nodesToUnselect.map((n) => {
|
|
||||||
n.selected = false;
|
|
||||||
return createSelectionChange(n.id, false);
|
|
||||||
}) as NodeSelectionChange[];
|
|
||||||
const changedEdges = edgesToUnselect.map((edge) =>
|
|
||||||
createSelectionChange(edge.id, false)
|
|
||||||
) as EdgeSelectionChange[];
|
|
||||||
|
|
||||||
updateNodesAndEdgesSelections({
|
|
||||||
changedNodes,
|
|
||||||
changedEdges,
|
|
||||||
get,
|
|
||||||
set,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
setMinZoom: (minZoom) => {
|
|
||||||
const { panZoom, maxZoom } = get();
|
|
||||||
panZoom?.setScaleExtent([minZoom, maxZoom]);
|
|
||||||
|
|
||||||
set({ minZoom });
|
|
||||||
},
|
|
||||||
setMaxZoom: (maxZoom) => {
|
|
||||||
const { panZoom, minZoom } = get();
|
|
||||||
panZoom?.setScaleExtent([minZoom, maxZoom]);
|
|
||||||
|
|
||||||
set({ maxZoom });
|
|
||||||
},
|
|
||||||
setTranslateExtent: (translateExtent) => {
|
|
||||||
get().panZoom?.setTranslateExtent(translateExtent);
|
|
||||||
|
|
||||||
set({ translateExtent });
|
|
||||||
},
|
|
||||||
resetSelectedElements: () => {
|
|
||||||
const { edges, nodes } = get();
|
|
||||||
|
|
||||||
const nodesToUnselect = nodes
|
|
||||||
.filter((e) => e.selected)
|
|
||||||
.map((n) => createSelectionChange(n.id, false)) as NodeSelectionChange[];
|
|
||||||
const edgesToUnselect = edges
|
|
||||||
.filter((e) => e.selected)
|
|
||||||
.map((e) => createSelectionChange(e.id, false)) as EdgeSelectionChange[];
|
|
||||||
|
|
||||||
updateNodesAndEdgesSelections({
|
|
||||||
changedNodes: nodesToUnselect,
|
|
||||||
changedEdges: edgesToUnselect,
|
|
||||||
get,
|
|
||||||
set,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
setNodeExtent: (nodeExtent) => {
|
|
||||||
const { nodes } = get();
|
|
||||||
|
|
||||||
set({
|
|
||||||
nodeExtent,
|
|
||||||
nodes: nodes.map((node) => {
|
|
||||||
const positionAbsolute = clampPosition(node.position, nodeExtent);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
positionAbsolute,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
panBy: (delta): boolean => {
|
|
||||||
const { transform, width, height, panZoom, translateExtent } = get();
|
|
||||||
return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
|
|
||||||
},
|
|
||||||
fitView: (options?: FitViewOptions): boolean => {
|
|
||||||
const { panZoom, nodes, width, height, minZoom, maxZoom, nodeOrigin } = get();
|
|
||||||
const fitViewNodes = options?.nodes || nodes;
|
|
||||||
|
|
||||||
if (!panZoom) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fitViewSystem(
|
|
||||||
{
|
|
||||||
nodes: fitViewNodes as Node[],
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
panZoom,
|
|
||||||
minZoom,
|
|
||||||
maxZoom,
|
|
||||||
nodeOrigin,
|
|
||||||
},
|
|
||||||
options
|
|
||||||
);
|
|
||||||
},
|
|
||||||
cancelConnection: () =>
|
|
||||||
set({
|
|
||||||
connectionStatus: initialState.connectionStatus,
|
|
||||||
connectionStartHandle: initialState.connectionStartHandle,
|
|
||||||
connectionEndHandle: initialState.connectionEndHandle,
|
|
||||||
}),
|
|
||||||
updateConnection: (params) => {
|
|
||||||
const { connectionStatus, connectionStartHandle, connectionEndHandle, connectionPosition } = get();
|
|
||||||
|
|
||||||
const currentConnection = {
|
|
||||||
connectionPosition: params.connectionPosition ?? connectionPosition,
|
|
||||||
connectionStatus: params.connectionStatus ?? connectionStatus,
|
|
||||||
connectionStartHandle: params.connectionStartHandle ?? connectionStartHandle,
|
|
||||||
connectionEndHandle: params.connectionEndHandle ?? connectionEndHandle,
|
|
||||||
};
|
|
||||||
|
|
||||||
set(currentConnection);
|
|
||||||
},
|
|
||||||
reset: () => set({ ...initialState }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export { createRFStore };
|
export { createRFStore };
|
||||||
|
|||||||
@@ -39,8 +39,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^2.1.0",
|
"@sveltejs/adapter-auto": "^2.1.0",
|
||||||
"@sveltejs/kit": "^1.20.5",
|
"@sveltejs/kit": "^1.22.6",
|
||||||
"@sveltejs/package": "^2.1.0",
|
"@sveltejs/package": "^2.2.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
||||||
"@typescript-eslint/parser": "^5.60.0",
|
"@typescript-eslint/parser": "^5.60.0",
|
||||||
"eslint": "^8.43.0",
|
"eslint": "^8.43.0",
|
||||||
@@ -48,9 +48,9 @@
|
|||||||
"eslint-plugin-svelte": "^2.31.1",
|
"eslint-plugin-svelte": "^2.31.1",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"prettier-plugin-svelte": "^2.10.1",
|
"prettier-plugin-svelte": "^2.10.1",
|
||||||
"svelte": "^4.0.0",
|
"svelte": "^4.2.0",
|
||||||
"svelte-check": "^3.4.4",
|
"svelte-check": "^3.5.0",
|
||||||
"svelte-eslint-parser": "^0.31.0",
|
"svelte-eslint-parser": "^0.32.2",
|
||||||
"tslib": "^2.5.3",
|
"tslib": "^2.5.3",
|
||||||
"typescript": "5.1.3",
|
"typescript": "5.1.3",
|
||||||
"vite": "^4.3.9"
|
"vite": "^4.3.9"
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
export let targetY: $$Props['targetY'];
|
export let targetY: $$Props['targetY'];
|
||||||
export let sourcePosition: $$Props['sourcePosition'];
|
export let sourcePosition: $$Props['sourcePosition'];
|
||||||
export let targetPosition: $$Props['targetPosition'];
|
export let targetPosition: $$Props['targetPosition'];
|
||||||
|
export let ariaLabel: $$Props['ariaLabel'] = undefined;
|
||||||
// @ todo: support edge updates
|
// @ todo: support edge updates
|
||||||
|
|
||||||
const { edges, edgeTypes, flowId, addSelectedEdges } = useStore();
|
const { edges, edgeTypes, flowId, addSelectedEdges } = useStore();
|
||||||
@@ -51,7 +52,19 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<g class="svelte-flow__edge" class:animated class:selected data-id={id} on:click={onClick}>
|
<g
|
||||||
|
class="svelte-flow__edge"
|
||||||
|
class:animated
|
||||||
|
class:selected
|
||||||
|
data-id={id}
|
||||||
|
on:click={onClick}
|
||||||
|
aria-label={ariaLabel === null
|
||||||
|
? undefined
|
||||||
|
: ariaLabel
|
||||||
|
? ariaLabel
|
||||||
|
: `Edge from ${source} to ${target}`}
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={edgeComponent}
|
this={edgeComponent}
|
||||||
{id}
|
{id}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export type EdgeLayouted = Pick<
|
|||||||
| 'markerEnd'
|
| 'markerEnd'
|
||||||
| 'sourceHandle'
|
| 'sourceHandle'
|
||||||
| 'targetHandle'
|
| 'targetHandle'
|
||||||
|
| 'ariaLabel'
|
||||||
> &
|
> &
|
||||||
EdgePosition & {
|
EdgePosition & {
|
||||||
sourceHandleId?: string | null;
|
sourceHandleId?: string | null;
|
||||||
|
|||||||
@@ -16,8 +16,15 @@
|
|||||||
],
|
],
|
||||||
"source": "src/index.ts",
|
"source": "src/index.ts",
|
||||||
"main": "dist/umd/index.js",
|
"main": "dist/umd/index.js",
|
||||||
"module": "dist/esm/index.js",
|
"module": "dist/esm/index.mjs",
|
||||||
"types": "dist/esm/index.d.ts",
|
"types": "dist/esm/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/esm/index.d.ts",
|
||||||
|
"import": "./dist/esm/index.mjs",
|
||||||
|
"require": "./dist/umd/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export const errorMessages = {
|
|||||||
}", edge id: ${id}.`,
|
}", edge id: ${id}.`,
|
||||||
error010: () => 'Handle: No node id found. Make sure to only use a Handle inside a custom Node.',
|
error010: () => 'Handle: No node id found. Make sure to only use a Handle inside a custom Node.',
|
||||||
error011: (edgeType: string) => `Edge type "${edgeType}" not found. Using fallback type "default".`,
|
error011: (edgeType: string) => `Edge type "${edgeType}" not found. Using fallback type "default".`,
|
||||||
|
error012: (id: string) =>
|
||||||
|
`Node with id "${id}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const internalsSymbol = Symbol.for('internals');
|
export const internalsSymbol = Symbol.for('internals');
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ function getPoints({
|
|||||||
|
|
||||||
let points: XYPosition[] = [];
|
let points: XYPosition[] = [];
|
||||||
let centerX, centerY;
|
let centerX, centerY;
|
||||||
|
const sourceGapOffset = { x: 0, y: 0 };
|
||||||
|
const targetGapOffset = { x: 0, y: 0 };
|
||||||
|
|
||||||
const [defaultCenterX, defaultCenterY, defaultOffsetX, defaultOffsetY] = getEdgeCenter({
|
const [defaultCenterX, defaultCenterY, defaultOffsetX, defaultOffsetY] = getEdgeCenter({
|
||||||
sourceX: source.x,
|
sourceX: source.x,
|
||||||
sourceY: source.y,
|
sourceY: source.y,
|
||||||
@@ -111,6 +114,20 @@ function getPoints({
|
|||||||
points = sourceDir.y === currDir ? sourceTarget : targetSource;
|
points = sourceDir.y === currDir ? sourceTarget : targetSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sourcePosition === targetPosition) {
|
||||||
|
const diff = Math.abs(source[dirAccessor] - target[dirAccessor]);
|
||||||
|
|
||||||
|
// if an edge goes from right to right for example (sourcePosition === targetPosition) and the distance between source.x and target.x is less than the offset, the added point and the gapped source/target will overlap. This leads to a weird edge path. To avoid this we add a gapOffset to the source/target
|
||||||
|
if (diff <= offset) {
|
||||||
|
const gapOffset = Math.min(offset - 1, offset - diff);
|
||||||
|
if (sourceDir[dirAccessor] === currDir) {
|
||||||
|
sourceGapOffset[dirAccessor] = gapOffset;
|
||||||
|
} else {
|
||||||
|
targetGapOffset[dirAccessor] = gapOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// these are conditions for handling mixed handle positions like Right -> Bottom for example
|
// these are conditions for handling mixed handle positions like Right -> Bottom for example
|
||||||
if (sourcePosition !== targetPosition) {
|
if (sourcePosition !== targetPosition) {
|
||||||
const dirAccessorOpposite = dirAccessor === 'x' ? 'y' : 'x';
|
const dirAccessorOpposite = dirAccessor === 'x' ? 'y' : 'x';
|
||||||
@@ -126,11 +143,28 @@ function getPoints({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
centerX = points[0].x;
|
const sourceGapPoint = { x: sourceGapped.x - sourceGapOffset.x, y: sourceGapped.y - sourceGapOffset.y };
|
||||||
centerY = points[0].y;
|
const targetGapPoint = { x: targetGapped.x - targetGapOffset.x, y: targetGapped.y - targetGapOffset.y };
|
||||||
|
const maxXDistance = Math.max(Math.abs(sourceGapPoint.x - points[0].x), Math.abs(targetGapPoint.x - points[0].x));
|
||||||
|
const maxYDistance = Math.max(Math.abs(sourceGapPoint.y - points[0].y), Math.abs(targetGapPoint.y - points[0].y));
|
||||||
|
|
||||||
|
// we want to place the label on the longest segment of the edge
|
||||||
|
if (maxXDistance >= maxYDistance) {
|
||||||
|
centerX = (sourceGapPoint.x + targetGapPoint.x) / 2;
|
||||||
|
centerY = points[0].y;
|
||||||
|
} else {
|
||||||
|
centerX = points[0].x;
|
||||||
|
centerY = (sourceGapPoint.y + targetGapPoint.y) / 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathPoints = [source, sourceGapped, ...points, targetGapped, target];
|
const pathPoints = [
|
||||||
|
source,
|
||||||
|
{ x: sourceGapped.x - sourceGapOffset.x, y: sourceGapped.y - sourceGapOffset.y },
|
||||||
|
...points,
|
||||||
|
{ x: targetGapped.x - targetGapOffset.x, y: targetGapped.y - targetGapOffset.y },
|
||||||
|
target,
|
||||||
|
];
|
||||||
|
|
||||||
return [pathPoints, centerX, centerY, defaultOffsetX, defaultOffsetY];
|
return [pathPoints, centerX, centerY, defaultOffsetX, defaultOffsetY];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,3 +173,5 @@ export const getTransformForBounds = (
|
|||||||
|
|
||||||
return [x, y, clampedZoom];
|
return [x, y, clampedZoom];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isMacOs = () => typeof navigator !== 'undefined' && navigator?.userAgent?.indexOf('Mac') >= 0;
|
||||||
|
|||||||
@@ -254,9 +254,10 @@ export function calcNextPosition<NodeType extends NodeBase>(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const positionAbsolute = currentExtent
|
const positionAbsolute =
|
||||||
? clampPosition(nextPosition, currentExtent as CoordinateExtent)
|
currentExtent && currentExtent !== 'parent'
|
||||||
: nextPosition;
|
? clampPosition(nextPosition, currentExtent as CoordinateExtent)
|
||||||
|
: nextPosition;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
position: {
|
position: {
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import { drag } from 'd3-drag';
|
import { drag } from 'd3-drag';
|
||||||
import { select } from 'd3-selection';
|
import { select } from 'd3-selection';
|
||||||
|
|
||||||
import { calcAutoPan, getEventPosition, getPointerPosition, calcNextPosition, snapPosition } from '../utils';
|
import {
|
||||||
|
calcAutoPan,
|
||||||
|
getEventPosition,
|
||||||
|
getPointerPosition,
|
||||||
|
calcNextPosition,
|
||||||
|
snapPosition,
|
||||||
|
getRectOfNodes,
|
||||||
|
rectToBox,
|
||||||
|
} from '../utils';
|
||||||
import { getDragItems, getEventHandlerParams, hasSelector, wrapSelectionDragFunc } from './utils';
|
import { getDragItems, getEventHandlerParams, hasSelector, wrapSelectionDragFunc } from './utils';
|
||||||
import type {
|
import type {
|
||||||
NodeBase,
|
NodeBase,
|
||||||
@@ -18,6 +26,7 @@ import type {
|
|||||||
OnNodeDrag,
|
OnNodeDrag,
|
||||||
OnSelectionDrag,
|
OnSelectionDrag,
|
||||||
UpdateNodePositions,
|
UpdateNodePositions,
|
||||||
|
Box,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
export type OnDrag = (event: MouseEvent, dragItems: NodeDragItem[], node: NodeBase, nodes: NodeBase[]) => void;
|
export type OnDrag = (event: MouseEvent, dragItems: NodeDragItem[], node: NodeBase, nodes: NodeBase[]) => void;
|
||||||
@@ -105,6 +114,12 @@ export function XYDrag({
|
|||||||
lastPos = { x, y };
|
lastPos = { x, y };
|
||||||
|
|
||||||
let hasChange = false;
|
let hasChange = false;
|
||||||
|
let nodesBox: Box = { x: 0, y: 0, x2: 0, y2: 0 };
|
||||||
|
|
||||||
|
if (dragItems.length > 1 && nodeExtent) {
|
||||||
|
const rect = getRectOfNodes(dragItems as unknown as NodeBase[], nodeOrigin);
|
||||||
|
nodesBox = rectToBox(rect);
|
||||||
|
}
|
||||||
|
|
||||||
dragItems = dragItems.map((n) => {
|
dragItems = dragItems.map((n) => {
|
||||||
let nextPosition = { x: x - n.distance.x, y: y - n.distance.y };
|
let nextPosition = { x: x - n.distance.x, y: y - n.distance.y };
|
||||||
@@ -113,9 +128,24 @@ export function XYDrag({
|
|||||||
nextPosition = snapPosition(nextPosition, snapGrid);
|
nextPosition = snapPosition(nextPosition, snapGrid);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedPos = calcNextPosition(n, nextPosition, nodes, nodeExtent, nodeOrigin, onError);
|
// if there is selection with multiple nodes and a node extent is set, we need to adjust the node extent for each node
|
||||||
|
// based on its position so that the node stays at it's position relative to the selection.
|
||||||
|
const adjustedNodeExtent: CoordinateExtent = [
|
||||||
|
[nodeExtent[0][0], nodeExtent[0][1]],
|
||||||
|
[nodeExtent[1][0], nodeExtent[1][1]],
|
||||||
|
];
|
||||||
|
|
||||||
// we want to make sure that we only fire a change event when there is a changes
|
if (dragItems.length > 1 && nodeExtent && !n.extent) {
|
||||||
|
adjustedNodeExtent[0][0] = n.positionAbsolute.x - nodesBox.x + nodeExtent[0][0];
|
||||||
|
adjustedNodeExtent[1][0] = n.positionAbsolute.x + (n.width ?? 0) - nodesBox.x2 + nodeExtent[1][0];
|
||||||
|
|
||||||
|
adjustedNodeExtent[0][1] = n.positionAbsolute.y - nodesBox.y + nodeExtent[0][1];
|
||||||
|
adjustedNodeExtent[1][1] = n.positionAbsolute.y + (n.height ?? 0) - nodesBox.y2 + nodeExtent[1][1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedPos = calcNextPosition(n, nextPosition, nodes, adjustedNodeExtent, nodeOrigin, onError);
|
||||||
|
|
||||||
|
// we want to make sure that we only fire a change event when there is a change
|
||||||
hasChange = hasChange || n.position.x !== updatedPos.position.x || n.position.y !== updatedPos.position.y;
|
hasChange = hasChange || n.position.x !== updatedPos.position.x || n.position.y !== updatedPos.position.y;
|
||||||
|
|
||||||
n.position = updatedPos.position;
|
n.position = updatedPos.position;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
PanZoomInstance,
|
PanZoomInstance,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { clamp } from '../utils';
|
import { clamp } from '../utils';
|
||||||
import { getD3Transition, viewportToTransform } from './utils';
|
import { getD3Transition, viewportToTransform, wheelDelta } from './utils';
|
||||||
import {
|
import {
|
||||||
createPanOnScrollHandler,
|
createPanOnScrollHandler,
|
||||||
createPanZoomEndHandler,
|
createPanZoomEndHandler,
|
||||||
@@ -26,6 +26,8 @@ export type ZoomPanValues = {
|
|||||||
prevViewport: Viewport;
|
prevViewport: Viewport;
|
||||||
mouseButton: number;
|
mouseButton: number;
|
||||||
timerId: ReturnType<typeof setTimeout> | undefined;
|
timerId: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
panScrollTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
isPanScrolling: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function XYPanZoom({
|
export function XYPanZoom({
|
||||||
@@ -46,6 +48,8 @@ export function XYPanZoom({
|
|||||||
prevViewport: { x: 0, y: 0, zoom: 0 },
|
prevViewport: { x: 0, y: 0, zoom: 0 },
|
||||||
mouseButton: 0,
|
mouseButton: 0,
|
||||||
timerId: undefined,
|
timerId: undefined,
|
||||||
|
panScrollTimeout: undefined,
|
||||||
|
isPanScrolling: false,
|
||||||
};
|
};
|
||||||
const bbox = domNode.getBoundingClientRect();
|
const bbox = domNode.getBoundingClientRect();
|
||||||
const d3ZoomInstance = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent);
|
const d3ZoomInstance = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent);
|
||||||
@@ -65,6 +69,7 @@ export function XYPanZoom({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const d3ZoomHandler = d3Selection.on('wheel.zoom')!;
|
const d3ZoomHandler = d3Selection.on('wheel.zoom')!;
|
||||||
|
d3ZoomInstance.wheelDelta(wheelDelta);
|
||||||
|
|
||||||
function setTransform(transform: ZoomTransform, options?: PanZoomTransformOptions) {
|
function setTransform(transform: ZoomTransform, options?: PanZoomTransformOptions) {
|
||||||
if (d3Selection) {
|
if (d3Selection) {
|
||||||
@@ -97,12 +102,16 @@ export function XYPanZoom({
|
|||||||
|
|
||||||
const wheelHandler = isPanOnScroll
|
const wheelHandler = isPanOnScroll
|
||||||
? createPanOnScrollHandler({
|
? createPanOnScrollHandler({
|
||||||
|
zoomPanValues,
|
||||||
noWheelClassName,
|
noWheelClassName,
|
||||||
d3Selection,
|
d3Selection,
|
||||||
d3Zoom: d3ZoomInstance,
|
d3Zoom: d3ZoomInstance,
|
||||||
panOnScrollMode,
|
panOnScrollMode,
|
||||||
panOnScrollSpeed,
|
panOnScrollSpeed,
|
||||||
zoomOnPinch,
|
zoomOnPinch,
|
||||||
|
onPanZoomStart,
|
||||||
|
onPanZoom,
|
||||||
|
onPanZoomEnd,
|
||||||
})
|
})
|
||||||
: createZoomOnScrollHandler({
|
: createZoomOnScrollHandler({
|
||||||
noWheelClassName,
|
noWheelClassName,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
import type { D3ZoomEvent } from 'd3-zoom';
|
import type { D3ZoomEvent } from 'd3-zoom';
|
||||||
import { pointer } from 'd3-selection';
|
import { pointer } from 'd3-selection';
|
||||||
|
|
||||||
@@ -7,27 +9,24 @@ import {
|
|||||||
type D3ZoomHandler,
|
type D3ZoomHandler,
|
||||||
type D3ZoomInstance,
|
type D3ZoomInstance,
|
||||||
type OnPanZoom,
|
type OnPanZoom,
|
||||||
type Viewport,
|
|
||||||
type OnDraggingChange,
|
type OnDraggingChange,
|
||||||
type OnTransformChange,
|
type OnTransformChange,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { isRightClickPan, isWrappedWithClass, transformToViewport, viewChanged } from './utils';
|
import { isRightClickPan, isWrappedWithClass, transformToViewport, viewChanged, wheelDelta } from './utils';
|
||||||
|
import { isMacOs } from '../utils';
|
||||||
export type ZoomPanValues = {
|
import { ZoomPanValues } from './XYPanZoom';
|
||||||
isZoomingOrPanning: boolean;
|
|
||||||
usedRightMouseButton: boolean;
|
|
||||||
prevViewport: Viewport;
|
|
||||||
mouseButton: number;
|
|
||||||
timerId: ReturnType<typeof setTimeout> | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PanOnScrollParams = {
|
export type PanOnScrollParams = {
|
||||||
|
zoomPanValues: ZoomPanValues;
|
||||||
noWheelClassName: string;
|
noWheelClassName: string;
|
||||||
d3Selection: D3SelectionInstance;
|
d3Selection: D3SelectionInstance;
|
||||||
d3Zoom: D3ZoomInstance;
|
d3Zoom: D3ZoomInstance;
|
||||||
panOnScrollMode: PanOnScrollMode;
|
panOnScrollMode: PanOnScrollMode;
|
||||||
panOnScrollSpeed: number;
|
panOnScrollSpeed: number;
|
||||||
zoomOnPinch: boolean;
|
zoomOnPinch: boolean;
|
||||||
|
onPanZoomStart?: OnPanZoom;
|
||||||
|
onPanZoom?: OnPanZoom;
|
||||||
|
onPanZoomEnd?: OnPanZoom;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ZoomOnScrollParams = {
|
export type ZoomOnScrollParams = {
|
||||||
@@ -60,12 +59,16 @@ export type PanZoomEndParams = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function createPanOnScrollHandler({
|
export function createPanOnScrollHandler({
|
||||||
|
zoomPanValues,
|
||||||
noWheelClassName,
|
noWheelClassName,
|
||||||
d3Selection,
|
d3Selection,
|
||||||
d3Zoom,
|
d3Zoom,
|
||||||
panOnScrollMode,
|
panOnScrollMode,
|
||||||
panOnScrollSpeed,
|
panOnScrollSpeed,
|
||||||
zoomOnPinch,
|
zoomOnPinch,
|
||||||
|
onPanZoomStart,
|
||||||
|
onPanZoom,
|
||||||
|
onPanZoomEnd,
|
||||||
}: PanOnScrollParams) {
|
}: PanOnScrollParams) {
|
||||||
return (event: any) => {
|
return (event: any) => {
|
||||||
if (isWrappedWithClass(event, noWheelClassName)) {
|
if (isWrappedWithClass(event, noWheelClassName)) {
|
||||||
@@ -75,13 +78,15 @@ export function createPanOnScrollHandler({
|
|||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
|
|
||||||
const currentZoom = d3Selection.property('__zoom').k || 1;
|
const currentZoom = d3Selection.property('__zoom').k || 1;
|
||||||
|
const _isMacOs = isMacOs();
|
||||||
|
|
||||||
if (event.ctrlKey && zoomOnPinch) {
|
// macos sets ctrlKey=true for pinch gesture on a trackpad
|
||||||
|
if (event.ctrlKey && zoomOnPinch && _isMacOs) {
|
||||||
const point = pointer(event);
|
const point = pointer(event);
|
||||||
// taken from https://github.com/d3/d3-zoom/blob/master/src/zoom.js
|
const pinchDelta = wheelDelta(event);
|
||||||
const pinchDelta = -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * 10;
|
|
||||||
const zoom = currentZoom * Math.pow(2, pinchDelta);
|
const zoom = currentZoom * Math.pow(2, pinchDelta);
|
||||||
d3Zoom.scaleTo(d3Selection, zoom, point);
|
// @ts-ignore
|
||||||
|
d3Zoom.scaleTo(d3Selection, zoom, point, event);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -89,14 +94,45 @@ export function createPanOnScrollHandler({
|
|||||||
// increase scroll speed in firefox
|
// increase scroll speed in firefox
|
||||||
// firefox: deltaMode === 1; chrome: deltaMode === 0
|
// firefox: deltaMode === 1; chrome: deltaMode === 0
|
||||||
const deltaNormalize = event.deltaMode === 1 ? 20 : 1;
|
const deltaNormalize = event.deltaMode === 1 ? 20 : 1;
|
||||||
const deltaX = panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize;
|
let deltaX = panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize;
|
||||||
const deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize;
|
let deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize;
|
||||||
|
|
||||||
|
// this enables vertical scrolling with shift + scroll on windows
|
||||||
|
if (!_isMacOs && event.shiftKey && panOnScrollMode !== PanOnScrollMode.Vertical) {
|
||||||
|
deltaX = event.deltaY * deltaNormalize;
|
||||||
|
deltaY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
d3Zoom.translateBy(
|
d3Zoom.translateBy(
|
||||||
d3Selection,
|
d3Selection,
|
||||||
-(deltaX / currentZoom) * panOnScrollSpeed,
|
-(deltaX / currentZoom) * panOnScrollSpeed,
|
||||||
-(deltaY / currentZoom) * panOnScrollSpeed
|
-(deltaY / currentZoom) * panOnScrollSpeed,
|
||||||
|
// @ts-ignore
|
||||||
|
{ internal: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const nextViewport = transformToViewport(d3Selection.property('__zoom'));
|
||||||
|
|
||||||
|
clearTimeout(zoomPanValues.panScrollTimeout);
|
||||||
|
|
||||||
|
// for pan on scroll we need to handle the event calls on our own
|
||||||
|
// we can't use the start, zoom and end events from d3-zoom
|
||||||
|
// because start and move gets called on every scroll event and not once at the beginning
|
||||||
|
if (!zoomPanValues.isPanScrolling) {
|
||||||
|
zoomPanValues.isPanScrolling = true;
|
||||||
|
|
||||||
|
onPanZoomStart?.(event, nextViewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zoomPanValues.isPanScrolling) {
|
||||||
|
onPanZoom?.(event, nextViewport);
|
||||||
|
|
||||||
|
zoomPanValues.panScrollTimeout = setTimeout(() => {
|
||||||
|
onPanZoomEnd?.(event, nextViewport);
|
||||||
|
|
||||||
|
zoomPanValues.isPanScrolling = false;
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,19 +150,22 @@ export function createZoomOnScrollHandler({ noWheelClassName, preventScrolling,
|
|||||||
|
|
||||||
export function createPanZoomStartHandler({ zoomPanValues, onDraggingChange, onPanZoomStart }: PanZoomStartParams) {
|
export function createPanZoomStartHandler({ zoomPanValues, onDraggingChange, onPanZoomStart }: PanZoomStartParams) {
|
||||||
return (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
return (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||||
|
if (event.sourceEvent?.internal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewport = transformToViewport(event.transform);
|
||||||
|
|
||||||
// we need to remember it here, because it's always 0 in the "zoom" event
|
// we need to remember it here, because it's always 0 in the "zoom" event
|
||||||
zoomPanValues.mouseButton = event.sourceEvent?.button || 0;
|
zoomPanValues.mouseButton = event.sourceEvent?.button || 0;
|
||||||
|
|
||||||
zoomPanValues.isZoomingOrPanning = true;
|
zoomPanValues.isZoomingOrPanning = true;
|
||||||
|
zoomPanValues.prevViewport = viewport;
|
||||||
|
|
||||||
if (event.sourceEvent?.type === 'mousedown') {
|
if (event.sourceEvent?.type === 'mousedown') {
|
||||||
onDraggingChange(true);
|
onDraggingChange(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onPanZoomStart) {
|
if (onPanZoomStart) {
|
||||||
const viewport = transformToViewport(event.transform);
|
|
||||||
zoomPanValues.prevViewport = viewport;
|
|
||||||
|
|
||||||
onPanZoomStart?.(event.sourceEvent as MouseEvent | TouchEvent, viewport);
|
onPanZoomStart?.(event.sourceEvent as MouseEvent | TouchEvent, viewport);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -146,7 +185,7 @@ export function createPanZoomHandler({
|
|||||||
|
|
||||||
onTransformChange([event.transform.x, event.transform.y, event.transform.k]);
|
onTransformChange([event.transform.x, event.transform.y, event.transform.k]);
|
||||||
|
|
||||||
if (onPanZoom) {
|
if (onPanZoom && !event.sourceEvent?.internal) {
|
||||||
onPanZoom?.(event.sourceEvent as MouseEvent | TouchEvent, transformToViewport(event.transform));
|
onPanZoom?.(event.sourceEvent as MouseEvent | TouchEvent, transformToViewport(event.transform));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -161,6 +200,9 @@ export function createPanZoomEndHandler({
|
|||||||
onPaneContextMenu,
|
onPaneContextMenu,
|
||||||
}: PanZoomEndParams) {
|
}: PanZoomEndParams) {
|
||||||
return (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
return (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||||
|
if (event.sourceEvent?.internal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
zoomPanValues.isZoomingOrPanning = false;
|
zoomPanValues.isZoomingOrPanning = false;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type ZoomTransform, zoomIdentity } from 'd3-zoom';
|
import { type ZoomTransform, zoomIdentity } from 'd3-zoom';
|
||||||
|
|
||||||
import { type D3SelectionInstance, type Viewport } from '../types';
|
import { type D3SelectionInstance, type Viewport } from '../types';
|
||||||
|
import { isMacOs } from '../utils';
|
||||||
|
|
||||||
export const viewChanged = (prevViewport: Viewport, eventViewport: any): boolean =>
|
export const viewChanged = (prevViewport: Viewport, eventViewport: any): boolean =>
|
||||||
prevViewport.x !== eventViewport.x || prevViewport.y !== eventViewport.y || prevViewport.zoom !== eventViewport.k;
|
prevViewport.x !== eventViewport.x || prevViewport.y !== eventViewport.y || prevViewport.zoom !== eventViewport.k;
|
||||||
@@ -21,3 +22,9 @@ export const isRightClickPan = (panOnDrag: boolean | number[], usedButton: numbe
|
|||||||
|
|
||||||
export const getD3Transition = (selection: D3SelectionInstance, duration = 0) =>
|
export const getD3Transition = (selection: D3SelectionInstance, duration = 0) =>
|
||||||
typeof duration === 'number' && duration > 0 ? selection.transition().duration(duration) : selection;
|
typeof duration === 'number' && duration > 0 ? selection.transition().duration(duration) : selection;
|
||||||
|
|
||||||
|
export const wheelDelta = (event: any) => {
|
||||||
|
const factor = event.ctrlKey && isMacOs() ? 10 : 1;
|
||||||
|
|
||||||
|
return -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * factor;
|
||||||
|
};
|
||||||
|
|||||||
Generated
+2883
-2548
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user