extract all logic to system

This commit is contained in:
peterkogo
2024-01-08 15:37:34 +01:00
parent da47117585
commit 4a1d5be831
9 changed files with 452 additions and 328 deletions
-4
View File
@@ -49,12 +49,8 @@
},
"dependencies": {
"@types/d3": "^7.4.0",
"@types/d3-drag": "^3.0.1",
"@types/d3-selection": "^3.0.3",
"@xyflow/system": "workspace:*",
"classcat": "^5.0.3",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0",
"zustand": "^4.4.0"
},
"peerDependencies": {
@@ -1,28 +1,11 @@
import { useRef, useEffect, memo } from 'react';
import cc from 'classcat';
import { drag } from 'd3-drag';
import { select } from 'd3-selection';
import {
getPointerPosition,
getControlDirection,
getDimensionsAfterResize,
getPositionAfterResize,
} from '@xyflow/system';
import { XYResizerInstance, XYResizer, XYResizeChange } from '@xyflow/system';
import { ResizeControlVariant } from '@xyflow/system';
import { useStoreApi } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext';
import type { NodeChange, NodeDimensionChange, NodePositionChange } from '../../types';
import { type ResizeDragEvent, type ResizeControlProps, type ResizeControlLineProps } from './types';
import { getResizeDirection, ResizeControlVariant } from '@xyflow/system';
const initPrevValues = { width: 0, height: 0, x: 0, y: 0 };
const initStartValues = {
...initPrevValues,
pointerX: 0,
pointerY: 0,
aspectRatio: 1,
};
import { type ResizeControlProps, type ResizeControlLineProps } from './types';
function ResizeControl({
nodeId,
@@ -46,146 +29,93 @@ function ResizeControl({
const id = typeof nodeId === 'string' ? nodeId : contextNodeId;
const store = useStoreApi();
const resizeControlRef = useRef<HTMLDivElement>(null);
const startValues = useRef<typeof initStartValues>(initStartValues);
const prevValues = useRef<typeof initPrevValues>(initPrevValues);
const defaultPosition = variant === ResizeControlVariant.Line ? 'right' : 'bottom-right';
const controlPosition = position ?? defaultPosition;
const resizer = useRef<XYResizerInstance | null>(null);
useEffect(() => {
if (!resizeControlRef.current || !id) {
return;
}
const selection = select(resizeControlRef.current);
if (!resizer.current) {
resizer.current = XYResizer({
domNode: resizeControlRef.current,
nodeId: id,
getStoreItems: () => {
const { nodeLookup, transform, snapGrid, snapToGrid } = store.getState();
return {
nodeLookup,
transform,
snapGrid,
snapToGrid,
};
},
onChange: (change: XYResizeChange) => {
const { triggerNodeChanges } = store.getState();
const controlDirection = getControlDirection(controlPosition);
const dragHandler = drag<HTMLDivElement, unknown>()
.on('start', (event: ResizeDragEvent) => {
const { nodeLookup, transform, snapGrid, snapToGrid } = store.getState();
const node = nodeLookup.get(id);
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
prevValues.current = {
width: node?.computed?.width ?? 0,
height: node?.computed?.height ?? 0,
x: node?.position.x ?? 0,
y: node?.position.y ?? 0,
};
startValues.current = {
...prevValues.current,
pointerX: xSnapped,
pointerY: ySnapped,
aspectRatio: prevValues.current.width / prevValues.current.height,
};
onResizeStart?.(event, { ...prevValues.current });
})
.on('drag', (event: ResizeDragEvent) => {
const { nodeLookup, transform, snapGrid, snapToGrid, triggerNodeChanges } = store.getState();
const pointerPosition = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
const node = nodeLookup.get(id);
if (node) {
const changes: NodeChange[] = [];
const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues.current;
if (change.isXPosChange || change.isYPosChange) {
const positionChange: NodePositionChange = {
id,
type: 'position',
position: {
x: change.x,
y: change.y,
},
};
const { width, height } = getDimensionsAfterResize(
startValues.current,
controlDirection,
pointerPosition,
{ minWidth, minHeight, maxWidth, maxHeight },
keepAspectRatio
);
const isWidthChange = width !== prevWidth;
const isHeightChange = height !== prevHeight;
if (controlDirection.affectsX || controlDirection.affectsY) {
const { x, y } = getPositionAfterResize(startValues.current, controlDirection, width, height);
// only transform the node if the width or height changes
const isXPosChange = x !== prevX && isWidthChange;
const isYPosChange = y !== prevY && isHeightChange;
if (isXPosChange || isYPosChange) {
const positionChange: NodePositionChange = {
id: node.id,
type: 'position',
position: {
x: isXPosChange ? x : prevX,
y: isYPosChange ? y : prevY,
},
};
changes.push(positionChange);
prevValues.current.x = positionChange.position!.x;
prevValues.current.y = positionChange.position!.y;
}
changes.push(positionChange);
}
if (isWidthChange || isHeightChange) {
if (change.isWidthChange || change.isHeightChange) {
const dimensionChange: NodeDimensionChange = {
id: id,
id,
type: 'dimensions',
updateStyle: true,
resizing: true,
dimensions: {
width: width,
height: height,
width: change.width,
height: change.height,
},
};
changes.push(dimensionChange);
prevValues.current.width = width;
prevValues.current.height = height;
}
if (changes.length === 0) {
return;
}
const direction = getResizeDirection({
width: prevValues.current.width,
prevWidth,
height: prevValues.current.height,
prevHeight,
affectsX: controlDirection.affectsX,
affectsY: controlDirection.affectsY,
});
const nextValues = { ...prevValues.current, direction };
const callResize = shouldResize?.(event, nextValues);
if (callResize === false) {
return;
}
onResize?.(event, nextValues);
triggerNodeChanges(changes);
}
})
.on('end', (event: ResizeDragEvent) => {
const dimensionChange: NodeDimensionChange = {
id: id,
type: 'dimensions',
resizing: false,
};
onResizeEnd?.(event, { ...prevValues.current });
store.getState().triggerNodeChanges([dimensionChange]);
},
onEnd: () => {
const dimensionChange: NodeDimensionChange = {
id: id,
type: 'dimensions',
resizing: false,
};
store.getState().triggerNodeChanges([dimensionChange]);
},
});
}
selection.call(dragHandler);
resizer.current.update({
controlPosition,
boundries: {
minWidth,
minHeight,
maxWidth,
maxHeight,
},
keepAspectRatio,
onResizeStart,
onResize,
onResizeEnd,
shouldResize,
});
return () => {
selection.on('.drag', null);
resizer.current?.destroy();
};
}, [
id,
controlPosition,
minWidth,
minHeight,
@@ -195,6 +125,7 @@ function ResizeControl({
onResizeStart,
onResize,
onResizeEnd,
shouldResize,
]);
const positionClassNames = controlPosition.split('-');
@@ -1,21 +1,14 @@
import type { CSSProperties, ReactNode } from 'react';
import type { D3DragEvent, SubjectPosition } from 'd3-drag';
import type {
XYResizeParams,
XYResizeParamsWithDirection,
XYResizeControlPosition,
XYResizeControlLinePosition,
ResizeControlVariant,
ShouldResize,
OnResizeStart,
OnResize,
OnResizeEnd,
} from '@xyflow/system';
type OnResizeHandler<Params = XYResizeParams, Result = void> = (event: ResizeDragEvent, params: Params) => Result;
export type ResizeDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
export type ShouldResize = OnResizeHandler<XYResizeParamsWithDirection, boolean>;
export type OnResizeStart = OnResizeHandler;
export type OnResize = OnResizeHandler<XYResizeParamsWithDirection>;
export type OnResizeEnd = OnResizeHandler;
export type NodeResizerProps = {
nodeId?: string;
color?: string;
+1 -5
View File
@@ -42,12 +42,8 @@
},
"dependencies": {
"@svelte-put/shortcut": "^3.1.0",
"@types/d3-drag": "^3.0.1",
"@types/d3-selection": "^3.0.3",
"@xyflow/system": "workspace:*",
"classcat": "^5.0.4",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0"
"classcat": "^5.0.4"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.1.0",
@@ -1,17 +1,14 @@
<script lang="ts">
import { getContext, onMount } from 'svelte';
import { drag } from 'd3-drag';
import { select } from 'd3-selection';
import { get } from 'svelte/store';
import cc from 'classcat';
import type { ResizeControlProps, ResizeDragEvent } from './types';
import type { ResizeControlProps } from './types';
import {
ResizeControlVariant,
getPointerPosition,
getResizeDirection,
getControlDirection,
type XYResizeControlPosition,
getDimensionsAfterResize,
getPositionAfterResize
type XYResizerInstance,
XYResizer,
type XYResizeChange
} from '@xyflow/system';
import { useStore } from '$lib/store';
@@ -38,22 +35,13 @@
let className: $$Props['class'] = '';
export { className as class };
const initPrevValues = { width: 0, height: 0, x: 0, y: 0 };
const initStartValues = {
...initPrevValues,
pointerX: 0,
pointerY: 0,
aspectRatio: 1
};
const { nodeLookup, snapGrid, viewport, nodes } = useStore();
const contextNodeId = getContext<string>('svelteflow__node_id');
$: id = typeof nodeId === 'string' ? nodeId : contextNodeId;
let resizeControlRef: HTMLDivElement;
let startValues = { ...initStartValues };
let prevValues = { ...initPrevValues };
let resizeControlRef: HTMLDivElement;
let resizer: XYResizerInstance | null = null;
$: defaultPosition = (
variant === ResizeControlVariant.Line ? 'right' : 'bottom-right'
@@ -61,133 +49,75 @@
$: controlPosition = position ?? defaultPosition;
$: positionClassNames = controlPosition.split('-');
$: colorStyleProp = variant === ResizeControlVariant.Line ? 'border-color' : 'background-color';
$: colorStyleProp = variant === ResizeControlVariant.Line ? 'border-color' : 'background-color';
$: _style = style ?? '';
$: controlStyle = !!color ? `${_style} ${colorStyleProp}: ${color};` : _style;
function updateNodes() {
$nodes = $nodes;
}
onMount(() => {
if (!resizeControlRef || !id) {
return;
}
const selection = select(resizeControlRef);
const controlDirection = getControlDirection(controlPosition);
const dragHandler = drag<HTMLDivElement, unknown>()
.on('start', (event: ResizeDragEvent) => {
const node = $nodeLookup.get(id);
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, {
transform: [$viewport.x, $viewport.y, $viewport.zoom],
snapGrid: $snapGrid ?? undefined,
snapToGrid: !!$snapGrid
});
prevValues = {
width: node?.computed?.width ?? 0,
height: node?.computed?.height ?? 0,
x: node?.position.x ?? 0,
y: node?.position.y ?? 0
};
startValues = {
...prevValues,
pointerX: xSnapped,
pointerY: ySnapped,
aspectRatio: prevValues.width / prevValues.height
};
onResizeStart?.(event, { ...prevValues });
})
.on('drag', (event: ResizeDragEvent) => {
const pointerPosition = getPointerPosition(event.sourceEvent, {
transform: [$viewport.x, $viewport.y, $viewport.zoom],
snapGrid: $snapGrid ?? undefined,
snapToGrid: !!$snapGrid
});
const node = $nodeLookup.get(id);
if (node) {
const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues;
const { width, height } = getDimensionsAfterResize(
startValues,
controlDirection,
pointerPosition,
{
minWidth: _minWidth,
minHeight: _minHeight,
maxWidth: _maxWidth,
maxHeight: _maxHeight
},
!!keepAspectRatio
);
const isWidthChange = width !== prevWidth;
const isHeightChange = height !== prevHeight;
let changes = false;
if (controlDirection.affectsX || controlDirection.affectsY) {
const { x, y } = getPositionAfterResize(startValues, controlDirection, width, height);
// only transform the node if the width or height changes
const isXPosChange = x !== prevX && isWidthChange;
const isYPosChange = y !== prevY && isHeightChange;
if (isXPosChange || isYPosChange) {
let newX = isXPosChange ? x : prevX;
let newY = isYPosChange ? y : prevY;
node.position = { x: newX, y: newY };
prevValues.x = newX;
prevValues.y = newY;
changes = true;
}
}
if (isWidthChange || isHeightChange) {
node.width = width;
node.height = height;
prevValues.width = width;
prevValues.height = height;
changes = true;
}
if (!changes) {
return;
}
const direction = getResizeDirection({
width: prevValues.width,
prevWidth,
height: prevValues.height,
prevHeight,
affectsX: controlDirection.affectsX,
affectsY: controlDirection.affectsY
});
const nextValues = { ...prevValues, direction };
const callResize = shouldResize?.(event, nextValues);
if (callResize === false) {
return;
}
onResize?.(event, nextValues);
$nodes = $nodes;
}
});
selection.call(dragHandler);
return () => {
selection.on('.drag', null);
resizer?.destroy();
};
});
$: {
if (resizeControlRef) {
if (!resizer) {
const _snapGrid = get(snapGrid);
const _viewport = get(viewport);
resizer = XYResizer({
domNode: resizeControlRef,
nodeId: id,
getStoreItems: () => {
return {
nodeLookup: get(nodeLookup),
transform: [_viewport.x, _viewport.y, _viewport.zoom],
snapGrid: _snapGrid ?? undefined,
snapToGrid: !!_snapGrid
};
},
onChange: (change: XYResizeChange) => {
const _nodeLookup = get(nodeLookup);
const node = _nodeLookup.get(id);
if (node) {
if (change.isHeightChange) {
node.height = change.height;
}
if (change.isWidthChange) {
node.width = change.width;
}
if (change.isXPosChange || change.isYPosChange) {
node.position = { x: change.x, y: change.y };
}
// This needs to be a function to prevent unnecessary updates
updateNodes();
}
}
});
}
resizer.update({
controlPosition,
boundries: {
minWidth: _minWidth,
minHeight: _minHeight,
maxWidth: _maxWidth,
maxHeight: _maxHeight
},
keepAspectRatio: !!keepAspectRatio,
onResizeStart,
onResize,
onResizeEnd,
shouldResize
});
}
}
</script>
<div
+199
View File
@@ -0,0 +1,199 @@
import { drag } from 'd3-drag';
import { select } from 'd3-selection';
import { NodeLookup, Transform } from '../types';
import { OnResize, OnResizeEnd, OnResizeStart, ResizeDragEvent, ShouldResize, XYResizeControlPosition } from './types';
import { getControlDirection, getDimensionsAfterResize, getPositionAfterResize, getResizeDirection } from './utils';
import { getPointerPosition } from '../utils';
const initPrevValues = { width: 0, height: 0, x: 0, y: 0 };
const initStartValues = {
...initPrevValues,
pointerX: 0,
pointerY: 0,
aspectRatio: 1,
};
const initChange = {
x: 0,
y: 0,
width: 0,
height: 0,
isXPosChange: false,
isYPosChange: false,
isWidthChange: false,
isHeightChange: false,
};
export type XYResizeChange = {
x: number;
y: number;
width: number;
height: number;
isXPosChange: boolean;
isYPosChange: boolean;
isWidthChange: boolean;
isHeightChange: boolean;
};
type XYResizerParams = {
domNode: HTMLDivElement;
nodeId: string;
getStoreItems: () => {
nodeLookup: NodeLookup;
transform: Transform;
snapGrid?: [number, number];
snapToGrid: boolean;
};
onChange: (changes: XYResizeChange) => void;
onEnd?: () => void;
};
type XYResizerUpdateParams = {
controlPosition: XYResizeControlPosition;
boundries: {
minWidth: number;
minHeight: number;
maxWidth: number;
maxHeight: number;
};
keepAspectRatio: boolean;
onResizeStart: OnResizeStart | undefined;
onResize: OnResize | undefined;
onResizeEnd: OnResizeEnd | undefined;
shouldResize: ShouldResize | undefined;
};
export type XYResizerInstance = {
update: (params: XYResizerUpdateParams) => void;
destroy: () => void;
};
export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResizerParams): XYResizerInstance {
const selection = select(domNode);
function update({
controlPosition,
boundries,
keepAspectRatio,
onResizeStart,
onResize,
onResizeEnd,
shouldResize,
}: XYResizerUpdateParams) {
console.log('updated');
let prevValues = { ...initPrevValues };
let startValues = { ...initStartValues };
const controlDirection = getControlDirection(controlPosition);
const dragHandler = drag<HTMLDivElement, unknown>()
.on('start', (event: ResizeDragEvent) => {
const { nodeLookup, transform, snapGrid, snapToGrid } = getStoreItems();
const node = nodeLookup.get(nodeId);
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
prevValues = {
width: node?.computed?.width ?? 0,
height: node?.computed?.height ?? 0,
x: node?.position.x ?? 0,
y: node?.position.y ?? 0,
};
startValues = {
...prevValues,
pointerX: xSnapped,
pointerY: ySnapped,
aspectRatio: prevValues.width / prevValues.height,
};
onResizeStart?.(event, { ...prevValues });
})
.on('drag', (event: ResizeDragEvent) => {
const { nodeLookup, transform, snapGrid, snapToGrid } = getStoreItems();
const pointerPosition = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
const node = nodeLookup.get(nodeId);
if (node) {
const change = { ...initChange };
const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues;
const { width, height } = getDimensionsAfterResize(
startValues,
controlDirection,
pointerPosition,
boundries,
keepAspectRatio
);
const isWidthChange = width !== prevWidth;
const isHeightChange = height !== prevHeight;
if (controlDirection.affectsX || controlDirection.affectsY) {
const { x, y } = getPositionAfterResize(startValues, controlDirection, width, height);
// only transform the node if the width or height changes
const isXPosChange = x !== prevX && isWidthChange;
const isYPosChange = y !== prevY && isHeightChange;
if (isXPosChange || isYPosChange) {
change.isXPosChange = isXPosChange;
change.isYPosChange = isYPosChange;
change.x = isXPosChange ? x : prevX;
change.y = isYPosChange ? y : prevY;
prevValues.x = change.x;
prevValues.y = change.y;
}
}
if (isWidthChange || isHeightChange) {
change.isWidthChange = isWidthChange;
change.isHeightChange = isHeightChange;
change.width = width;
change.height = height;
prevValues.width = width;
prevValues.height = height;
}
if (!change.isXPosChange && !change.isYPosChange && !isWidthChange && !isHeightChange) {
return;
}
const direction = getResizeDirection({
width: prevValues.width,
prevWidth,
height: prevValues.height,
prevHeight,
affectsX: controlDirection.affectsX,
affectsY: controlDirection.affectsY,
});
const nextValues = { ...prevValues, direction };
const callResize = shouldResize?.(event, nextValues);
if (callResize === false) {
return;
}
onResize?.(event, nextValues);
onChange(change);
}
})
.on('end', (event: ResizeDragEvent) => {
onResizeEnd?.(event, { ...prevValues });
});
selection.call(dragHandler);
}
function destroy() {
selection.on('.drag', null);
}
return {
update,
destroy,
};
}
+1 -1
View File
@@ -1,2 +1,2 @@
export * from './types';
export * from './utils';
export * from './XYResizer';
+9 -1
View File
@@ -1,4 +1,4 @@
export type { D3DragEvent, SubjectPosition } from 'd3-drag';
import type { D3DragEvent, SubjectPosition } from 'd3-drag';
export type XYResizeParams = {
x: number;
@@ -32,3 +32,11 @@ export const XY_RESIZER_HANDLE_CONTROLS: XYResizeControlPosition[] = [
'bottom-right',
];
export const XY_RESIZER_LINE_CONTROLS: XYResizeControlLinePosition[] = ['top', 'right', 'bottom', 'left'];
type OnResizeHandler<Params = XYResizeParams, Result = void> = (event: ResizeDragEvent, params: Params) => Result;
export type ResizeDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
export type ShouldResize = OnResizeHandler<XYResizeParamsWithDirection, boolean>;
export type OnResizeStart = OnResizeHandler;
export type OnResize = OnResizeHandler<XYResizeParamsWithDirection>;
export type OnResizeEnd = OnResizeHandler;