Files
xyflow/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte
T

282 lines
8.2 KiB
Svelte

<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
import { setContext, onDestroy } from 'svelte';
import {
elementSelectionKeys,
errorMessages,
isInputDOMNode,
nodeHasDimensions,
Position
} from '@xyflow/system';
import drag from '$lib/actions/drag';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
import type { ConnectableContext, NodeWrapperProps } from './types';
import type { Node, Edge, NodeEvents } from '$lib/types';
import { arrowKeyDiffs, toPxString } from '$lib/utils';
import { ARIA_NODE_DESC_KEY } from '../A11yDescriptions';
let {
store = $bindable(),
node,
resizeObserver,
nodeClickDistance,
onnodeclick,
onnodedrag,
onnodedragstart,
onnodedragstop,
onnodepointerenter,
onnodepointerleave,
onnodepointermove,
onnodecontextmenu
}: NodeWrapperProps<NodeType, EdgeType> & NodeEvents<NodeType> = $props();
let {
data = {},
selected = false,
draggable: _draggable,
selectable: _selectable,
deletable = true,
connectable: _connectable,
focusable: _focusable,
hidden = false,
dragging = false,
style = '',
class: className,
type = 'default',
parentId,
sourcePosition,
targetPosition,
measured: { width: measuredWidth, height: measuredHeight } = { width: 0, height: 0 },
initialWidth,
initialHeight,
width,
height,
dragHandle,
internals: {
z: zIndex = 0,
positionAbsolute: { x: positionX, y: positionY },
userNode
}
} = $derived(node);
let { id } = node;
let draggable = $derived(_draggable ?? store.nodesDraggable);
let selectable = $derived(_selectable ?? store.elementsSelectable);
let connectable = $derived(_connectable ?? store.nodesConnectable);
let initialized = $derived(nodeHasDimensions(node) && !!node.internals.handleBounds);
let focusable = $derived(_focusable ?? store.nodesFocusable);
function isInParentLookup(id: string) {
return store.parentLookup.has(id);
}
let isParent = $derived(isInParentLookup(id));
let nodeRef: HTMLDivElement | null = $state(null);
let prevNodeRef: HTMLDivElement | null = null;
// svelte-ignore state_referenced_locally
let prevType: string | undefined = type;
// svelte-ignore state_referenced_locally
let prevSourcePosition: Position | undefined = sourcePosition;
// svelte-ignore state_referenced_locally
let prevTargetPosition: Position | undefined = targetPosition;
let NodeComponent = $derived(store.nodeTypes[type] ?? DefaultNode);
let connectableContext: ConnectableContext = {
get value() {
return connectable;
}
};
setContext('svelteflow__node_connectable', connectableContext);
setContext('svelteflow__node_id', id);
if (process.env.NODE_ENV === 'development') {
$effect(() => {
const valid = !!store.nodeTypes[type];
if (!valid) {
console.warn('003', errorMessages['error003'](type!));
}
});
}
let nodeStyle = $derived.by(() => {
const w = measuredWidth === undefined ? (width ?? initialWidth) : width;
const h = measuredHeight === undefined ? (height ?? initialHeight) : height;
if (w === undefined && h === undefined && style === undefined) {
return undefined;
}
return `${style};${w ? `width:${toPxString(w)};` : ''}${h ? `height:${toPxString(h)};` : ''}`;
});
$effect(() => {
// if type, sourcePosition or targetPosition changes,
// we need to re-calculate the handle positions
const doUpdate =
type !== prevType ||
sourcePosition !== prevSourcePosition ||
targetPosition !== prevTargetPosition;
if (doUpdate && nodeRef !== null) {
requestAnimationFrame(() => {
if (nodeRef !== null) {
store.updateNodeInternals(
new Map([
[
id,
{
id,
nodeElement: nodeRef,
force: true
}
]
])
);
}
});
}
prevType = type;
prevSourcePosition = sourcePosition;
prevTargetPosition = targetPosition;
});
$effect(() => {
/* eslint-disable @typescript-eslint/no-unused-expressions */
if (resizeObserver && (!initialized || nodeRef !== prevNodeRef)) {
prevNodeRef && resizeObserver.unobserve(prevNodeRef);
nodeRef && resizeObserver.observe(nodeRef);
prevNodeRef = nodeRef;
}
/* eslint-enable @typescript-eslint/no-unused-expressions */
});
onDestroy(() => {
if (prevNodeRef) {
resizeObserver?.unobserve(prevNodeRef);
}
});
function onSelectNodeHandler(event: MouseEvent | TouchEvent) {
if (selectable && (!store.selectNodesOnDrag || !draggable || store.nodeDragThreshold > 0)) {
// this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
// here we only need to call it when selectNodesOnDrag=false
store.handleNodeSelection(id);
}
onnodeclick?.({ node: userNode, event });
}
function onKeyDown(event: KeyboardEvent) {
if (isInputDOMNode(event) || store.disableKeyboardA11y) {
return;
}
if (elementSelectionKeys.includes(event.key) && selectable) {
const unselect = event.key === 'Escape';
store.handleNodeSelection(id, unselect, nodeRef);
} else if (
draggable &&
node.selected &&
Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)
) {
// prevent default scrolling behavior on arrow key press when node is moved
event.preventDefault();
store.ariaLiveMessage = `Moved selected node ${event.key
.replace('Arrow', '')
.toLowerCase()}. New position, x: ${node.internals.positionAbsolute.x}, y: ${node.internals.positionAbsolute.y}`;
store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1);
}
}
</script>
{#if !hidden}
<div
use:drag={{
nodeId: id,
isSelectable: selectable,
disabled: !draggable,
handleSelector: dragHandle,
noDragClass: store.noDragClass,
nodeClickDistance,
onNodeMouseDown: store.handleNodeSelection,
onDrag: (event, _, targetNode, nodes) => {
onnodedrag?.({ event, targetNode: targetNode as NodeType, nodes: nodes as NodeType[] });
},
onDragStart: (event, _, targetNode, nodes) => {
onnodedragstart?.({
event,
targetNode: targetNode as NodeType,
nodes: nodes as NodeType[]
});
},
onDragStop: (event, _, targetNode, nodes) => {
onnodedragstop?.({ event, targetNode: targetNode as NodeType, nodes: nodes as NodeType[] });
},
store
}}
bind:this={nodeRef}
data-id={id}
class={['svelte-flow__node', `svelte-flow__node-${type}`, className]}
class:dragging
class:selected
class:draggable
class:connectable
class:selectable
class:nopan={draggable}
class:parent={isParent}
style:z-index={zIndex}
style:transform="translate({positionX}px, {positionY}px)"
style:visibility={initialized ? 'visible' : 'hidden'}
style={nodeStyle}
onclick={onSelectNodeHandler}
onpointerenter={onnodepointerenter
? (event) => onnodepointerenter({ node: userNode, event })
: undefined}
onpointerleave={onnodepointerleave
? (event) => onnodepointerleave({ node: userNode, event })
: undefined}
onpointermove={onnodepointermove
? (event) => onnodepointermove({ node: userNode, event })
: undefined}
oncontextmenu={onnodecontextmenu
? (event) => onnodecontextmenu({ node: userNode, event })
: undefined}
onkeydown={focusable ? onKeyDown : undefined}
tabIndex={focusable ? 0 : undefined}
role={focusable ? 'button' : undefined}
aria-describedby={store.disableKeyboardA11y
? undefined
: `${ARIA_NODE_DESC_KEY}-${store.flowId}`}
>
<NodeComponent
{data}
{id}
{selected}
{selectable}
{deletable}
{sourcePosition}
{targetPosition}
{zIndex}
{dragging}
{draggable}
{dragHandle}
{parentId}
{type}
isConnectable={connectable}
positionAbsoluteX={positionX}
positionAbsoluteY={positionY}
{width}
{height}
/>
</div>
{/if}