Merge pull request #3774 from xyflow/svelte-node-resizer
Svelte node resizer
This commit is contained in:
@@ -9,7 +9,7 @@ const DefaultResizerNode: FC<NodeProps> = ({ data, selected }) => {
|
|||||||
maxWidth={data.maxWidth ?? undefined}
|
maxWidth={data.maxWidth ?? undefined}
|
||||||
minHeight={data.minHeight ?? undefined}
|
minHeight={data.minHeight ?? undefined}
|
||||||
maxHeight={data.maxHeight ?? undefined}
|
maxHeight={data.maxHeight ?? undefined}
|
||||||
isVisible={data.isVisible ?? selected}
|
isVisible={data.isVisible ?? !!selected}
|
||||||
shouldResize={data.shouldResize ?? undefined}
|
shouldResize={data.shouldResize ?? undefined}
|
||||||
onResizeStart={data.onResizeStart ?? undefined}
|
onResizeStart={data.onResizeStart ?? undefined}
|
||||||
onResize={data.onResize ?? undefined}
|
onResize={data.onResize ?? undefined}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
'interaction',
|
'interaction',
|
||||||
'intersections',
|
'intersections',
|
||||||
'node-toolbar',
|
'node-toolbar',
|
||||||
|
'node-resizer',
|
||||||
'overview',
|
'overview',
|
||||||
'stress',
|
'stress',
|
||||||
'subflows',
|
'subflows',
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { SvelteFlow, Controls, Panel, type Node, type Edge } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
import DefaultResizer from './DefaultResizer.svelte';
|
||||||
|
import CustomResizer from './CustomResizer.svelte';
|
||||||
|
import VerticalResizer from './VerticalResizer.svelte';
|
||||||
|
import HorizontalResizer from './HorizontalResizer.svelte';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
import '@xyflow/svelte/dist/style.css';
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
defaultResizer: DefaultResizer,
|
||||||
|
customResizer: CustomResizer,
|
||||||
|
verticalResizer: VerticalResizer,
|
||||||
|
horizontalResizer: HorizontalResizer
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodeStyle = 'border: 1px solid #222; font-size: 10px; background-color: #ddd;';
|
||||||
|
|
||||||
|
const edges = writable<Edge[]>([]);
|
||||||
|
|
||||||
|
const nodes = writable<Node[]>([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'defaultResizer',
|
||||||
|
data: { label: 'default resizer' },
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
style: nodeStyle
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1a',
|
||||||
|
type: 'defaultResizer',
|
||||||
|
data: {
|
||||||
|
label: 'default resizer with min and max dimensions',
|
||||||
|
minWidth: 100,
|
||||||
|
minHeight: 80,
|
||||||
|
maxWidth: 200,
|
||||||
|
maxHeight: 200
|
||||||
|
},
|
||||||
|
position: { x: 0, y: 60 },
|
||||||
|
style: nodeStyle + ' width: 100px; height: 80px;'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1b',
|
||||||
|
type: 'defaultResizer',
|
||||||
|
data: {
|
||||||
|
label: 'default resizer with initial size and aspect ratio',
|
||||||
|
keepAspectRatio: true,
|
||||||
|
minWidth: 100,
|
||||||
|
minHeight: 60,
|
||||||
|
maxWidth: 400,
|
||||||
|
maxHeight: 400
|
||||||
|
},
|
||||||
|
position: { x: 250, y: 0 },
|
||||||
|
style: nodeStyle + ' width: 174px; height: 123px;'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: 'customResizer',
|
||||||
|
data: { label: 'custom resize icon' },
|
||||||
|
position: { x: 0, y: 200 },
|
||||||
|
style: nodeStyle + ' width: 100px; height: 60px;'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: 'verticalResizer',
|
||||||
|
data: { label: 'vertical resizer' },
|
||||||
|
position: { x: 250, y: 200 },
|
||||||
|
style: nodeStyle
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3a',
|
||||||
|
type: 'verticalResizer',
|
||||||
|
data: {
|
||||||
|
label: 'vertical resizer with min/maxHeight and aspect ratio',
|
||||||
|
minHeight: 50,
|
||||||
|
maxHeight: 200,
|
||||||
|
keepAspectRatio: true
|
||||||
|
},
|
||||||
|
position: { x: 400, y: 200 },
|
||||||
|
style: nodeStyle + ' height: 50px;'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
type: 'horizontalResizer',
|
||||||
|
data: {
|
||||||
|
label: 'horizontal resizer with aspect ratio',
|
||||||
|
keepAspectRatio: true,
|
||||||
|
minHeight: 20,
|
||||||
|
maxHeight: 80,
|
||||||
|
maxWidth: 300
|
||||||
|
},
|
||||||
|
position: { x: 250, y: 300 },
|
||||||
|
style: nodeStyle
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4a',
|
||||||
|
type: 'horizontalResizer',
|
||||||
|
data: { label: 'horizontal resizer with maxWidth', maxWidth: 300 },
|
||||||
|
position: { x: 250, y: 400 },
|
||||||
|
style: nodeStyle
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
let snapToGrid = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SvelteFlow
|
||||||
|
{nodes}
|
||||||
|
{edges}
|
||||||
|
{nodeTypes}
|
||||||
|
minZoom={0.2}
|
||||||
|
maxZoom={5}
|
||||||
|
snapGrid={snapToGrid ? [10, 10] : undefined}
|
||||||
|
fitView
|
||||||
|
>
|
||||||
|
<Controls />
|
||||||
|
<Panel position="bottom-right">
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
snapToGrid = !snapToGrid;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
snapToGrid: {snapToGrid ? 'on' : 'off'}
|
||||||
|
</button>
|
||||||
|
</Panel>
|
||||||
|
</SvelteFlow>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Handle, Position, type NodeProps, NodeResizeControl } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
type $$Props = NodeProps;
|
||||||
|
|
||||||
|
export let data: $$Props['data'] = undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NodeResizeControl
|
||||||
|
minWidth={data.minWidth ?? undefined}
|
||||||
|
maxWidth={data.maxWidth ?? undefined}
|
||||||
|
minHeight={data.minHeight ?? undefined}
|
||||||
|
maxHeight={data.maxHeight ?? undefined}
|
||||||
|
shouldResize={data.shouldResize ?? undefined}
|
||||||
|
onResizeStart={data.onResizeStart ?? undefined}
|
||||||
|
onResize={data.onResize ?? undefined}
|
||||||
|
onResizeEnd={data.onResizeEnd ?? undefined}
|
||||||
|
keepAspectRatio={data.keepAspectRatio ?? undefined}
|
||||||
|
style="background: transparent; border: none;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="8"
|
||||||
|
height="8"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
style="position: absolute; right: 2px; bottom: 2px;"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<polyline points="16 20 20 20 20 16" />
|
||||||
|
<line x1="14" y1="14" x2="20" y2="20" />
|
||||||
|
<polyline points="8 4 4 4 4 8" />
|
||||||
|
<line x1="4" y1="4" x2="10" y2="10" />
|
||||||
|
</svg>
|
||||||
|
</NodeResizeControl>
|
||||||
|
<Handle type="target" position={Position.Left} />
|
||||||
|
<div>{data.label}</div>
|
||||||
|
<Handle type="source" position={Position.Right} />
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Handle, NodeResizer, Position, type NodeProps } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
type $$Props = NodeProps;
|
||||||
|
|
||||||
|
export let data: $$Props['data'] = undefined;
|
||||||
|
export let selected: $$Props['selected'] = undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NodeResizer
|
||||||
|
minWidth={data.minWidth ?? undefined}
|
||||||
|
maxWidth={data.maxWidth ?? undefined}
|
||||||
|
minHeight={data.minHeight ?? undefined}
|
||||||
|
maxHeight={data.maxHeight ?? undefined}
|
||||||
|
isVisible={data.isVisible ?? !!selected}
|
||||||
|
shouldResize={data.shouldResize ?? undefined}
|
||||||
|
onResizeStart={data.onResizeStart ?? undefined}
|
||||||
|
onResize={data.onResize ?? undefined}
|
||||||
|
onResizeEnd={data.onResizeEnd ?? undefined}
|
||||||
|
keepAspectRatio={data.keepAspectRatio ?? undefined}
|
||||||
|
/>
|
||||||
|
<Handle type="target" position={Position.Left} />
|
||||||
|
<div>{data.label}</div>
|
||||||
|
<Handle type="source" position={Position.Right} />
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Handle, NodeResizeControl, Position, type NodeProps } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
type $$Props = NodeProps;
|
||||||
|
|
||||||
|
export let data: $$Props['data'] = undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NodeResizeControl
|
||||||
|
minWidth={data.minWidth ?? undefined}
|
||||||
|
maxWidth={data.maxWidth ?? undefined}
|
||||||
|
minHeight={data.minHeight ?? undefined}
|
||||||
|
maxHeight={data.maxHeight ?? undefined}
|
||||||
|
shouldResize={data.shouldResize ?? undefined}
|
||||||
|
onResizeStart={data.onResizeStart ?? undefined}
|
||||||
|
onResize={data.onResize ?? undefined}
|
||||||
|
onResizeEnd={data.onResizeEnd ?? undefined}
|
||||||
|
keepAspectRatio={data.keepAspectRatio ?? undefined}
|
||||||
|
color="red"
|
||||||
|
position={Position.Left}
|
||||||
|
/>
|
||||||
|
<NodeResizeControl
|
||||||
|
minWidth={data.minWidth ?? undefined}
|
||||||
|
maxWidth={data.maxWidth ?? undefined}
|
||||||
|
minHeight={data.minHeight ?? undefined}
|
||||||
|
maxHeight={data.maxHeight ?? undefined}
|
||||||
|
shouldResize={data.shouldResize ?? undefined}
|
||||||
|
onResizeStart={data.onResizeStart ?? undefined}
|
||||||
|
onResize={data.onResize ?? undefined}
|
||||||
|
onResizeEnd={data.onResizeEnd ?? undefined}
|
||||||
|
keepAspectRatio={data.keepAspectRatio ?? undefined}
|
||||||
|
color="red"
|
||||||
|
position={Position.Right}
|
||||||
|
/>
|
||||||
|
<Handle type="target" position={Position.Top} />
|
||||||
|
<div style="padding: 10px;">{data.label}</div>
|
||||||
|
<Handle type="source" position={Position.Bottom} />
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
function ResizeIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="8"
|
||||||
|
height="8"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
style={{ position: 'absolute', right: 2, bottom: 2 }}
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<polyline points="16 20 20 20 20 16" />
|
||||||
|
<line x1="14" y1="14" x2="20" y2="20" />
|
||||||
|
<polyline points="8 4 4 4 4 8" />
|
||||||
|
<line x1="4" y1="4" x2="10" y2="10" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResizeIcon;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Handle, NodeResizeControl, Position, type NodeProps } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
type $$Props = NodeProps;
|
||||||
|
|
||||||
|
export let data: $$Props['data'] = undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NodeResizeControl
|
||||||
|
minWidth={data.minWidth ?? undefined}
|
||||||
|
maxWidth={data.maxWidth ?? undefined}
|
||||||
|
minHeight={data.minHeight ?? undefined}
|
||||||
|
maxHeight={data.maxHeight ?? undefined}
|
||||||
|
shouldResize={data.shouldResize ?? undefined}
|
||||||
|
onResizeStart={data.onResizeStart ?? undefined}
|
||||||
|
onResize={data.onResize ?? undefined}
|
||||||
|
onResizeEnd={data.onResizeEnd ?? undefined}
|
||||||
|
keepAspectRatio={data.keepAspectRatio ?? undefined}
|
||||||
|
color="red"
|
||||||
|
position={Position.Top}
|
||||||
|
/>
|
||||||
|
<NodeResizeControl
|
||||||
|
minWidth={data.minWidth ?? undefined}
|
||||||
|
maxWidth={data.maxWidth ?? undefined}
|
||||||
|
minHeight={data.minHeight ?? undefined}
|
||||||
|
maxHeight={data.maxHeight ?? undefined}
|
||||||
|
shouldResize={data.shouldResize ?? undefined}
|
||||||
|
onResizeStart={data.onResizeStart ?? undefined}
|
||||||
|
onResize={data.onResize ?? undefined}
|
||||||
|
onResizeEnd={data.onResizeEnd ?? undefined}
|
||||||
|
keepAspectRatio={data.keepAspectRatio ?? undefined}
|
||||||
|
color="red"
|
||||||
|
position={Position.Bottom}
|
||||||
|
/>
|
||||||
|
<Handle type="target" position={Position.Left} />
|
||||||
|
<div style="padding: 10px;">{data.label}</div>
|
||||||
|
<Handle type="source" position={Position.Right} />
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
### Minor changes
|
### Minor changes
|
||||||
|
|
||||||
- fix applyChanges: handle multi changes for one node
|
- fix applyChanges: handle multi changes for one node
|
||||||
|
- use `XYResizer` from @xyflow/system
|
||||||
|
|
||||||
## 12.0.0-next.4
|
## 12.0.0-next.4
|
||||||
|
|
||||||
|
|||||||
@@ -48,13 +48,8 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/d3": "^7.4.0",
|
|
||||||
"@types/d3-drag": "^3.0.1",
|
|
||||||
"@types/d3-selection": "^3.0.3",
|
|
||||||
"@xyflow/system": "workspace:*",
|
"@xyflow/system": "workspace:*",
|
||||||
"classcat": "^5.0.3",
|
"classcat": "^5.0.3",
|
||||||
"d3-drag": "^3.0.0",
|
|
||||||
"d3-selection": "^3.0.0",
|
|
||||||
"zustand": "^4.4.0"
|
"zustand": "^4.4.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -82,8 +77,6 @@
|
|||||||
"rollup": {
|
"rollup": {
|
||||||
"globals": {
|
"globals": {
|
||||||
"classcat": "cc",
|
"classcat": "cc",
|
||||||
"d3-selection": "d3Selection",
|
|
||||||
"d3-drag": "d3Drag",
|
|
||||||
"zustand": "zustand",
|
"zustand": "zustand",
|
||||||
"zustand/shallow": "zustandShallow"
|
"zustand/shallow": "zustandShallow"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { useRef, useEffect, memo } from 'react';
|
||||||
|
import cc from 'classcat';
|
||||||
|
import { XYResizer, ResizeControlVariant, type XYResizerInstance, type XYResizerChange } from '@xyflow/system';
|
||||||
|
|
||||||
|
import { useStoreApi } from '../../hooks/useStore';
|
||||||
|
import { useNodeId } from '../../contexts/NodeIdContext';
|
||||||
|
import type { NodeChange, NodeDimensionChange, NodePositionChange } from '../../types';
|
||||||
|
import type { ResizeControlProps, ResizeControlLineProps } from './types';
|
||||||
|
|
||||||
|
function ResizeControl({
|
||||||
|
nodeId,
|
||||||
|
position,
|
||||||
|
variant = ResizeControlVariant.Handle,
|
||||||
|
className,
|
||||||
|
style = {},
|
||||||
|
children,
|
||||||
|
color,
|
||||||
|
minWidth = 10,
|
||||||
|
minHeight = 10,
|
||||||
|
maxWidth = Number.MAX_VALUE,
|
||||||
|
maxHeight = Number.MAX_VALUE,
|
||||||
|
keepAspectRatio = false,
|
||||||
|
shouldResize,
|
||||||
|
onResizeStart,
|
||||||
|
onResize,
|
||||||
|
onResizeEnd,
|
||||||
|
}: ResizeControlProps) {
|
||||||
|
const contextNodeId = useNodeId();
|
||||||
|
const id = typeof nodeId === 'string' ? nodeId : contextNodeId;
|
||||||
|
const store = useStoreApi();
|
||||||
|
const resizeControlRef = useRef<HTMLDivElement>(null);
|
||||||
|
const defaultPosition = variant === ResizeControlVariant.Line ? 'right' : 'bottom-right';
|
||||||
|
const controlPosition = position ?? defaultPosition;
|
||||||
|
|
||||||
|
const resizer = useRef<XYResizerInstance | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!resizeControlRef.current || !id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: XYResizerChange) => {
|
||||||
|
const { triggerNodeChanges } = store.getState();
|
||||||
|
|
||||||
|
const changes: NodeChange[] = [];
|
||||||
|
|
||||||
|
if (change.isXPosChange || change.isYPosChange) {
|
||||||
|
const positionChange: NodePositionChange = {
|
||||||
|
id,
|
||||||
|
type: 'position',
|
||||||
|
position: {
|
||||||
|
x: change.x,
|
||||||
|
y: change.y,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
changes.push(positionChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change.isWidthChange || change.isHeightChange) {
|
||||||
|
const dimensionChange: NodeDimensionChange = {
|
||||||
|
id,
|
||||||
|
type: 'dimensions',
|
||||||
|
resizing: true,
|
||||||
|
dimensions: {
|
||||||
|
width: change.width,
|
||||||
|
height: change.height,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
changes.push(dimensionChange);
|
||||||
|
}
|
||||||
|
triggerNodeChanges(changes);
|
||||||
|
},
|
||||||
|
onEnd: () => {
|
||||||
|
const dimensionChange: NodeDimensionChange = {
|
||||||
|
id: id,
|
||||||
|
type: 'dimensions',
|
||||||
|
resizing: false,
|
||||||
|
};
|
||||||
|
store.getState().triggerNodeChanges([dimensionChange]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resizer.current.update({
|
||||||
|
controlPosition,
|
||||||
|
boundaries: {
|
||||||
|
minWidth,
|
||||||
|
minHeight,
|
||||||
|
maxWidth,
|
||||||
|
maxHeight,
|
||||||
|
},
|
||||||
|
keepAspectRatio,
|
||||||
|
onResizeStart,
|
||||||
|
onResize,
|
||||||
|
onResizeEnd,
|
||||||
|
shouldResize,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizer.current?.destroy();
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
controlPosition,
|
||||||
|
minWidth,
|
||||||
|
minHeight,
|
||||||
|
maxWidth,
|
||||||
|
maxHeight,
|
||||||
|
keepAspectRatio,
|
||||||
|
onResizeStart,
|
||||||
|
onResize,
|
||||||
|
onResizeEnd,
|
||||||
|
shouldResize,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const positionClassNames = controlPosition.split('-');
|
||||||
|
const colorStyleProp = variant === ResizeControlVariant.Line ? 'borderColor' : 'backgroundColor';
|
||||||
|
const controlStyle = color ? { ...style, [colorStyleProp]: color } : style;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cc(['react-flow__resize-control', 'nodrag', ...positionClassNames, variant, className])}
|
||||||
|
ref={resizeControlRef}
|
||||||
|
style={controlStyle}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResizeControlLine(props: ResizeControlLineProps) {
|
||||||
|
return <ResizeControl {...props} variant={ResizeControlVariant.Line} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NodeResizeControl = memo(ResizeControl);
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import ResizeControl from './ResizeControl';
|
import { ResizeControlVariant, XY_RESIZER_HANDLE_POSITIONS, XY_RESIZER_LINE_POSITIONS } from '@xyflow/system';
|
||||||
import { ControlPosition, NodeResizerProps, ResizeControlVariant, ControlLinePosition } from './types';
|
|
||||||
|
|
||||||
const handleControls: ControlPosition[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
import { NodeResizeControl } from './NodeResizeControl';
|
||||||
const lineControls: ControlLinePosition[] = ['top', 'right', 'bottom', 'left'];
|
import type { NodeResizerProps } from './types';
|
||||||
|
|
||||||
export default function NodeResizer({
|
export function NodeResizer({
|
||||||
nodeId,
|
nodeId,
|
||||||
isVisible = true,
|
isVisible = true,
|
||||||
handleClassName,
|
handleClassName,
|
||||||
@@ -28,13 +27,13 @@ export default function NodeResizer({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{lineControls.map((c) => (
|
{XY_RESIZER_LINE_POSITIONS.map((position) => (
|
||||||
<ResizeControl
|
<NodeResizeControl
|
||||||
key={c}
|
key={position}
|
||||||
className={lineClassName}
|
className={lineClassName}
|
||||||
style={lineStyle}
|
style={lineStyle}
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
position={c}
|
position={position}
|
||||||
variant={ResizeControlVariant.Line}
|
variant={ResizeControlVariant.Line}
|
||||||
color={color}
|
color={color}
|
||||||
minWidth={minWidth}
|
minWidth={minWidth}
|
||||||
@@ -48,13 +47,13 @@ export default function NodeResizer({
|
|||||||
onResizeEnd={onResizeEnd}
|
onResizeEnd={onResizeEnd}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{handleControls.map((c) => (
|
{XY_RESIZER_HANDLE_POSITIONS.map((position) => (
|
||||||
<ResizeControl
|
<NodeResizeControl
|
||||||
key={c}
|
key={position}
|
||||||
className={handleClassName}
|
className={handleClassName}
|
||||||
style={handleStyle}
|
style={handleStyle}
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
position={c}
|
position={position}
|
||||||
color={color}
|
color={color}
|
||||||
minWidth={minWidth}
|
minWidth={minWidth}
|
||||||
minHeight={minHeight}
|
minHeight={minHeight}
|
||||||
|
|||||||
@@ -1,256 +0,0 @@
|
|||||||
import { useRef, useEffect, memo } from 'react';
|
|
||||||
import cc from 'classcat';
|
|
||||||
import { drag } from 'd3-drag';
|
|
||||||
import { select } from 'd3-selection';
|
|
||||||
import { getPointerPosition, clamp } 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,
|
|
||||||
ResizeControlVariant,
|
|
||||||
} from './types';
|
|
||||||
import { getDirection } from './utils';
|
|
||||||
|
|
||||||
const initPrevValues = { width: 0, height: 0, x: 0, y: 0 };
|
|
||||||
|
|
||||||
const initStartValues = {
|
|
||||||
...initPrevValues,
|
|
||||||
pointerX: 0,
|
|
||||||
pointerY: 0,
|
|
||||||
aspectRatio: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
function ResizeControl({
|
|
||||||
nodeId,
|
|
||||||
position,
|
|
||||||
variant = ResizeControlVariant.Handle,
|
|
||||||
className,
|
|
||||||
style = {},
|
|
||||||
children,
|
|
||||||
color,
|
|
||||||
minWidth = 10,
|
|
||||||
minHeight = 10,
|
|
||||||
maxWidth = Number.MAX_VALUE,
|
|
||||||
maxHeight = Number.MAX_VALUE,
|
|
||||||
keepAspectRatio = false,
|
|
||||||
shouldResize,
|
|
||||||
onResizeStart,
|
|
||||||
onResize,
|
|
||||||
onResizeEnd,
|
|
||||||
}: ResizeControlProps) {
|
|
||||||
const contextNodeId = useNodeId();
|
|
||||||
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;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!resizeControlRef.current || !id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selection = select(resizeControlRef.current);
|
|
||||||
|
|
||||||
const enableX = controlPosition.includes('right') || controlPosition.includes('left');
|
|
||||||
const enableY = controlPosition.includes('bottom') || controlPosition.includes('top');
|
|
||||||
const invertX = controlPosition.includes('left');
|
|
||||||
const invertY = controlPosition.includes('top');
|
|
||||||
|
|
||||||
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 { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
|
||||||
const node = nodeLookup.get(id);
|
|
||||||
|
|
||||||
if (node) {
|
|
||||||
const changes: NodeChange[] = [];
|
|
||||||
const {
|
|
||||||
pointerX: startX,
|
|
||||||
pointerY: startY,
|
|
||||||
width: startWidth,
|
|
||||||
height: startHeight,
|
|
||||||
x: startNodeX,
|
|
||||||
y: startNodeY,
|
|
||||||
aspectRatio,
|
|
||||||
} = startValues.current;
|
|
||||||
|
|
||||||
const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues.current;
|
|
||||||
|
|
||||||
const distX = Math.floor(enableX ? xSnapped - startX : 0);
|
|
||||||
const distY = Math.floor(enableY ? ySnapped - startY : 0);
|
|
||||||
|
|
||||||
let width = clamp(startWidth + (invertX ? -distX : distX), minWidth, maxWidth);
|
|
||||||
let height = clamp(startHeight + (invertY ? -distY : distY), minHeight, maxHeight);
|
|
||||||
|
|
||||||
if (keepAspectRatio) {
|
|
||||||
const nextAspectRatio = width / height;
|
|
||||||
const isDiagonal = enableX && enableY;
|
|
||||||
const isHorizontal = enableX && !enableY;
|
|
||||||
const isVertical = enableY && !enableX;
|
|
||||||
|
|
||||||
width = (nextAspectRatio <= aspectRatio && isDiagonal) || isVertical ? height * aspectRatio : width;
|
|
||||||
height = (nextAspectRatio > aspectRatio && isDiagonal) || isHorizontal ? width / aspectRatio : height;
|
|
||||||
|
|
||||||
if (width >= maxWidth) {
|
|
||||||
width = maxWidth;
|
|
||||||
height = maxWidth / aspectRatio;
|
|
||||||
} else if (width <= minWidth) {
|
|
||||||
width = minWidth;
|
|
||||||
height = minWidth / aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (height >= maxHeight) {
|
|
||||||
height = maxHeight;
|
|
||||||
width = maxHeight * aspectRatio;
|
|
||||||
} else if (height <= minHeight) {
|
|
||||||
height = minHeight;
|
|
||||||
width = minHeight * aspectRatio;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isWidthChange = width !== prevWidth;
|
|
||||||
const isHeightChange = height !== prevHeight;
|
|
||||||
|
|
||||||
if (invertX || invertY) {
|
|
||||||
const x = invertX ? startNodeX - (width - startWidth) : startNodeX;
|
|
||||||
const y = invertY ? startNodeY - (height - startHeight) : startNodeY;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isWidthChange || isHeightChange) {
|
|
||||||
const dimensionChange: NodeDimensionChange = {
|
|
||||||
id: id,
|
|
||||||
type: 'dimensions',
|
|
||||||
updateStyle: true,
|
|
||||||
resizing: true,
|
|
||||||
dimensions: {
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
changes.push(dimensionChange);
|
|
||||||
prevValues.current.width = width;
|
|
||||||
prevValues.current.height = height;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changes.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const direction = getDirection({
|
|
||||||
width: prevValues.current.width,
|
|
||||||
prevWidth,
|
|
||||||
height: prevValues.current.height,
|
|
||||||
prevHeight,
|
|
||||||
invertX,
|
|
||||||
invertY,
|
|
||||||
});
|
|
||||||
|
|
||||||
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]);
|
|
||||||
});
|
|
||||||
|
|
||||||
selection.call(dragHandler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
selection.on('.drag', null);
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
id,
|
|
||||||
controlPosition,
|
|
||||||
minWidth,
|
|
||||||
minHeight,
|
|
||||||
maxWidth,
|
|
||||||
maxHeight,
|
|
||||||
keepAspectRatio,
|
|
||||||
onResizeStart,
|
|
||||||
onResize,
|
|
||||||
onResizeEnd,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const positionClassNames = controlPosition.split('-');
|
|
||||||
const colorStyleProp = variant === ResizeControlVariant.Line ? 'borderColor' : 'backgroundColor';
|
|
||||||
const controlStyle = color ? { ...style, [colorStyleProp]: color } : style;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cc(['react-flow__resize-control', 'nodrag', ...positionClassNames, variant, className])}
|
|
||||||
ref={resizeControlRef}
|
|
||||||
style={controlStyle}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ResizeControlLine(props: ResizeControlLineProps) {
|
|
||||||
return <ResizeControl {...props} variant={ResizeControlVariant.Line} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(ResizeControl);
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export { default as NodeResizer } from './NodeResizer';
|
export { NodeResizer } from './NodeResizer';
|
||||||
export { default as NodeResizeControl } from './ResizeControl';
|
export { NodeResizeControl } from './NodeResizeControl';
|
||||||
|
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|||||||
@@ -1,23 +1,13 @@
|
|||||||
import type { CSSProperties, ReactNode } from 'react';
|
import type { CSSProperties, ReactNode } from 'react';
|
||||||
import type { D3DragEvent, SubjectPosition } from 'd3-drag';
|
import type {
|
||||||
|
ControlPosition,
|
||||||
export type ResizeParams = {
|
ControlLinePosition,
|
||||||
x: number;
|
ResizeControlVariant,
|
||||||
y: number;
|
ShouldResize,
|
||||||
width: number;
|
OnResizeStart,
|
||||||
height: number;
|
OnResize,
|
||||||
};
|
OnResizeEnd,
|
||||||
|
} from '@xyflow/system';
|
||||||
export type ResizeParamsWithDirection = ResizeParams & {
|
|
||||||
direction: number[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type OnResizeHandler<Params = ResizeParams, Result = void> = (event: ResizeDragEvent, params: Params) => Result;
|
|
||||||
|
|
||||||
export type ShouldResize = OnResizeHandler<ResizeParamsWithDirection, boolean>;
|
|
||||||
export type OnResizeStart = OnResizeHandler;
|
|
||||||
export type OnResize = OnResizeHandler<ResizeParamsWithDirection>;
|
|
||||||
export type OnResizeEnd = OnResizeHandler;
|
|
||||||
|
|
||||||
export type NodeResizerProps = {
|
export type NodeResizerProps = {
|
||||||
nodeId?: string;
|
nodeId?: string;
|
||||||
@@ -38,15 +28,6 @@ export type NodeResizerProps = {
|
|||||||
onResizeEnd?: OnResizeEnd;
|
onResizeEnd?: OnResizeEnd;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right';
|
|
||||||
|
|
||||||
export type ControlPosition = ControlLinePosition | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
||||||
|
|
||||||
export enum ResizeControlVariant {
|
|
||||||
Line = 'line',
|
|
||||||
Handle = 'handle',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ResizeControlProps = Pick<
|
export type ResizeControlProps = Pick<
|
||||||
NodeResizerProps,
|
NodeResizerProps,
|
||||||
| 'nodeId'
|
| 'nodeId'
|
||||||
@@ -71,5 +52,3 @@ export type ResizeControlProps = Pick<
|
|||||||
export type ResizeControlLineProps = ResizeControlProps & {
|
export type ResizeControlLineProps = ResizeControlProps & {
|
||||||
position?: ControlLinePosition;
|
position?: ControlLinePosition;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ResizeDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
type GetDirectionParams = {
|
|
||||||
width: number;
|
|
||||||
prevWidth: number;
|
|
||||||
height: number;
|
|
||||||
prevHeight: number;
|
|
||||||
invertX: boolean;
|
|
||||||
invertY: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
// returns an array of two numbers (0, 1 or -1) representing the direction of the resize
|
|
||||||
// 0 = no change, 1 = increase, -1 = decrease
|
|
||||||
export function getDirection({ width, prevWidth, height, prevHeight, invertX, invertY }: GetDirectionParams) {
|
|
||||||
const deltaWidth = width - prevWidth;
|
|
||||||
const deltaHeight = height - prevHeight;
|
|
||||||
|
|
||||||
const direction = [deltaWidth > 0 ? 1 : deltaWidth < 0 ? -1 : 0, deltaHeight > 0 ? 1 : deltaHeight < 0 ? -1 : 0];
|
|
||||||
|
|
||||||
if (deltaWidth && invertX) {
|
|
||||||
direction[0] = direction[0] * -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deltaHeight && invertY) {
|
|
||||||
direction[1] = direction[1] * -1;
|
|
||||||
}
|
|
||||||
return direction;
|
|
||||||
}
|
|
||||||
@@ -219,9 +219,9 @@ export function NodeWrapper({
|
|||||||
transform: `translate(${positionAbsoluteOrigin.x}px,${positionAbsoluteOrigin.y}px)`,
|
transform: `translate(${positionAbsoluteOrigin.x}px,${positionAbsoluteOrigin.y}px)`,
|
||||||
pointerEvents: hasPointerEvents ? 'all' : 'none',
|
pointerEvents: hasPointerEvents ? 'all' : 'none',
|
||||||
visibility: initialized ? 'visible' : 'hidden',
|
visibility: initialized ? 'visible' : 'hidden',
|
||||||
width,
|
|
||||||
height,
|
|
||||||
...node.style,
|
...node.style,
|
||||||
|
width: width ?? node.style?.width,
|
||||||
|
height: height ?? node.style?.height,
|
||||||
}}
|
}}
|
||||||
data-id={id}
|
data-id={id}
|
||||||
data-testid={`rf__node-${id}`}
|
data-testid={`rf__node-${id}`}
|
||||||
|
|||||||
@@ -81,6 +81,13 @@ export {
|
|||||||
type ColorModeClass,
|
type ColorModeClass,
|
||||||
type HandleType,
|
type HandleType,
|
||||||
type OnBeforeDelete,
|
type OnBeforeDelete,
|
||||||
|
type ShouldResize,
|
||||||
|
type OnResizeStart,
|
||||||
|
type OnResize,
|
||||||
|
type OnResizeEnd,
|
||||||
|
type ControlPosition,
|
||||||
|
type ControlLinePosition,
|
||||||
|
type ResizeControlVariant,
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
// system utils
|
// system utils
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export type NodeDimensionChange = {
|
|||||||
id: string;
|
id: string;
|
||||||
type: 'dimensions';
|
type: 'dimensions';
|
||||||
dimensions?: Dimensions;
|
dimensions?: Dimensions;
|
||||||
updateStyle?: boolean;
|
|
||||||
resizing?: boolean;
|
resizing?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -121,10 +121,12 @@ function applyChanges(changes: any[], elements: any[]): any[] {
|
|||||||
}
|
}
|
||||||
updateItem.computed.width = currentChange.dimensions.width;
|
updateItem.computed.width = currentChange.dimensions.width;
|
||||||
updateItem.computed.height = currentChange.dimensions.height;
|
updateItem.computed.height = currentChange.dimensions.height;
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof currentChange.updateStyle !== 'undefined') {
|
// this is needed for the node resizer to work
|
||||||
updateItem.style = { ...(updateItem.style || {}), ...currentChange.dimensions };
|
if (currentChange.resizing) {
|
||||||
|
updateItem.width = currentChange.dimensions.width;
|
||||||
|
updateItem.height = currentChange.dimensions.height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof currentChange.resizing === 'boolean') {
|
if (typeof currentChange.resizing === 'boolean') {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# @xyflow/svelte
|
# @xyflow/svelte
|
||||||
|
|
||||||
|
## 0.0.32
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- add `NodeResizer` and `NodeResizeControl` components for resizing nodes
|
||||||
|
|
||||||
## 0.0.31
|
## 0.0.31
|
||||||
|
|
||||||
### Bugfix
|
### Bugfix
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xyflow/svelte",
|
"name": "@xyflow/svelte",
|
||||||
"version": "0.0.31",
|
"version": "0.0.32",
|
||||||
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
|
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"svelte",
|
"svelte",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export * from '$lib/plugins/Controls';
|
|||||||
export * from '$lib/plugins/Background';
|
export * from '$lib/plugins/Background';
|
||||||
export * from '$lib/plugins/Minimap';
|
export * from '$lib/plugins/Minimap';
|
||||||
export * from '$lib/plugins/NodeToolbar';
|
export * from '$lib/plugins/NodeToolbar';
|
||||||
|
export * from '$lib/plugins/NodeResizer';
|
||||||
|
|
||||||
// store
|
// store
|
||||||
export { useStore } from '$lib/store';
|
export { useStore } from '$lib/store';
|
||||||
@@ -89,7 +90,14 @@ export {
|
|||||||
type Transform,
|
type Transform,
|
||||||
type CoordinateExtent,
|
type CoordinateExtent,
|
||||||
type ColorMode,
|
type ColorMode,
|
||||||
type ColorModeClass
|
type ColorModeClass,
|
||||||
|
type ShouldResize,
|
||||||
|
type OnResizeStart,
|
||||||
|
type OnResize,
|
||||||
|
type OnResizeEnd,
|
||||||
|
type ControlPosition,
|
||||||
|
type ControlLinePosition,
|
||||||
|
type ResizeControlVariant
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
// system utils
|
// system utils
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ResizeControl from './ResizeControl.svelte';
|
||||||
|
import type { NodeResizerProps } from './types';
|
||||||
|
import {
|
||||||
|
ResizeControlVariant,
|
||||||
|
XY_RESIZER_HANDLE_POSITIONS,
|
||||||
|
XY_RESIZER_LINE_POSITIONS
|
||||||
|
} from '@xyflow/system';
|
||||||
|
|
||||||
|
type $$Props = NodeResizerProps;
|
||||||
|
|
||||||
|
export let nodeId: $$Props['nodeId'] = undefined;
|
||||||
|
export let isVisible: $$Props['isVisible'] = true;
|
||||||
|
export let handleClass: $$Props['handleClass'] = undefined;
|
||||||
|
export let handleStyle: $$Props['handleStyle'] = undefined;
|
||||||
|
export let lineClass: $$Props['lineClass'] = undefined;
|
||||||
|
export let lineStyle: $$Props['lineStyle'] = undefined;
|
||||||
|
export let color: $$Props['color'] = undefined;
|
||||||
|
export let minWidth: $$Props['minWidth'] = 10;
|
||||||
|
export let minHeight: $$Props['minHeight'] = 10;
|
||||||
|
export let maxWidth: $$Props['maxWidth'] = Number.MAX_VALUE;
|
||||||
|
export let maxHeight: $$Props['maxHeight'] = Number.MAX_VALUE;
|
||||||
|
export let keepAspectRatio: $$Props['keepAspectRatio'] = false;
|
||||||
|
export let shouldResize: $$Props['shouldResize'] = undefined;
|
||||||
|
export let onResizeStart: $$Props['onResizeStart'] = undefined;
|
||||||
|
export let onResize: $$Props['onResize'] = undefined;
|
||||||
|
export let onResizeEnd: $$Props['onResizeEnd'] = undefined;
|
||||||
|
|
||||||
|
let _minWidth = minWidth || 10;
|
||||||
|
let _minHeight = minHeight || 10;
|
||||||
|
let _maxWidth = maxWidth || Number.MAX_VALUE;
|
||||||
|
let _maxHeight = maxHeight || Number.MAX_VALUE;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isVisible}
|
||||||
|
{#each XY_RESIZER_LINE_POSITIONS as position (position)}
|
||||||
|
<ResizeControl
|
||||||
|
class={lineClass}
|
||||||
|
style={lineStyle}
|
||||||
|
{nodeId}
|
||||||
|
{position}
|
||||||
|
variant={ResizeControlVariant.Line}
|
||||||
|
{color}
|
||||||
|
minWidth={_minWidth}
|
||||||
|
minHeight={_minHeight}
|
||||||
|
maxWidth={_maxWidth}
|
||||||
|
maxHeight={_maxHeight}
|
||||||
|
{onResizeStart}
|
||||||
|
{keepAspectRatio}
|
||||||
|
{shouldResize}
|
||||||
|
{onResize}
|
||||||
|
{onResizeEnd}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{#each XY_RESIZER_HANDLE_POSITIONS as position (position)}
|
||||||
|
<ResizeControl
|
||||||
|
class={handleClass}
|
||||||
|
style={handleStyle}
|
||||||
|
{nodeId}
|
||||||
|
{position}
|
||||||
|
{color}
|
||||||
|
minWidth={_minWidth}
|
||||||
|
minHeight={_minHeight}
|
||||||
|
maxWidth={_maxWidth}
|
||||||
|
maxHeight={_maxHeight}
|
||||||
|
{onResizeStart}
|
||||||
|
{keepAspectRatio}
|
||||||
|
{shouldResize}
|
||||||
|
{onResize}
|
||||||
|
{onResizeEnd}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
import cc from 'classcat';
|
||||||
|
import { useStore } from '$lib/store';
|
||||||
|
import {
|
||||||
|
XYResizer,
|
||||||
|
ResizeControlVariant,
|
||||||
|
type ControlPosition,
|
||||||
|
type XYResizerInstance,
|
||||||
|
type XYResizerChange
|
||||||
|
} from '@xyflow/system';
|
||||||
|
import type { ResizeControlProps } from './types';
|
||||||
|
|
||||||
|
type $$Props = ResizeControlProps;
|
||||||
|
|
||||||
|
export let nodeId: $$Props['nodeId'] = undefined;
|
||||||
|
export let position: $$Props['position'] = undefined;
|
||||||
|
export let variant: $$Props['variant'] = ResizeControlVariant.Handle;
|
||||||
|
export let color: $$Props['color'] = undefined;
|
||||||
|
export let minWidth: $$Props['minWidth'] = 10;
|
||||||
|
$: _minWidth = minWidth ?? 10;
|
||||||
|
export let minHeight: $$Props['minHeight'] = 10;
|
||||||
|
$: _minHeight = minHeight ?? 10;
|
||||||
|
export let maxWidth: $$Props['maxWidth'] = Number.MAX_VALUE;
|
||||||
|
$: _maxWidth = maxWidth ?? Number.MAX_VALUE;
|
||||||
|
export let maxHeight: $$Props['maxHeight'] = Number.MAX_VALUE;
|
||||||
|
$: _maxHeight = maxHeight ?? Number.MAX_VALUE;
|
||||||
|
export let keepAspectRatio: $$Props['keepAspectRatio'] = false;
|
||||||
|
export let shouldResize: $$Props['shouldResize'] = undefined;
|
||||||
|
export let onResizeStart: $$Props['onResizeStart'] = undefined;
|
||||||
|
export let onResize: $$Props['onResize'] = undefined;
|
||||||
|
export let onResizeEnd: $$Props['onResizeEnd'] = undefined;
|
||||||
|
export let style: $$Props['style'] = '';
|
||||||
|
let className: $$Props['class'] = '';
|
||||||
|
export { className as class };
|
||||||
|
|
||||||
|
const { nodeLookup, snapGrid, viewport, nodes } = useStore();
|
||||||
|
|
||||||
|
const contextNodeId = getContext<string>('svelteflow__node_id');
|
||||||
|
$: id = typeof nodeId === 'string' ? nodeId : contextNodeId;
|
||||||
|
|
||||||
|
let resizeControlRef: HTMLDivElement;
|
||||||
|
let resizer: XYResizerInstance | null = null;
|
||||||
|
|
||||||
|
$: defaultPosition = (
|
||||||
|
variant === ResizeControlVariant.Line ? 'right' : 'bottom-right'
|
||||||
|
) as ControlPosition;
|
||||||
|
$: controlPosition = position ?? defaultPosition;
|
||||||
|
|
||||||
|
$: positionClassNames = controlPosition.split('-');
|
||||||
|
|
||||||
|
$: colorStyleProp = variant === ResizeControlVariant.Line ? 'border-color' : 'background-color';
|
||||||
|
$: _style = style ?? '';
|
||||||
|
$: controlStyle = !!color ? `${_style} ${colorStyleProp}: ${color};` : _style;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (resizeControlRef) {
|
||||||
|
resizer = XYResizer({
|
||||||
|
domNode: resizeControlRef,
|
||||||
|
nodeId: id,
|
||||||
|
getStoreItems: () => {
|
||||||
|
return {
|
||||||
|
nodeLookup: $nodeLookup,
|
||||||
|
transform: [$viewport.x, $viewport.y, $viewport.zoom],
|
||||||
|
snapGrid: $snapGrid ?? undefined,
|
||||||
|
snapToGrid: !!$snapGrid
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onChange: (change: XYResizerChange) => {
|
||||||
|
const node = $nodeLookup.get(id);
|
||||||
|
if (node) {
|
||||||
|
node.height = change.isHeightChange ? change.height : node.height;
|
||||||
|
node.width = change.isWidthChange ? change.width : node.width;
|
||||||
|
node.position =
|
||||||
|
change.isXPosChange || change.isYPosChange
|
||||||
|
? { x: change.x, y: change.y }
|
||||||
|
: node.position;
|
||||||
|
|
||||||
|
$nodes = $nodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
resizer?.destroy();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
$: {
|
||||||
|
resizer?.update({
|
||||||
|
controlPosition,
|
||||||
|
boundaries: {
|
||||||
|
minWidth: _minWidth,
|
||||||
|
minHeight: _minHeight,
|
||||||
|
maxWidth: _maxWidth,
|
||||||
|
maxHeight: _maxHeight
|
||||||
|
},
|
||||||
|
keepAspectRatio: !!keepAspectRatio,
|
||||||
|
onResizeStart,
|
||||||
|
onResize,
|
||||||
|
onResizeEnd,
|
||||||
|
shouldResize
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class={cc(['svelte-flow__resize-control', 'nodrag', ...positionClassNames, variant, className])}
|
||||||
|
bind:this={resizeControlRef}
|
||||||
|
style={controlStyle}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as NodeResizer } from './NodeResizer.svelte';
|
||||||
|
export { default as NodeResizeControl } from './ResizeControl.svelte';
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type {
|
||||||
|
ControlPosition,
|
||||||
|
ResizeControlVariant,
|
||||||
|
ShouldResize,
|
||||||
|
OnResizeStart,
|
||||||
|
OnResize,
|
||||||
|
OnResizeEnd
|
||||||
|
} from '@xyflow/system';
|
||||||
|
|
||||||
|
export type NodeResizerProps = {
|
||||||
|
nodeId?: string;
|
||||||
|
color?: string;
|
||||||
|
handleClass?: string;
|
||||||
|
handleStyle?: string;
|
||||||
|
lineClass?: string;
|
||||||
|
lineStyle?: string;
|
||||||
|
isVisible?: boolean;
|
||||||
|
minWidth?: number;
|
||||||
|
minHeight?: number;
|
||||||
|
maxWidth?: number;
|
||||||
|
maxHeight?: number;
|
||||||
|
keepAspectRatio?: boolean;
|
||||||
|
shouldResize?: ShouldResize;
|
||||||
|
onResizeStart?: OnResizeStart;
|
||||||
|
onResize?: OnResize;
|
||||||
|
onResizeEnd?: OnResizeEnd;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResizeControlProps = Pick<
|
||||||
|
NodeResizerProps,
|
||||||
|
| 'nodeId'
|
||||||
|
| 'color'
|
||||||
|
| 'minWidth'
|
||||||
|
| 'minHeight'
|
||||||
|
| 'maxWidth'
|
||||||
|
| 'maxHeight'
|
||||||
|
| 'keepAspectRatio'
|
||||||
|
| 'shouldResize'
|
||||||
|
| 'onResizeStart'
|
||||||
|
| 'onResize'
|
||||||
|
| 'onResizeEnd'
|
||||||
|
> & {
|
||||||
|
position?: ControlPosition;
|
||||||
|
variant?: ResizeControlVariant;
|
||||||
|
class?: string;
|
||||||
|
style?: string;
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
/* this gets exported as style.css and can be used for the default theming */
|
/* this gets exported as style.css and can be used for the default theming */
|
||||||
@import '../../../system/src/styles/init.css';
|
@import '../../../system/src/styles/init.css';
|
||||||
@import '../../../system/src/styles/style.css';
|
@import '../../../system/src/styles/style.css';
|
||||||
|
@import '../../../system/src/styles/node-resizer.css';
|
||||||
|
|
||||||
.svelte-flow {
|
.svelte-flow {
|
||||||
--edge-label-color-default: inherit;
|
--edge-label-color-default: inherit;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xyflow/system",
|
"name": "@xyflow/system",
|
||||||
"version": "0.0.13",
|
"version": "0.0.14",
|
||||||
"description": "xyflow core system that powers React Flow and Svelte Flow.",
|
"description": "xyflow core system that powers React Flow and Svelte Flow.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"node-based UI",
|
"node-based UI",
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ export * from './xydrag';
|
|||||||
export * from './xyhandle';
|
export * from './xyhandle';
|
||||||
export * from './xyminimap';
|
export * from './xyminimap';
|
||||||
export * from './xypanzoom';
|
export * from './xypanzoom';
|
||||||
|
export * from './xyresizer';
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
.xy-flow {
|
||||||
|
--xy-resize-background-color-default: #3367d9;
|
||||||
|
}
|
||||||
|
|
||||||
.xy-flow__resize-control {
|
.xy-flow__resize-control {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
@@ -28,7 +32,7 @@
|
|||||||
height: 4px;
|
height: 4px;
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
background-color: #3367d9;
|
background-color: var(--xy-resize-background-color, var(--xy-resize-background-color-default));
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +67,7 @@
|
|||||||
|
|
||||||
/* line styles */
|
/* line styles */
|
||||||
.xy-flow__resize-control.line {
|
.xy-flow__resize-control.line {
|
||||||
border-color: #3367d9;
|
border-color: var(--xy-resize-background-color, var(--xy-resize-background-color-default));
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { Optional } from '../utils/types';
|
|
||||||
|
|
||||||
export enum Position {
|
export enum Position {
|
||||||
Left = 'left',
|
Left = 'left',
|
||||||
Top = 'top',
|
Top = 'top',
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import { drag } from 'd3-drag';
|
||||||
|
import { select } from 'd3-selection';
|
||||||
|
|
||||||
|
import { getControlDirection, getDimensionsAfterResize, getPositionAfterResize, getResizeDirection } from './utils';
|
||||||
|
import { getPointerPosition } from '../utils';
|
||||||
|
import type { NodeLookup, Transform } from '../types';
|
||||||
|
import type { OnResize, OnResizeEnd, OnResizeStart, ResizeDragEvent, ShouldResize, ControlPosition } from './types';
|
||||||
|
|
||||||
|
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 XYResizerChange = typeof initChange;
|
||||||
|
|
||||||
|
type XYResizerParams = {
|
||||||
|
domNode: HTMLDivElement;
|
||||||
|
nodeId: string;
|
||||||
|
getStoreItems: () => {
|
||||||
|
nodeLookup: NodeLookup;
|
||||||
|
transform: Transform;
|
||||||
|
snapGrid?: [number, number];
|
||||||
|
snapToGrid: boolean;
|
||||||
|
};
|
||||||
|
onChange: (changes: XYResizerChange) => void;
|
||||||
|
onEnd?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type XYResizerUpdateParams = {
|
||||||
|
controlPosition: ControlPosition;
|
||||||
|
boundaries: {
|
||||||
|
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,
|
||||||
|
boundaries,
|
||||||
|
keepAspectRatio,
|
||||||
|
onResizeStart,
|
||||||
|
onResize,
|
||||||
|
onResizeEnd,
|
||||||
|
shouldResize,
|
||||||
|
}: XYResizerUpdateParams) {
|
||||||
|
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,
|
||||||
|
boundaries,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './types';
|
||||||
|
export * from './XYResizer';
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { D3DragEvent, SubjectPosition } from 'd3-drag';
|
||||||
|
|
||||||
|
export type XYResizerParams = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type XYResizerParamsWithDirection = XYResizerParams & {
|
||||||
|
direction: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right';
|
||||||
|
|
||||||
|
export type ControlPosition = ControlLinePosition | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
||||||
|
|
||||||
|
export enum ResizeControlVariant {
|
||||||
|
Line = 'line',
|
||||||
|
Handle = 'handle',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const XY_RESIZER_HANDLE_POSITIONS: ControlPosition[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
||||||
|
export const XY_RESIZER_LINE_POSITIONS: ControlLinePosition[] = ['top', 'right', 'bottom', 'left'];
|
||||||
|
|
||||||
|
type OnResizeHandler<Params = XYResizerParams, Result = void> = (event: ResizeDragEvent, params: Params) => Result;
|
||||||
|
export type ResizeDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
|
||||||
|
|
||||||
|
export type ShouldResize = OnResizeHandler<XYResizerParamsWithDirection, boolean>;
|
||||||
|
export type OnResizeStart = OnResizeHandler;
|
||||||
|
export type OnResize = OnResizeHandler<XYResizerParamsWithDirection>;
|
||||||
|
export type OnResizeEnd = OnResizeHandler;
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { clamp, getPointerPosition } from '../utils';
|
||||||
|
import { ControlPosition } from './types';
|
||||||
|
|
||||||
|
type GetResizeDirectionParams = {
|
||||||
|
width: number;
|
||||||
|
prevWidth: number;
|
||||||
|
height: number;
|
||||||
|
prevHeight: number;
|
||||||
|
affectsX: boolean;
|
||||||
|
affectsY: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all connecting edges for a given set of nodes
|
||||||
|
* @param width - new width of the node
|
||||||
|
* @param prevWidth - previous width of the node
|
||||||
|
* @param height - new height of the node
|
||||||
|
* @param prevHeight - previous height of the node
|
||||||
|
* @param affectsX - whether to invert the resize direction for the x axis
|
||||||
|
* @param affectsY - whether to invert the resize direction for the y axis
|
||||||
|
* @returns array of two numbers representing the direction of the resize for each axis, 0 = no change, 1 = increase, -1 = decrease
|
||||||
|
*/
|
||||||
|
export function getResizeDirection({
|
||||||
|
width,
|
||||||
|
prevWidth,
|
||||||
|
height,
|
||||||
|
prevHeight,
|
||||||
|
affectsX,
|
||||||
|
affectsY,
|
||||||
|
}: GetResizeDirectionParams) {
|
||||||
|
const deltaWidth = width - prevWidth;
|
||||||
|
const deltaHeight = height - prevHeight;
|
||||||
|
|
||||||
|
const direction = [deltaWidth > 0 ? 1 : deltaWidth < 0 ? -1 : 0, deltaHeight > 0 ? 1 : deltaHeight < 0 ? -1 : 0];
|
||||||
|
|
||||||
|
if (deltaWidth && affectsX) {
|
||||||
|
direction[0] = direction[0] * -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deltaHeight && affectsY) {
|
||||||
|
direction[1] = direction[1] * -1;
|
||||||
|
}
|
||||||
|
return direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the control position that is being dragged to dimensions that are being resized
|
||||||
|
* @param controlPosition - position of the control that is being dragged
|
||||||
|
* @returns isHorizontal, isVertical, affectsX, affectsY,
|
||||||
|
*/
|
||||||
|
export function getControlDirection(controlPosition: ControlPosition) {
|
||||||
|
const isHorizontal = controlPosition.includes('right') || controlPosition.includes('left');
|
||||||
|
const isVertical = controlPosition.includes('bottom') || controlPosition.includes('top');
|
||||||
|
const affectsX = controlPosition.includes('left');
|
||||||
|
const affectsY = controlPosition.includes('top');
|
||||||
|
|
||||||
|
return {
|
||||||
|
isHorizontal,
|
||||||
|
isVertical,
|
||||||
|
affectsX,
|
||||||
|
affectsY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrevValues = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StartValues = PrevValues & {
|
||||||
|
pointerX: number;
|
||||||
|
pointerY: number;
|
||||||
|
aspectRatio: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates new width & height of node after resize based on pointer position
|
||||||
|
* @param startValues - starting values of resize
|
||||||
|
* @param controlDirection - dimensions affected by the resize
|
||||||
|
* @param pointerPosition - the current pointer position corrected for snapping
|
||||||
|
* @param boundaries - minimum and maximum dimensions of the node
|
||||||
|
* @param keepAspectRatio - prevent changes of asprect ratio
|
||||||
|
* @returns width: new width of node, height: new height of node
|
||||||
|
*/
|
||||||
|
export function getDimensionsAfterResize(
|
||||||
|
startValues: StartValues,
|
||||||
|
controlDirection: ReturnType<typeof getControlDirection>,
|
||||||
|
pointerPosition: ReturnType<typeof getPointerPosition>,
|
||||||
|
boundaries: { minWidth: number; maxWidth: number; minHeight: number; maxHeight: number },
|
||||||
|
keepAspectRatio: boolean
|
||||||
|
) {
|
||||||
|
const { isHorizontal, isVertical, affectsX, affectsY } = controlDirection;
|
||||||
|
const { xSnapped, ySnapped } = pointerPosition;
|
||||||
|
const { minWidth, maxWidth, minHeight, maxHeight } = boundaries;
|
||||||
|
|
||||||
|
const { pointerX: startX, pointerY: startY, width: startWidth, height: startHeight, aspectRatio } = startValues;
|
||||||
|
const distX = Math.floor(isHorizontal ? xSnapped - startX : 0);
|
||||||
|
const distY = Math.floor(isVertical ? ySnapped - startY : 0);
|
||||||
|
|
||||||
|
let width = clamp(startWidth + (affectsX ? -distX : distX), minWidth, maxWidth);
|
||||||
|
let height = clamp(startHeight + (affectsY ? -distY : distY), minHeight, maxHeight);
|
||||||
|
|
||||||
|
if (keepAspectRatio) {
|
||||||
|
const nextAspectRatio = width / height;
|
||||||
|
const isDiagonal = isHorizontal && isVertical;
|
||||||
|
const isOnlyHorizontal = isHorizontal && !isVertical;
|
||||||
|
const isOnlyVertical = isVertical && !isHorizontal;
|
||||||
|
|
||||||
|
width = (nextAspectRatio <= aspectRatio && isDiagonal) || isOnlyVertical ? height * aspectRatio : width;
|
||||||
|
height = (nextAspectRatio > aspectRatio && isDiagonal) || isOnlyHorizontal ? width / aspectRatio : height;
|
||||||
|
|
||||||
|
if (width >= maxWidth) {
|
||||||
|
width = maxWidth;
|
||||||
|
height = maxWidth / aspectRatio;
|
||||||
|
} else if (width <= minWidth) {
|
||||||
|
width = minWidth;
|
||||||
|
height = minWidth / aspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (height >= maxHeight) {
|
||||||
|
height = maxHeight;
|
||||||
|
width = maxHeight * aspectRatio;
|
||||||
|
} else if (height <= minHeight) {
|
||||||
|
height = minHeight;
|
||||||
|
width = minHeight * aspectRatio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines new x & y position of node after resize based on new width & height
|
||||||
|
* @param startValues - starting values of resize
|
||||||
|
* @param controlDirection - dimensions affected by the resize
|
||||||
|
* @param width - new width of node
|
||||||
|
* @param height - new height of node
|
||||||
|
* @returns x: new x position of node, y: new y position of node
|
||||||
|
*/
|
||||||
|
export function getPositionAfterResize(
|
||||||
|
startValues: StartValues,
|
||||||
|
controlDirection: ReturnType<typeof getControlDirection>,
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
x: controlDirection.affectsX ? startValues.x - (width - startValues.width) : startValues.x,
|
||||||
|
y: controlDirection.affectsY ? startValues.y - (height - startValues.height) : startValues.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
Generated
+5564
-8940
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user