feat(react/svelte): controlled viewport (#3461)
* feat(react): controlled viewport * refactor(react): cleanup viewport * feat(svelte): two way data binding for viewport, cleanup transform <> viewport stuff * refactor(svelte): viewport stuff
This commit is contained in:
@@ -4,6 +4,7 @@ import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-ro
|
|||||||
import Basic from '../examples/Basic';
|
import Basic from '../examples/Basic';
|
||||||
import Backgrounds from '../examples/Backgrounds';
|
import Backgrounds from '../examples/Backgrounds';
|
||||||
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
|
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
|
||||||
|
import ControlledViewport from '../examples/ControlledViewport';
|
||||||
import CustomConnectionLine from '../examples/CustomConnectionLine';
|
import CustomConnectionLine from '../examples/CustomConnectionLine';
|
||||||
import CustomMiniMapNode from '../examples/CustomMiniMapNode';
|
import CustomMiniMapNode from '../examples/CustomMiniMapNode';
|
||||||
import CustomNode from '../examples/CustomNode';
|
import CustomNode from '../examples/CustomNode';
|
||||||
@@ -74,6 +75,11 @@ const routes: IRoute[] = [
|
|||||||
path: '/controlled-uncontrolled',
|
path: '/controlled-uncontrolled',
|
||||||
component: ControlledUncontrolled,
|
component: ControlledUncontrolled,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Controlled Viewport',
|
||||||
|
path: '/controlled-viewport',
|
||||||
|
component: ControlledViewport,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Custom Connection Line',
|
name: 'Custom Connection Line',
|
||||||
path: '/custom-connectionline',
|
path: '/custom-connectionline',
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { MouseEvent, useCallback, useState } from 'react';
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
addEdge,
|
||||||
|
Node,
|
||||||
|
Connection,
|
||||||
|
Edge,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
Viewport,
|
||||||
|
Panel,
|
||||||
|
MiniMap,
|
||||||
|
Background,
|
||||||
|
ReactFlowProvider,
|
||||||
|
useReactFlow,
|
||||||
|
Controls,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
|
||||||
|
const initNodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: '1a',
|
||||||
|
type: 'input',
|
||||||
|
data: { label: 'Node 1' },
|
||||||
|
position: { x: 250, y: 5 },
|
||||||
|
className: 'light',
|
||||||
|
ariaLabel: 'Input Node 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2a',
|
||||||
|
data: { label: 'Node 2' },
|
||||||
|
position: { x: 100, y: 100 },
|
||||||
|
className: 'light',
|
||||||
|
ariaLabel: 'Default Node 2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3a',
|
||||||
|
data: { label: 'Node 3' },
|
||||||
|
position: { x: 400, y: 100 },
|
||||||
|
className: 'light',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4a',
|
||||||
|
data: { label: 'Node 4' },
|
||||||
|
position: { x: 400, y: 200 },
|
||||||
|
className: 'light',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const initEdges: Edge[] = [
|
||||||
|
{ id: 'e1-2', source: '1a', target: '2a', ariaLabel: undefined },
|
||||||
|
{ id: 'e1-3', source: '1a', target: '3a' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const Flow = () => {
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState(initNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
|
||||||
|
const [viewport, setViewport] = useState<Viewport>({ x: 0, y: 0, zoom: 1 });
|
||||||
|
const { fitView } = useReactFlow();
|
||||||
|
|
||||||
|
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
viewport={viewport}
|
||||||
|
onViewportChange={setViewport}
|
||||||
|
>
|
||||||
|
<Panel position="top-left">
|
||||||
|
<button onClick={() => setViewport((vp) => ({ ...vp, y: vp.y + 10 }))}>update viewport</button>
|
||||||
|
<button onClick={() => fitView()}>fitView</button>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<MiniMap />
|
||||||
|
<Background />
|
||||||
|
<Controls />
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<Flow />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
|
'custom-connection-line',
|
||||||
'customnode',
|
'customnode',
|
||||||
'drag-n-drop',
|
'drag-n-drop',
|
||||||
'edges',
|
'edges',
|
||||||
@@ -11,10 +12,10 @@
|
|||||||
'overview',
|
'overview',
|
||||||
'stress',
|
'stress',
|
||||||
'subflows',
|
'subflows',
|
||||||
|
'two-way-viewport',
|
||||||
'usesvelteflow',
|
'usesvelteflow',
|
||||||
'useupdatenodeinternals',
|
'useupdatenodeinternals',
|
||||||
'validation',
|
'validation'
|
||||||
'custom-connection-line'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const onChange = (event: Event) => {
|
const onChange = (event: Event) => {
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { SvelteFlowProvider } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
import Flow from './Flow.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SvelteFlowProvider>
|
||||||
|
<Flow />
|
||||||
|
</SvelteFlowProvider>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import {
|
||||||
|
SvelteFlow,
|
||||||
|
Controls,
|
||||||
|
Background,
|
||||||
|
BackgroundVariant,
|
||||||
|
Panel,
|
||||||
|
useSvelteFlow,
|
||||||
|
type Viewport
|
||||||
|
} from '@xyflow/svelte';
|
||||||
|
|
||||||
|
import '@xyflow/svelte/dist/style.css';
|
||||||
|
|
||||||
|
const nodes = writable([
|
||||||
|
{
|
||||||
|
id: 'A',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: { label: 'A' }
|
||||||
|
},
|
||||||
|
{ id: 'B', position: { x: 0, y: 100 }, data: { label: 'B' } }
|
||||||
|
]);
|
||||||
|
const edges = writable([{ id: 'ab', source: 'A', target: 'B' }]);
|
||||||
|
const viewport = writable<Viewport>({ x: 0, y: 10, zoom: 1.25 });
|
||||||
|
|
||||||
|
const { fitView } = useSvelteFlow();
|
||||||
|
|
||||||
|
const updateViewport = () => {
|
||||||
|
$viewport.x += 10;
|
||||||
|
$viewport = $viewport;
|
||||||
|
};
|
||||||
|
|
||||||
|
viewport.subscribe((vp) => {
|
||||||
|
console.log('viewport update', vp);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SvelteFlow {nodes} {edges} fitView {viewport}>
|
||||||
|
<Controls />
|
||||||
|
<Background variant={BackgroundVariant.Dots} />
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<button on:click={updateViewport}>update viewport</button>
|
||||||
|
<button on:click={() => fitView()}>fitView</button>
|
||||||
|
</Panel>
|
||||||
|
</SvelteFlow>
|
||||||
@@ -24,6 +24,7 @@ export type FlowRendererProps = Omit<
|
|||||||
| 'rfId'
|
| 'rfId'
|
||||||
| 'nodeOrigin'
|
| 'nodeOrigin'
|
||||||
> & {
|
> & {
|
||||||
|
isControlledViewport: boolean;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,6 +64,8 @@ const FlowRenderer = ({
|
|||||||
noWheelClassName,
|
noWheelClassName,
|
||||||
noPanClassName,
|
noPanClassName,
|
||||||
disableKeyboardA11y,
|
disableKeyboardA11y,
|
||||||
|
onViewportChange,
|
||||||
|
isControlledViewport,
|
||||||
}: FlowRendererProps) => {
|
}: FlowRendererProps) => {
|
||||||
const nodesSelectionActive = useStore(selector);
|
const nodesSelectionActive = useStore(selector);
|
||||||
const selectionKeyPressed = useKeyPress(selectionKeyCode);
|
const selectionKeyPressed = useKeyPress(selectionKeyCode);
|
||||||
@@ -92,6 +95,8 @@ const FlowRenderer = ({
|
|||||||
preventScrolling={preventScrolling}
|
preventScrolling={preventScrolling}
|
||||||
noWheelClassName={noWheelClassName}
|
noWheelClassName={noWheelClassName}
|
||||||
noPanClassName={noPanClassName}
|
noPanClassName={noPanClassName}
|
||||||
|
onViewportChange={onViewportChange}
|
||||||
|
isControlledViewport={isControlledViewport}
|
||||||
>
|
>
|
||||||
<Pane
|
<Pane
|
||||||
onSelectionStart={onSelectionStart}
|
onSelectionStart={onSelectionStart}
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import NodeRenderer from '../NodeRenderer';
|
|||||||
import EdgeRenderer from '../EdgeRenderer';
|
import EdgeRenderer from '../EdgeRenderer';
|
||||||
import ViewportWrapper from '../Viewport';
|
import ViewportWrapper from '../Viewport';
|
||||||
import useOnInitHandler from '../../hooks/useOnInitHandler';
|
import useOnInitHandler from '../../hooks/useOnInitHandler';
|
||||||
|
import useViewportSync from '../../hooks/useViewportSync';
|
||||||
import ConnectionLine from '../../components/ConnectionLine';
|
import ConnectionLine from '../../components/ConnectionLine';
|
||||||
import type { NodeTypesWrapped, ReactFlowProps } from '../../types';
|
import type { ReactFlowProps } from '../../types';
|
||||||
import { createNodeTypes } from '../NodeRenderer/utils';
|
import { createNodeTypes } from '../NodeRenderer/utils';
|
||||||
import { createEdgeTypes } from '../EdgeRenderer/utils';
|
import { createEdgeTypes } from '../EdgeRenderer/utils';
|
||||||
import { useNodeOrEdgeTypes } from './utils';
|
import { useNodeOrEdgeTypes } from './utils';
|
||||||
@@ -104,11 +105,14 @@ const GraphView = ({
|
|||||||
nodeOrigin,
|
nodeOrigin,
|
||||||
nodeExtent,
|
nodeExtent,
|
||||||
rfId,
|
rfId,
|
||||||
|
viewport,
|
||||||
|
onViewportChange,
|
||||||
}: GraphViewProps) => {
|
}: GraphViewProps) => {
|
||||||
const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes);
|
const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes);
|
||||||
const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes);
|
const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes);
|
||||||
|
|
||||||
useOnInitHandler(onInit);
|
useOnInitHandler(onInit);
|
||||||
|
useViewportSync(viewport);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlowRenderer
|
<FlowRenderer
|
||||||
@@ -145,6 +149,8 @@ const GraphView = ({
|
|||||||
noWheelClassName={noWheelClassName}
|
noWheelClassName={noWheelClassName}
|
||||||
noPanClassName={noPanClassName}
|
noPanClassName={noPanClassName}
|
||||||
disableKeyboardA11y={disableKeyboardA11y}
|
disableKeyboardA11y={disableKeyboardA11y}
|
||||||
|
onViewportChange={onViewportChange}
|
||||||
|
isControlledViewport={!!viewport}
|
||||||
>
|
>
|
||||||
<ViewportWrapper>
|
<ViewportWrapper>
|
||||||
<EdgeRenderer
|
<EdgeRenderer
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
|||||||
style,
|
style,
|
||||||
id,
|
id,
|
||||||
nodeDragThreshold,
|
nodeDragThreshold,
|
||||||
|
viewport,
|
||||||
|
onViewportChange,
|
||||||
...rest
|
...rest
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
@@ -242,6 +244,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
|||||||
disableKeyboardA11y={disableKeyboardA11y}
|
disableKeyboardA11y={disableKeyboardA11y}
|
||||||
nodeOrigin={nodeOrigin}
|
nodeOrigin={nodeOrigin}
|
||||||
nodeExtent={nodeExtent}
|
nodeExtent={nodeExtent}
|
||||||
|
viewport={viewport}
|
||||||
|
onViewportChange={onViewportChange}
|
||||||
/>
|
/>
|
||||||
<StoreUpdater
|
<StoreUpdater
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react';
|
|||||||
|
|
||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
import type { ReactFlowState } from '../../types';
|
import type { ReactFlowState } from '../../types';
|
||||||
|
import { Viewport } from '@xyflow/system';
|
||||||
|
|
||||||
const selector = (s: ReactFlowState) => `translate(${s.transform[0]}px,${s.transform[1]}px) scale(${s.transform[2]})`;
|
const selector = (s: ReactFlowState) => `translate(${s.transform[0]}px,${s.transform[1]}px) scale(${s.transform[2]})`;
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ type ZoomPaneProps = Omit<
|
|||||||
| 'noDragClassName'
|
| 'noDragClassName'
|
||||||
| 'disableKeyboardA11y'
|
| 'disableKeyboardA11y'
|
||||||
| 'selectionOnDrag'
|
| 'selectionOnDrag'
|
||||||
>;
|
> & {
|
||||||
|
isControlledViewport: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const selector = (s: ReactFlowState) => ({
|
const selector = (s: ReactFlowState) => ({
|
||||||
userSelectionActive: s.userSelectionActive,
|
userSelectionActive: s.userSelectionActive,
|
||||||
@@ -43,6 +45,8 @@ const ZoomPane = ({
|
|||||||
children,
|
children,
|
||||||
noWheelClassName,
|
noWheelClassName,
|
||||||
noPanClassName,
|
noPanClassName,
|
||||||
|
onViewportChange,
|
||||||
|
isControlledViewport,
|
||||||
}: ZoomPaneProps) => {
|
}: ZoomPaneProps) => {
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const zoomPane = useRef<HTMLDivElement>(null);
|
const zoomPane = useRef<HTMLDivElement>(null);
|
||||||
@@ -60,7 +64,13 @@ const ZoomPane = ({
|
|||||||
maxZoom,
|
maxZoom,
|
||||||
translateExtent,
|
translateExtent,
|
||||||
viewport: defaultViewport,
|
viewport: defaultViewport,
|
||||||
onTransformChange: (transform: Transform) => store.setState({ transform }),
|
onTransformChange: (transform: Transform) => {
|
||||||
|
onViewportChange?.({ x: transform[0], y: transform[1], zoom: transform[2] });
|
||||||
|
|
||||||
|
if (!isControlledViewport) {
|
||||||
|
store.setState({ transform });
|
||||||
|
}
|
||||||
|
},
|
||||||
onDraggingChange: (paneDragging: boolean) => store.setState({ paneDragging }),
|
onDraggingChange: (paneDragging: boolean) => store.setState({ paneDragging }),
|
||||||
onPanZoomStart: (event, vp) => {
|
onPanZoomStart: (event, vp) => {
|
||||||
const { onViewportChangeStart, onMoveStart } = store.getState();
|
const { onViewportChangeStart, onMoveStart } = store.getState();
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import type { Viewport } from '@xyflow/system';
|
||||||
|
|
||||||
|
import { useStore, useStoreApi } from './useStore';
|
||||||
|
import type { ReactFlowState } from '../types';
|
||||||
|
|
||||||
|
const selector = (state: ReactFlowState) => state.panZoom?.syncViewport;
|
||||||
|
|
||||||
|
export default function useViewportSync(viewport?: Viewport) {
|
||||||
|
const syncViewport = useStore(selector);
|
||||||
|
const store = useStoreApi();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewport) {
|
||||||
|
syncViewport?.(viewport);
|
||||||
|
store.setState({ transform: [viewport.x, viewport.y, viewport.zoom] });
|
||||||
|
}
|
||||||
|
}, [viewport, syncViewport]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -120,7 +120,9 @@ export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
|
|||||||
panOnDrag?: boolean | number[];
|
panOnDrag?: boolean | number[];
|
||||||
minZoom?: number;
|
minZoom?: number;
|
||||||
maxZoom?: number;
|
maxZoom?: number;
|
||||||
|
viewport?: Viewport;
|
||||||
defaultViewport?: Viewport;
|
defaultViewport?: Viewport;
|
||||||
|
onViewportChange?: (viewport: Viewport) => void;
|
||||||
translateExtent?: CoordinateExtent;
|
translateExtent?: CoordinateExtent;
|
||||||
preventScrolling?: boolean;
|
preventScrolling?: boolean;
|
||||||
nodeExtent?: CoordinateExtent;
|
nodeExtent?: CoordinateExtent;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export default function drag(domNode: Element, params: UseDragParams) {
|
|||||||
onDragStop,
|
onDragStop,
|
||||||
getStoreItems: () => {
|
getStoreItems: () => {
|
||||||
const snapGrid = get(store.snapGrid);
|
const snapGrid = get(store.snapGrid);
|
||||||
|
const vp = get(store.viewport);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodes: get(store.nodes),
|
nodes: get(store.nodes),
|
||||||
@@ -34,7 +35,7 @@ export default function drag(domNode: Element, params: UseDragParams) {
|
|||||||
nodeOrigin: [0, 0],
|
nodeOrigin: [0, 0],
|
||||||
multiSelectionActive: false,
|
multiSelectionActive: false,
|
||||||
domNode: get(store.domNode),
|
domNode: get(store.domNode),
|
||||||
transform: get(store.transform),
|
transform: [vp.x, vp.y, vp.zoom],
|
||||||
autoPanOnNodeDrag: get(store.autoPanOnNodeDrag),
|
autoPanOnNodeDrag: get(store.autoPanOnNodeDrag),
|
||||||
nodesDraggable: get(store.nodesDraggable),
|
nodesDraggable: get(store.nodesDraggable),
|
||||||
selectNodesOnDrag: get(store.selectNodesOnDrag),
|
selectNodesOnDrag: get(store.selectNodesOnDrag),
|
||||||
|
|||||||
@@ -5,15 +5,14 @@ import {
|
|||||||
type CoordinateExtent,
|
type CoordinateExtent,
|
||||||
type OnPanZoom,
|
type OnPanZoom,
|
||||||
type PanZoomInstance,
|
type PanZoomInstance,
|
||||||
type Transform,
|
|
||||||
type Viewport
|
type Viewport
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
type ZoomParams = {
|
type ZoomParams = {
|
||||||
transform: Writable<Transform>;
|
viewport: Writable<Viewport>;
|
||||||
|
initialViewport: Viewport;
|
||||||
minZoom: number;
|
minZoom: number;
|
||||||
maxZoom: number;
|
maxZoom: number;
|
||||||
initialViewport: Viewport;
|
|
||||||
dragging: Writable<boolean>;
|
dragging: Writable<boolean>;
|
||||||
onPanZoomStart?: OnPanZoom;
|
onPanZoomStart?: OnPanZoom;
|
||||||
onPanZoom?: OnPanZoom;
|
onPanZoom?: OnPanZoom;
|
||||||
@@ -39,7 +38,7 @@ type ZoomParams = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function zoom(domNode: Element, params: ZoomParams) {
|
export default function zoom(domNode: Element, params: ZoomParams) {
|
||||||
const { panZoom, minZoom, maxZoom, initialViewport, transform, dragging, translateExtent } =
|
const { panZoom, minZoom, maxZoom, initialViewport, viewport, dragging, translateExtent } =
|
||||||
params;
|
params;
|
||||||
|
|
||||||
const panZoomInstance = XYPanZoom({
|
const panZoomInstance = XYPanZoom({
|
||||||
@@ -48,11 +47,12 @@ export default function zoom(domNode: Element, params: ZoomParams) {
|
|||||||
maxZoom,
|
maxZoom,
|
||||||
translateExtent,
|
translateExtent,
|
||||||
viewport: initialViewport,
|
viewport: initialViewport,
|
||||||
onTransformChange: transform.set,
|
onTransformChange: (transform) =>
|
||||||
|
viewport.set({ x: transform[0], y: transform[1], zoom: transform[2] }),
|
||||||
onDraggingChange: dragging.set
|
onDraggingChange: dragging.set
|
||||||
});
|
});
|
||||||
const { x, y, zoom } = panZoomInstance.getViewport();
|
const currentViewport = panZoomInstance.getViewport();
|
||||||
transform.set([x, y, zoom]);
|
viewport.set(currentViewport);
|
||||||
panZoom.set(panZoomInstance);
|
panZoom.set(panZoomInstance);
|
||||||
|
|
||||||
panZoomInstance.update(params);
|
panZoomInstance.update(params);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
domNode,
|
domNode,
|
||||||
nodes,
|
nodes,
|
||||||
connectionRadius,
|
connectionRadius,
|
||||||
transform,
|
viewport,
|
||||||
isValidConnection,
|
isValidConnection,
|
||||||
lib,
|
lib,
|
||||||
addEdge,
|
addEdge,
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
onConnectEnd: (event) => {
|
onConnectEnd: (event) => {
|
||||||
dispatch('connectend', { event });
|
dispatch('connectend', { event });
|
||||||
},
|
},
|
||||||
getTransform: () => $transform
|
getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
const {
|
const {
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
transform,
|
viewport,
|
||||||
dragging,
|
dragging,
|
||||||
elementsSelectable,
|
elementsSelectable,
|
||||||
selectionRect,
|
selectionRect,
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
selectedNodes = getNodesInside<Node>(
|
selectedNodes = getNodesInside<Node>(
|
||||||
$nodes,
|
$nodes,
|
||||||
nextUserSelectRect,
|
nextUserSelectRect,
|
||||||
$transform,
|
[$viewport.x, $viewport.y, $viewport.zoom],
|
||||||
$selectionMode === SelectionMode.Partial,
|
$selectionMode === SelectionMode.Partial,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
export let minZoom: $$Props['minZoom'] = undefined;
|
export let minZoom: $$Props['minZoom'] = undefined;
|
||||||
export let maxZoom: $$Props['maxZoom'] = undefined;
|
export let maxZoom: $$Props['maxZoom'] = undefined;
|
||||||
export let initialViewport: Viewport = { x: 0, y: 0, zoom: 1 };
|
export let initialViewport: Viewport = { x: 0, y: 0, zoom: 1 };
|
||||||
|
export let viewport: $$Props['viewport'] = undefined;
|
||||||
export let nodeTypes: $$Props['nodeTypes'] = undefined;
|
export let nodeTypes: $$Props['nodeTypes'] = undefined;
|
||||||
export let edgeTypes: $$Props['edgeTypes'] = undefined;
|
export let edgeTypes: $$Props['edgeTypes'] = undefined;
|
||||||
export let selectionKey: $$Props['selectionKey'] = undefined;
|
export let selectionKey: $$Props['selectionKey'] = undefined;
|
||||||
@@ -81,6 +82,7 @@
|
|||||||
|
|
||||||
store.syncNodeStores(nodes);
|
store.syncNodeStores(nodes);
|
||||||
store.syncEdgeStores(edges);
|
store.syncEdgeStores(edges);
|
||||||
|
store.syncViewport(viewport);
|
||||||
|
|
||||||
if (fitView !== undefined) {
|
if (fitView !== undefined) {
|
||||||
store.fitViewOnInit.set(fitView);
|
store.fitViewOnInit.set(fitView);
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
|
|||||||
minZoom?: number;
|
minZoom?: number;
|
||||||
maxZoom?: number;
|
maxZoom?: number;
|
||||||
initialViewport?: Viewport;
|
initialViewport?: Viewport;
|
||||||
|
viewport?: Writable<Viewport>;
|
||||||
connectionRadius?: number;
|
connectionRadius?: number;
|
||||||
connectionMode?: ConnectionMode;
|
connectionMode?: ConnectionMode;
|
||||||
connectionLineStyle?: string;
|
connectionLineStyle?: string;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useStore } from '$lib/store';
|
import { useStore } from '$lib/store';
|
||||||
|
|
||||||
const { transform } = useStore();
|
const { viewport } = useStore();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="svelte-flow__viewport xyflow__viewport"
|
class="svelte-flow__viewport xyflow__viewport"
|
||||||
style="transform: translate({$transform[0]}px, {$transform[1]}px) scale({$transform[2]})"
|
style="transform: translate({$viewport.x}px, {$viewport.y}px) scale({$viewport.zoom})"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
export let panOnScroll: $$Props['panOnScroll'];
|
export let panOnScroll: $$Props['panOnScroll'];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
transform,
|
viewport,
|
||||||
panZoom,
|
panZoom,
|
||||||
selectionKeyPressed,
|
selectionKeyPressed,
|
||||||
minZoom,
|
minZoom,
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
<div
|
<div
|
||||||
class="svelte-flow__zoom"
|
class="svelte-flow__zoom"
|
||||||
use:zoom={{
|
use:zoom={{
|
||||||
transform,
|
viewport,
|
||||||
minZoom: $minZoom,
|
minZoom: $minZoom,
|
||||||
maxZoom: $maxZoom,
|
maxZoom: $maxZoom,
|
||||||
initialViewport: viewPort,
|
initialViewport: viewPort,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { get, writable, type Writable } from 'svelte/store';
|
import { get, type Writable } from 'svelte/store';
|
||||||
import {
|
import {
|
||||||
pointToRendererPoint,
|
pointToRendererPoint,
|
||||||
type Project,
|
type Project,
|
||||||
@@ -32,7 +32,7 @@ export function useSvelteFlow(): {
|
|||||||
zoomOut,
|
zoomOut,
|
||||||
fitView,
|
fitView,
|
||||||
snapGrid,
|
snapGrid,
|
||||||
transform,
|
viewport,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
maxZoom,
|
maxZoom,
|
||||||
@@ -41,44 +41,26 @@ export function useSvelteFlow(): {
|
|||||||
edges
|
edges
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
const transformValues = get(transform);
|
|
||||||
const viewportWritable = writable({
|
|
||||||
x: transformValues[0],
|
|
||||||
y: transformValues[1],
|
|
||||||
zoom: transformValues[2]
|
|
||||||
});
|
|
||||||
|
|
||||||
transform.subscribe((ts) =>
|
|
||||||
viewportWritable.set({
|
|
||||||
x: ts[0],
|
|
||||||
y: ts[1],
|
|
||||||
zoom: ts[2]
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
zoomIn,
|
zoomIn,
|
||||||
zoomOut,
|
zoomOut,
|
||||||
setZoom: (zoomLevel, options) => {
|
setZoom: (zoomLevel, options) => {
|
||||||
get(panZoom)?.scaleTo(zoomLevel, { duration: options?.duration });
|
get(panZoom)?.scaleTo(zoomLevel, { duration: options?.duration });
|
||||||
},
|
},
|
||||||
getZoom: () => get(transform)[2],
|
getZoom: () => get(viewport).zoom,
|
||||||
setViewport: (viewport, options) => {
|
setViewport: (vieport, options) => {
|
||||||
const [x, y, zoom] = get(transform);
|
const currentViewport = get(viewport);
|
||||||
|
|
||||||
get(panZoom)?.setViewport(
|
get(panZoom)?.setViewport(
|
||||||
{
|
{
|
||||||
x: viewport.x ?? x,
|
x: vieport.x ?? currentViewport.x,
|
||||||
y: viewport.y ?? y,
|
y: vieport.y ?? currentViewport.y,
|
||||||
zoom: viewport.zoom ?? zoom
|
zoom: vieport.zoom ?? currentViewport.zoom
|
||||||
},
|
},
|
||||||
{ duration: options?.duration }
|
{ duration: options?.duration }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getViewport: () => {
|
getViewport: () => get(viewport),
|
||||||
const [x, y, zoom] = get(transform);
|
|
||||||
return { x, y, zoom };
|
|
||||||
},
|
|
||||||
setCenter: (x, y, options) => {
|
setCenter: (x, y, options) => {
|
||||||
const _width = get(width);
|
const _width = get(width);
|
||||||
const _height = get(height);
|
const _height = get(height);
|
||||||
@@ -98,15 +80,12 @@ export function useSvelteFlow(): {
|
|||||||
fitView,
|
fitView,
|
||||||
project: (position: XYPosition) => {
|
project: (position: XYPosition) => {
|
||||||
const _snapGrid = get(snapGrid);
|
const _snapGrid = get(snapGrid);
|
||||||
return pointToRendererPoint(
|
const { x, y, zoom } = get(viewport);
|
||||||
position,
|
|
||||||
get(transform),
|
return pointToRendererPoint(position, [x, y, zoom], _snapGrid !== null, _snapGrid || [1, 1]);
|
||||||
_snapGrid !== null,
|
|
||||||
_snapGrid || [1, 1]
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
viewport: viewportWritable
|
viewport: viewport
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,15 +26,15 @@
|
|||||||
let className: $$Props['class'] = '';
|
let className: $$Props['class'] = '';
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
|
||||||
const { transform, flowId } = useStore();
|
const { viewport, flowId } = useStore();
|
||||||
const patternSize = size || defaultSize[variant!];
|
const patternSize = size || defaultSize[variant!];
|
||||||
const isDots = variant === BackgroundVariant.Dots;
|
const isDots = variant === BackgroundVariant.Dots;
|
||||||
const isCross = variant === BackgroundVariant.Cross;
|
const isCross = variant === BackgroundVariant.Cross;
|
||||||
const gapXY: number[] = Array.isArray(gap!) ? gap! : [gap!, gap!];
|
const gapXY: number[] = Array.isArray(gap!) ? gap! : [gap!, gap!];
|
||||||
|
|
||||||
$: patternId = `background-pattern-${$flowId}`;
|
$: patternId = `background-pattern-${$flowId}`;
|
||||||
$: scaledGap = [gapXY[0] * $transform[2] || 1, gapXY[1] * $transform[2] || 1];
|
$: scaledGap = [gapXY[0] * $viewport.zoom || 1, gapXY[1] * $viewport.zoom || 1];
|
||||||
$: scaledSize = patternSize * $transform[2];
|
$: scaledSize = patternSize * $viewport.zoom;
|
||||||
$: patternDimensions = (isCross ? [scaledSize, scaledSize] : scaledGap) as [number, number];
|
$: patternDimensions = (isCross ? [scaledSize, scaledSize] : scaledGap) as [number, number];
|
||||||
$: patternOffset = isDots
|
$: patternOffset = isDots
|
||||||
? [scaledSize / 2, scaledSize / 2]
|
? [scaledSize / 2, scaledSize / 2]
|
||||||
@@ -49,8 +49,8 @@
|
|||||||
>
|
>
|
||||||
<pattern
|
<pattern
|
||||||
id={patternId}
|
id={patternId}
|
||||||
x={$transform[0] % scaledGap[0]}
|
x={$viewport.x % scaledGap[0]}
|
||||||
y={$transform[1] % scaledGap[1]}
|
y={$viewport.y % scaledGap[1]}
|
||||||
width={scaledGap[0]}
|
width={scaledGap[0]}
|
||||||
height={scaledGap[1]}
|
height={scaledGap[1]}
|
||||||
patternUnits="userSpaceOnUse"
|
patternUnits="userSpaceOnUse"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
zoomIn,
|
zoomIn,
|
||||||
zoomOut,
|
zoomOut,
|
||||||
fitView,
|
fitView,
|
||||||
transform,
|
viewport,
|
||||||
minZoom,
|
minZoom,
|
||||||
maxZoom,
|
maxZoom,
|
||||||
nodesDraggable,
|
nodesDraggable,
|
||||||
@@ -43,8 +43,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
$: isInteractive = $nodesDraggable || $nodesConnectable || $elementsSelectable;
|
$: isInteractive = $nodesDraggable || $nodesConnectable || $elementsSelectable;
|
||||||
$: minZoomReached = $transform[2] <= $minZoom;
|
$: minZoomReached = $viewport.zoom <= $minZoom;
|
||||||
$: maxZoomReached = $transform[2] >= $maxZoom;
|
$: maxZoomReached = $viewport.zoom >= $maxZoom;
|
||||||
|
|
||||||
const onZoomInHandler = () => {
|
const onZoomInHandler = () => {
|
||||||
zoomIn();
|
zoomIn();
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
const defaultHeight = 150;
|
const defaultHeight = 150;
|
||||||
const {
|
const {
|
||||||
nodes,
|
nodes,
|
||||||
transform,
|
viewport,
|
||||||
width: containerWidth,
|
width: containerWidth,
|
||||||
height: containerHeight,
|
height: containerHeight,
|
||||||
flowId,
|
flowId,
|
||||||
@@ -59,10 +59,10 @@
|
|||||||
const labelledBy = `svelte-flow__minimap-desc-${$flowId}`;
|
const labelledBy = `svelte-flow__minimap-desc-${$flowId}`;
|
||||||
|
|
||||||
$: viewBB = {
|
$: viewBB = {
|
||||||
x: -$transform[0] / $transform[2],
|
x: -$viewport.x / $viewport.zoom,
|
||||||
y: -$transform[1] / $transform[2],
|
y: -$viewport.y / $viewport.zoom,
|
||||||
width: $containerWidth / $transform[2],
|
width: $containerWidth / $viewport.zoom,
|
||||||
height: $containerHeight / $transform[2]
|
height: $containerHeight / $viewport.zoom
|
||||||
};
|
};
|
||||||
$: boundingRect = $nodes.length > 0 ? getBoundsOfRects(getRectOfNodes($nodes), viewBB) : viewBB;
|
$: boundingRect = $nodes.length > 0 ? getBoundsOfRects(getRectOfNodes($nodes), viewBB) : viewBB;
|
||||||
$: elementWidth = width ?? defaultWidth;
|
$: elementWidth = width ?? defaultWidth;
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
style:--minimap-mask-stroke-width-props={maskStrokeWidth}
|
style:--minimap-mask-stroke-width-props={maskStrokeWidth}
|
||||||
use:interactive={{
|
use:interactive={{
|
||||||
panZoom: $panZoom,
|
panZoom: $panZoom,
|
||||||
transform,
|
viewport,
|
||||||
getViewScale,
|
getViewScale,
|
||||||
translateExtent: $translateExtent,
|
translateExtent: $translateExtent,
|
||||||
width: $containerWidth,
|
width: $containerWidth,
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { get, type Writable } from 'svelte/store';
|
|||||||
import {
|
import {
|
||||||
XYMinimap,
|
XYMinimap,
|
||||||
type PanZoomInstance,
|
type PanZoomInstance,
|
||||||
type Transform,
|
type Viewport,
|
||||||
type XYMinimapUpdate
|
type XYMinimapUpdate
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
export type UseInteractiveParams = {
|
export type UseInteractiveParams = {
|
||||||
panZoom: PanZoomInstance;
|
panZoom: PanZoomInstance;
|
||||||
transform: Writable<Transform>;
|
viewport: Writable<Viewport>;
|
||||||
getViewScale: () => number;
|
getViewScale: () => number;
|
||||||
} & XYMinimapUpdate;
|
} & XYMinimapUpdate;
|
||||||
|
|
||||||
@@ -16,7 +16,10 @@ export default function interactive(domNode: Element, params: UseInteractivePara
|
|||||||
const minimap = XYMinimap({
|
const minimap = XYMinimap({
|
||||||
domNode,
|
domNode,
|
||||||
panZoom: params.panZoom,
|
panZoom: params.panZoom,
|
||||||
getTransform: () => get(params.transform),
|
getTransform: () => {
|
||||||
|
const viewport = get(params.viewport);
|
||||||
|
return [viewport.x, viewport.y, viewport.zoom];
|
||||||
|
},
|
||||||
getViewScale: params.getViewScale
|
getViewScale: params.getViewScale
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ export function getDerivedConnectionProps(
|
|||||||
store.connectionLineType,
|
store.connectionLineType,
|
||||||
store.connectionMode,
|
store.connectionMode,
|
||||||
store.nodes,
|
store.nodes,
|
||||||
store.transform
|
store.viewport
|
||||||
],
|
],
|
||||||
([connection, connectionLineType, connectionMode, nodes, transform]) => {
|
([connection, connectionLineType, connectionMode, nodes, viewport]) => {
|
||||||
if (!connection.connectionStartHandle?.nodeId) {
|
if (!connection.connectionStartHandle?.nodeId) {
|
||||||
return initConnectionProps;
|
return initConnectionProps;
|
||||||
}
|
}
|
||||||
@@ -91,8 +91,8 @@ export function getDerivedConnectionProps(
|
|||||||
sourceX: fromX,
|
sourceX: fromX,
|
||||||
sourceY: fromY,
|
sourceY: fromY,
|
||||||
sourcePosition: fromPosition,
|
sourcePosition: fromPosition,
|
||||||
targetX: ((connection.connectionPosition?.x ?? 0) - transform[0]) / transform[2],
|
targetX: ((connection.connectionPosition?.x ?? 0) - viewport.x) / viewport.zoom,
|
||||||
targetY: ((connection.connectionPosition?.y ?? 0) - transform[1]) / transform[2],
|
targetY: ((connection.connectionPosition?.y ?? 0) - viewport.y) / viewport.zoom,
|
||||||
targetPosition: toPosition
|
targetPosition: toPosition
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ export function getEdgeTree(store: SvelteFlowStoreState) {
|
|||||||
store.edges,
|
store.edges,
|
||||||
store.nodes,
|
store.nodes,
|
||||||
store.onlyRenderVisibleElements,
|
store.onlyRenderVisibleElements,
|
||||||
store.transform,
|
store.viewport,
|
||||||
store.width,
|
store.width,
|
||||||
store.height
|
store.height
|
||||||
],
|
],
|
||||||
([edges, nodes, onlyRenderVisibleElements, transform, width, height]) => {
|
([edges, nodes, onlyRenderVisibleElements, viewport, width, height]) => {
|
||||||
const visibleEdges =
|
const visibleEdges =
|
||||||
onlyRenderVisibleElements && width && height
|
onlyRenderVisibleElements && width && height
|
||||||
? edges.filter((edge) => {
|
? edges.filter((edge) => {
|
||||||
@@ -29,7 +29,7 @@ export function getEdgeTree(store: SvelteFlowStoreState) {
|
|||||||
targetNode,
|
targetNode,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
transform
|
transform: [viewport.x, viewport.y, viewport.zoom]
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { addEdge as addEdgeUtil } from '$lib/utils';
|
|||||||
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions, ConnectionData } from '$lib/types';
|
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions, ConnectionData } from '$lib/types';
|
||||||
import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store';
|
import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store';
|
||||||
import type { SvelteFlowStore } from './types';
|
import type { SvelteFlowStore } from './types';
|
||||||
import { syncNodeStores, syncEdgeStores } from './utils';
|
import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils';
|
||||||
import { getEdgeTree } from './edge-tree';
|
import { getEdgeTree } from './edge-tree';
|
||||||
import { getVisibleNodes } from './visible-nodes';
|
import { getVisibleNodes } from './visible-nodes';
|
||||||
import { getDerivedConnectionProps } from './derived-connection-props';
|
import { getDerivedConnectionProps } from './derived-connection-props';
|
||||||
@@ -241,10 +241,11 @@ export function createStore(): SvelteFlowStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function panBy(delta: XYPosition) {
|
function panBy(delta: XYPosition) {
|
||||||
|
const viewport = get(store.viewport);
|
||||||
return panBySystem({
|
return panBySystem({
|
||||||
delta,
|
delta,
|
||||||
panZoom: get(store.panZoom),
|
panZoom: get(store.panZoom),
|
||||||
transform: get(store.transform),
|
transform: [viewport.x, viewport.y, viewport.zoom],
|
||||||
translateExtent: get(store.translateExtent),
|
translateExtent: get(store.translateExtent),
|
||||||
width: get(store.width),
|
width: get(store.width),
|
||||||
height: get(store.height)
|
height: get(store.height)
|
||||||
@@ -298,6 +299,7 @@ export function createStore(): SvelteFlowStore {
|
|||||||
// actions
|
// actions
|
||||||
syncNodeStores: (nodes) => syncNodeStores(store.nodes, nodes),
|
syncNodeStores: (nodes) => syncNodeStores(store.nodes, nodes),
|
||||||
syncEdgeStores: (edges) => syncEdgeStores(store.edges, edges),
|
syncEdgeStores: (edges) => syncEdgeStores(store.edges, edges),
|
||||||
|
syncViewport: (viewport) => syncViewportStores(store.panZoom, store.viewport, viewport),
|
||||||
setNodeTypes,
|
setNodeTypes,
|
||||||
setEdgeTypes,
|
setEdgeTypes,
|
||||||
addEdge,
|
addEdge,
|
||||||
@@ -305,9 +307,7 @@ export function createStore(): SvelteFlowStore {
|
|||||||
updateNodeDimensions,
|
updateNodeDimensions,
|
||||||
zoomIn,
|
zoomIn,
|
||||||
zoomOut,
|
zoomOut,
|
||||||
fitView: (options?: FitViewOptions) => {
|
fitView: (options?: FitViewOptions) => fitView(get(store.nodes), options),
|
||||||
return fitView(get(store.nodes), options);
|
|
||||||
},
|
|
||||||
setMinZoom,
|
setMinZoom,
|
||||||
setMaxZoom,
|
setMaxZoom,
|
||||||
setTranslateExtent,
|
setTranslateExtent,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
ConnectionMode,
|
ConnectionMode,
|
||||||
ConnectionLineType,
|
ConnectionLineType,
|
||||||
type SelectionRect,
|
type SelectionRect,
|
||||||
type Transform,
|
|
||||||
type SnapGrid,
|
type SnapGrid,
|
||||||
type MarkerProps,
|
type MarkerProps,
|
||||||
type PanZoomInstance,
|
type PanZoomInstance,
|
||||||
@@ -14,7 +13,8 @@ import {
|
|||||||
type GroupedEdges,
|
type GroupedEdges,
|
||||||
type NodeOrigin,
|
type NodeOrigin,
|
||||||
type OnError,
|
type OnError,
|
||||||
devWarn
|
devWarn,
|
||||||
|
type Viewport
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
|
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
|
||||||
@@ -72,7 +72,7 @@ export const getInitialStore = () => ({
|
|||||||
selectionMode: writable<SelectionMode>(SelectionMode.Partial),
|
selectionMode: writable<SelectionMode>(SelectionMode.Partial),
|
||||||
nodeTypes: writable<NodeTypes>(initialNodeTypes),
|
nodeTypes: writable<NodeTypes>(initialNodeTypes),
|
||||||
edgeTypes: writable<EdgeTypes>(initialEdgeTypes),
|
edgeTypes: writable<EdgeTypes>(initialEdgeTypes),
|
||||||
transform: writable<Transform>([0, 0, 1]),
|
viewport: writable<Viewport>({ x: 0, y: 0, zoom: 1 }),
|
||||||
connectionMode: writable<ConnectionMode>(ConnectionMode.Strict),
|
connectionMode: writable<ConnectionMode>(ConnectionMode.Strict),
|
||||||
domNode: writable<HTMLDivElement | null>(null),
|
domNode: writable<HTMLDivElement | null>(null),
|
||||||
connection: readable<ConnectionProps>(initConnectionProps),
|
connection: readable<ConnectionProps>(initConnectionProps),
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import type {
|
|||||||
Connection,
|
Connection,
|
||||||
UpdateNodePositions,
|
UpdateNodePositions,
|
||||||
CoordinateExtent,
|
CoordinateExtent,
|
||||||
UpdateConnection
|
UpdateConnection,
|
||||||
|
Viewport
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import type { getInitialStore } from './initial-store';
|
import type { getInitialStore } from './initial-store';
|
||||||
@@ -15,6 +16,7 @@ import type { Node, Edge, NodeTypes, EdgeTypes, FitViewOptions } from '$lib/type
|
|||||||
export type SvelteFlowStoreActions = {
|
export type SvelteFlowStoreActions = {
|
||||||
syncNodeStores: (nodesStore: Writable<Node[]>) => void;
|
syncNodeStores: (nodesStore: Writable<Node[]>) => void;
|
||||||
syncEdgeStores: (edgeStore: Writable<Edge[]>) => void;
|
syncEdgeStores: (edgeStore: Writable<Edge[]>) => void;
|
||||||
|
syncViewport: (viewportStore?: Writable<Viewport>) => void;
|
||||||
setNodeTypes: (nodeTypes: NodeTypes) => void;
|
setNodeTypes: (nodeTypes: NodeTypes) => void;
|
||||||
setEdgeTypes: (edgeTypes: EdgeTypes) => void;
|
setEdgeTypes: (edgeTypes: EdgeTypes) => void;
|
||||||
addEdge: (edge: Edge | Connection) => void;
|
addEdge: (edge: Edge | Connection) => void;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
type Writable,
|
type Writable,
|
||||||
get
|
get
|
||||||
} from 'svelte/store';
|
} from 'svelte/store';
|
||||||
import { updateNodes } from '@xyflow/system';
|
import { updateNodes, type Transform, type Viewport, type PanZoomInstance } from '@xyflow/system';
|
||||||
|
|
||||||
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types';
|
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types';
|
||||||
|
|
||||||
@@ -56,6 +56,54 @@ export function syncEdgeStores(
|
|||||||
edgesStore.update = userEdgesStore.update = (fn: (nds: Edge[]) => Edge[]) => _set(fn(val));
|
edgesStore.update = userEdgesStore.update = (fn: (nds: Edge[]) => Edge[]) => _set(fn(val));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// it is possible to pass a viewport store to SvelteFlow for having more control
|
||||||
|
// if that's the case we need to sync the internal viewport with the user viewport
|
||||||
|
export const syncViewportStores = (
|
||||||
|
panZoomStore: Writable<PanZoomInstance | null>,
|
||||||
|
viewportStore: Writable<Viewport>,
|
||||||
|
userViewportStore?: Writable<Viewport>
|
||||||
|
) => {
|
||||||
|
if (!userViewportStore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panZoom = get(panZoomStore);
|
||||||
|
|
||||||
|
const viewportStoreSetter = viewportStore.set;
|
||||||
|
const userViewportStoreSetter = userViewportStore.set;
|
||||||
|
|
||||||
|
let val = userViewportStore ? get(userViewportStore) : { x: 0, y: 0, zoom: 1 };
|
||||||
|
viewportStore.set(val);
|
||||||
|
|
||||||
|
viewportStore.set = (vp: Viewport) => {
|
||||||
|
viewportStoreSetter(vp);
|
||||||
|
userViewportStoreSetter(vp);
|
||||||
|
|
||||||
|
val = vp;
|
||||||
|
|
||||||
|
return vp;
|
||||||
|
};
|
||||||
|
|
||||||
|
userViewportStore.set = (vp: Viewport) => {
|
||||||
|
panZoom?.syncViewport(vp);
|
||||||
|
|
||||||
|
viewportStoreSetter(vp);
|
||||||
|
userViewportStoreSetter(vp);
|
||||||
|
|
||||||
|
val = vp;
|
||||||
|
|
||||||
|
return vp;
|
||||||
|
};
|
||||||
|
|
||||||
|
viewportStore.update = (fn: (vp: Viewport) => Viewport) => {
|
||||||
|
viewportStore.set(fn(val));
|
||||||
|
};
|
||||||
|
|
||||||
|
userViewportStore.update = (fn: (vp: Viewport) => Viewport) => {
|
||||||
|
userViewportStore.set(fn(val));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type NodeStoreOptions = {
|
export type NodeStoreOptions = {
|
||||||
elevateNodesOnSelect?: boolean;
|
elevateNodesOnSelect?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { derived } from 'svelte/store';
|
import { derived } from 'svelte/store';
|
||||||
import { getNodesInside } from '@xyflow/system';
|
import { getNodesInside, type Transform } from '@xyflow/system';
|
||||||
|
|
||||||
import type { Node } from '$lib/types';
|
import type { Node } from '$lib/types';
|
||||||
import type { SvelteFlowStoreState } from './types';
|
import type { SvelteFlowStoreState } from './types';
|
||||||
|
|
||||||
export function getVisibleNodes(store: SvelteFlowStoreState) {
|
export function getVisibleNodes(store: SvelteFlowStoreState) {
|
||||||
return derived(
|
return derived(
|
||||||
[store.nodes, store.onlyRenderVisibleElements, store.width, store.height, store.transform],
|
[store.nodes, store.onlyRenderVisibleElements, store.width, store.height, store.viewport],
|
||||||
([nodes, onlyRenderVisibleElements, width, height, transform]) => {
|
([nodes, onlyRenderVisibleElements, width, height, viewport]) => {
|
||||||
|
const transform: Transform = [viewport.x, viewport.y, viewport.zoom];
|
||||||
return onlyRenderVisibleElements
|
return onlyRenderVisibleElements
|
||||||
? getNodesInside<Node>(nodes, { x: 0, y: 0, width, height }, transform, true)
|
? getNodesInside<Node>(nodes, { x: 0, y: 0, width, height }, transform, true)
|
||||||
: nodes;
|
: nodes;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ZoomTransform } from 'd3-zoom';
|
import type { ZoomTransform } from 'd3-zoom';
|
||||||
|
|
||||||
import { CoordinateExtent, PanOnScrollMode, Transform, Viewport } from './';
|
import { PanOnScrollMode, type CoordinateExtent, type Transform, type Viewport } from './';
|
||||||
|
|
||||||
export type OnDraggingChange = (dragging: boolean) => void;
|
export type OnDraggingChange = (dragging: boolean) => void;
|
||||||
export type OnTransformChange = (transform: Transform) => void;
|
export type OnTransformChange = (transform: Transform) => void;
|
||||||
@@ -55,4 +55,5 @@ export type PanZoomInstance = {
|
|||||||
setTranslateExtent: (translateExtent: CoordinateExtent) => void;
|
setTranslateExtent: (translateExtent: CoordinateExtent) => void;
|
||||||
scaleTo: (scale: number, options?: PanZoomTransformOptions) => void;
|
scaleTo: (scale: number, options?: PanZoomTransformOptions) => void;
|
||||||
scaleBy: (factor: number, options?: PanZoomTransformOptions) => void;
|
scaleBy: (factor: number, options?: PanZoomTransformOptions) => void;
|
||||||
|
syncViewport: (viewport: Viewport) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -194,6 +194,22 @@ export function XYPanZoom({
|
|||||||
return nextTransform;
|
return nextTransform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncViewport(viewport: Viewport) {
|
||||||
|
if (d3Selection) {
|
||||||
|
const nextTransform = viewportToTransform(viewport);
|
||||||
|
const currentTransform = d3Selection.property('__zoom');
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentTransform.k !== viewport.zoom ||
|
||||||
|
currentTransform.x !== viewport.x ||
|
||||||
|
currentTransform.y !== viewport.y
|
||||||
|
) {
|
||||||
|
// @ts-ignore
|
||||||
|
d3ZoomInstance?.transform(d3Selection, nextTransform, null, { sync: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getViewport(): Viewport {
|
function getViewport(): Viewport {
|
||||||
const transform = d3Selection ? zoomTransform(d3Selection.node() as Element) : { x: 0, y: 0, k: 1 };
|
const transform = d3Selection ? zoomTransform(d3Selection.node() as Element) : { x: 0, y: 0, k: 1 };
|
||||||
return { x: transform.x, y: transform.y, zoom: transform.k };
|
return { x: transform.x, y: transform.y, zoom: transform.k };
|
||||||
@@ -229,5 +245,6 @@ export function XYPanZoom({
|
|||||||
scaleBy,
|
scaleBy,
|
||||||
setScaleExtent,
|
setScaleExtent,
|
||||||
setTranslateExtent,
|
setTranslateExtent,
|
||||||
|
syncViewport,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,9 @@ export function createPanZoomHandler({
|
|||||||
onPaneContextMenu && isRightClickPan(panOnDrag, zoomPanValues.mouseButton ?? 0)
|
onPaneContextMenu && isRightClickPan(panOnDrag, zoomPanValues.mouseButton ?? 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
onTransformChange([event.transform.x, event.transform.y, event.transform.k]);
|
if (!event.sourceEvent?.sync) {
|
||||||
|
onTransformChange([event.transform.x, event.transform.y, event.transform.k]);
|
||||||
|
}
|
||||||
|
|
||||||
if (onPanZoom && !event.sourceEvent?.internal) {
|
if (onPanZoom && !event.sourceEvent?.internal) {
|
||||||
onPanZoom?.(event.sourceEvent as MouseEvent | TouchEvent, transformToViewport(event.transform));
|
onPanZoom?.(event.sourceEvent as MouseEvent | TouchEvent, transformToViewport(event.transform));
|
||||||
|
|||||||
Reference in New Issue
Block a user