Naive copy&paste implementation of node resizer in svelte, some stuff pulled out to system
This commit is contained in:
@@ -9,7 +9,7 @@ const DefaultResizerNode: FC<NodeProps> = ({ data, selected }) => {
|
||||
maxWidth={data.maxWidth ?? undefined}
|
||||
minHeight={data.minHeight ?? undefined}
|
||||
maxHeight={data.maxHeight ?? undefined}
|
||||
isVisible={data.isVisible ?? selected}
|
||||
isVisible={data.isVisible ?? !!selected}
|
||||
shouldResize={data.shouldResize ?? undefined}
|
||||
onResizeStart={data.onResizeStart ?? undefined}
|
||||
onResize={data.onResize ?? undefined}
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
'usenodesdata',
|
||||
'usesvelteflow',
|
||||
'useupdatenodeinternals',
|
||||
'validation'
|
||||
'validation',
|
||||
'node-resizer'
|
||||
];
|
||||
|
||||
const onChange = (event: Event) => {
|
||||
|
||||
128
examples/svelte/src/routes/examples/node-resizer/+page.svelte
Normal file
128
examples/svelte/src/routes/examples/node-resizer/+page.svelte
Normal file
@@ -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} />
|
||||
@@ -1,8 +1,6 @@
|
||||
import ResizeControl from './ResizeControl';
|
||||
import { ControlPosition, NodeResizerProps, ResizeControlVariant, ControlLinePosition } from './types';
|
||||
|
||||
const handleControls: ControlPosition[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
||||
const lineControls: ControlLinePosition[] = ['top', 'right', 'bottom', 'left'];
|
||||
import { NodeResizerProps } from './types';
|
||||
import { XY_RESIZER_HANDLE_CONTROLS, XY_RESIZER_LINE_CONTROLS, ResizeControlVariant } from '@xyflow/system';
|
||||
|
||||
export default function NodeResizer({
|
||||
nodeId,
|
||||
@@ -28,7 +26,7 @@ export default function NodeResizer({
|
||||
|
||||
return (
|
||||
<>
|
||||
{lineControls.map((c) => (
|
||||
{XY_RESIZER_LINE_CONTROLS.map((c) => (
|
||||
<ResizeControl
|
||||
key={c}
|
||||
className={lineClassName}
|
||||
@@ -48,7 +46,7 @@ export default function NodeResizer({
|
||||
onResizeEnd={onResizeEnd}
|
||||
/>
|
||||
))}
|
||||
{handleControls.map((c) => (
|
||||
{XY_RESIZER_HANDLE_CONTROLS.map((c) => (
|
||||
<ResizeControl
|
||||
key={c}
|
||||
className={handleClassName}
|
||||
|
||||
@@ -7,13 +7,8 @@ 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';
|
||||
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 };
|
||||
|
||||
@@ -184,7 +179,7 @@ function ResizeControl({
|
||||
return;
|
||||
}
|
||||
|
||||
const direction = getDirection({
|
||||
const direction = getResizeDirection({
|
||||
width: prevValues.current.width,
|
||||
prevWidth,
|
||||
height: prevValues.current.height,
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import type { D3DragEvent, SubjectPosition } from 'd3-drag';
|
||||
import type {
|
||||
XYResizeParams,
|
||||
XYResizeParamsWithDirection,
|
||||
XYResizeControlPosition,
|
||||
XYResizeControlLinePosition,
|
||||
ResizeControlVariant,
|
||||
} from '@xyflow/system';
|
||||
|
||||
export type ResizeParams = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
type OnResizeHandler<Params = XYResizeParams, Result = void> = (event: ResizeDragEvent, params: Params) => Result;
|
||||
export type ResizeDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
|
||||
|
||||
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 ShouldResize = OnResizeHandler<XYResizeParamsWithDirection, boolean>;
|
||||
export type OnResizeStart = OnResizeHandler;
|
||||
export type OnResize = OnResizeHandler<ResizeParamsWithDirection>;
|
||||
export type OnResize = OnResizeHandler<XYResizeParamsWithDirection>;
|
||||
export type OnResizeEnd = OnResizeHandler;
|
||||
|
||||
export type NodeResizerProps = {
|
||||
@@ -38,15 +35,6 @@ export type NodeResizerProps = {
|
||||
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<
|
||||
NodeResizerProps,
|
||||
| 'nodeId'
|
||||
@@ -61,7 +49,7 @@ export type ResizeControlProps = Pick<
|
||||
| 'onResize'
|
||||
| 'onResizeEnd'
|
||||
> & {
|
||||
position?: ControlPosition;
|
||||
position?: XYResizeControlPosition;
|
||||
variant?: ResizeControlVariant;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
@@ -69,7 +57,5 @@ export type ResizeControlProps = Pick<
|
||||
};
|
||||
|
||||
export type ResizeControlLineProps = ResizeControlProps & {
|
||||
position?: ControlLinePosition;
|
||||
position?: XYResizeControlLinePosition;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -42,8 +42,12 @@
|
||||
},
|
||||
"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"
|
||||
"classcat": "^5.0.4",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-selection": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.1.0",
|
||||
|
||||
@@ -16,6 +16,7 @@ export * from '$lib/plugins/Controls';
|
||||
export * from '$lib/plugins/Background';
|
||||
export * from '$lib/plugins/Minimap';
|
||||
export * from '$lib/plugins/NodeToolbar';
|
||||
export * from '$lib/plugins/NodeResizer';
|
||||
|
||||
// store
|
||||
export { useStore } from '$lib/store';
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<script lang="ts">
|
||||
import ResizeControl from './ResizeControl.svelte';
|
||||
import type { NodeResizerProps } from './types';
|
||||
import {
|
||||
XY_RESIZER_HANDLE_CONTROLS,
|
||||
XY_RESIZER_LINE_CONTROLS,
|
||||
ResizeControlVariant
|
||||
} 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;
|
||||
|
||||
$: console.log(isVisible);
|
||||
</script>
|
||||
|
||||
{#if isVisible}
|
||||
{#each XY_RESIZER_LINE_CONTROLS as c (c)}
|
||||
<ResizeControl
|
||||
class={lineClass}
|
||||
style={lineStyle}
|
||||
{nodeId}
|
||||
position={c}
|
||||
variant={ResizeControlVariant.Line}
|
||||
{color}
|
||||
minWidth={_minWidth}
|
||||
minHeight={_minHeight}
|
||||
maxWidth={_maxWidth}
|
||||
maxHeight={_maxHeight}
|
||||
{onResizeStart}
|
||||
{keepAspectRatio}
|
||||
{shouldResize}
|
||||
{onResize}
|
||||
{onResizeEnd}
|
||||
/>
|
||||
{/each}
|
||||
{#each XY_RESIZER_HANDLE_CONTROLS as c (c)}
|
||||
<ResizeControl
|
||||
class={handleClass}
|
||||
style={handleStyle}
|
||||
{nodeId}
|
||||
position={c}
|
||||
{color}
|
||||
minWidth={_minWidth}
|
||||
minHeight={_minHeight}
|
||||
maxWidth={_maxWidth}
|
||||
maxHeight={_maxHeight}
|
||||
{onResizeStart}
|
||||
{keepAspectRatio}
|
||||
{shouldResize}
|
||||
{onResize}
|
||||
{onResizeEnd}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
234
packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte
Normal file
234
packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte
Normal file
@@ -0,0 +1,234 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import { drag } from 'd3-drag';
|
||||
import { select } from 'd3-selection';
|
||||
import cc from 'classcat';
|
||||
import type { ResizeControlProps, ResizeDragEvent } from './types';
|
||||
import {
|
||||
ResizeControlVariant,
|
||||
getPointerPosition,
|
||||
clamp,
|
||||
getResizeDirection
|
||||
} from '@xyflow/system';
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
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 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 };
|
||||
|
||||
$: defaultPosition = variant === ResizeControlVariant.Line ? 'right' : 'bottom-right';
|
||||
$: 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 || !id) {
|
||||
return;
|
||||
}
|
||||
const selection = select(resizeControlRef);
|
||||
|
||||
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 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 { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, {
|
||||
transform: [$viewport.x, $viewport.y, $viewport.zoom],
|
||||
snapGrid: $snapGrid ?? undefined,
|
||||
snapToGrid: !!$snapGrid
|
||||
});
|
||||
const node = $nodeLookup.get(id);
|
||||
|
||||
if (node) {
|
||||
const {
|
||||
pointerX: startX,
|
||||
pointerY: startY,
|
||||
width: startWidth,
|
||||
height: startHeight,
|
||||
x: startNodeX,
|
||||
y: startNodeY,
|
||||
aspectRatio
|
||||
} = startValues;
|
||||
|
||||
const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues;
|
||||
|
||||
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;
|
||||
|
||||
let changes = false;
|
||||
|
||||
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) {
|
||||
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,
|
||||
invertX,
|
||||
invertY
|
||||
});
|
||||
|
||||
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);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cc(['svelte-flow__resize-control', 'nodrag', ...positionClassNames, variant, className])}
|
||||
bind:this={resizeControlRef}
|
||||
style={controlStyle}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
2
packages/svelte/src/lib/plugins/NodeResizer/index.ts
Normal file
2
packages/svelte/src/lib/plugins/NodeResizer/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as NodeResizer } from './NodeResizer.svelte';
|
||||
export { default as NodeResizeControl } from './ResizeControl.svelte';
|
||||
57
packages/svelte/src/lib/plugins/NodeResizer/types.ts
Normal file
57
packages/svelte/src/lib/plugins/NodeResizer/types.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type {
|
||||
XYResizeParams,
|
||||
XYResizeParamsWithDirection,
|
||||
XYResizeControlPosition,
|
||||
ResizeControlVariant,
|
||||
D3DragEvent,
|
||||
SubjectPosition
|
||||
} 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?: XYResizeControlPosition;
|
||||
variant?: ResizeControlVariant;
|
||||
class?: string;
|
||||
style?: string;
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -1,6 +1,7 @@
|
||||
/* this gets exported as style.css and can be used for the default theming */
|
||||
@import '../../../system/src/styles/init.css';
|
||||
@import '../../../system/src/styles/style.css';
|
||||
@import '../../../system/src/styles/node-resizer.css';
|
||||
|
||||
.svelte-flow {
|
||||
--edge-label-color-default: inherit;
|
||||
|
||||
@@ -5,3 +5,4 @@ export * from './xydrag';
|
||||
export * from './xyhandle';
|
||||
export * from './xyminimap';
|
||||
export * from './xypanzoom';
|
||||
export * from './xyresizer';
|
||||
|
||||
76
packages/system/src/xyresizer/index.ts
Normal file
76
packages/system/src/xyresizer/index.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
export type { D3DragEvent, SubjectPosition } from 'd3-drag';
|
||||
|
||||
export type XYResizeParams = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type XYResizeParamsWithDirection = XYResizeParams & {
|
||||
direction: number[];
|
||||
};
|
||||
|
||||
export type XYResizeControlLinePosition = 'top' | 'bottom' | 'left' | 'right';
|
||||
|
||||
export type XYResizeControlPosition =
|
||||
| XYResizeControlLinePosition
|
||||
| 'top-left'
|
||||
| 'top-right'
|
||||
| 'bottom-left'
|
||||
| 'bottom-right';
|
||||
|
||||
export enum ResizeControlVariant {
|
||||
Line = 'line',
|
||||
Handle = 'handle',
|
||||
}
|
||||
|
||||
export const XY_RESIZER_HANDLE_CONTROLS: XYResizeControlPosition[] = [
|
||||
'top-left',
|
||||
'top-right',
|
||||
'bottom-left',
|
||||
'bottom-right',
|
||||
];
|
||||
export const XY_RESIZER_LINE_CONTROLS: XYResizeControlLinePosition[] = ['top', 'right', 'bottom', 'left'];
|
||||
|
||||
type GetResizeDirectionParams = {
|
||||
width: number;
|
||||
prevWidth: number;
|
||||
height: number;
|
||||
prevHeight: number;
|
||||
invertX: boolean;
|
||||
invertY: 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 invertX - whether to invert the resize direction for the x axis
|
||||
* @param invertY - 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,
|
||||
invertX,
|
||||
invertY,
|
||||
}: 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 && invertX) {
|
||||
direction[0] = direction[0] * -1;
|
||||
}
|
||||
|
||||
if (deltaHeight && invertY) {
|
||||
direction[1] = direction[1] * -1;
|
||||
}
|
||||
return direction;
|
||||
}
|
||||
14425
pnpm-lock.yaml
generated
14425
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user