Skip to content

Commit d8434e6

Browse files
committed
fix(core,nodes): use render fn for node wrapper
Signed-off-by: braks <[email protected]>
1 parent 41a2ccc commit d8434e6

File tree

2 files changed

+333
-1
lines changed

2 files changed

+333
-1
lines changed
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
import { isNumber } from '@vueuse/core'
2+
import type { GraphNode, HandleConnectable, NodeComponent } from '~/types'
3+
4+
interface Props {
5+
id: string
6+
draggable: boolean
7+
selectable: boolean
8+
connectable: HandleConnectable
9+
focusable: boolean
10+
type: NodeComponent | Function | Object | false
11+
name: string
12+
node: GraphNode
13+
resizeObserver: ResizeObserver
14+
}
15+
16+
const NodeWrapper = defineComponent({
17+
name: 'Node',
18+
compatConfig: { MODE: 3 },
19+
props: ['name', 'type', 'id', 'draggable', 'selectable', 'focusable', 'connectable', 'node', 'resizeObserver'],
20+
setup(props: Props) {
21+
provide(NodeId, props.id)
22+
23+
const {
24+
id: vueFlowId,
25+
edges,
26+
noPanClassName,
27+
selectNodesOnDrag,
28+
nodesSelectionActive,
29+
multiSelectionActive,
30+
emits,
31+
findNode,
32+
removeSelectedNodes,
33+
addSelectedNodes,
34+
updateNodeDimensions,
35+
onUpdateNodeInternals,
36+
getIntersectingNodes,
37+
getNodeTypes,
38+
nodeExtent,
39+
elevateNodesOnSelect,
40+
disableKeyboardA11y,
41+
ariaLiveMessage,
42+
snapToGrid,
43+
snapGrid,
44+
} = $(useVueFlow())
45+
46+
const updateNodePositions = useUpdateNodePositions()
47+
48+
const node = $(useVModel(props, 'node'))
49+
50+
const parentNode = $computed(() => (node.parentNode ? findNode(node.parentNode) : undefined))
51+
52+
const connectedEdges = $computed(() => getConnectedEdges([node], edges))
53+
54+
const nodeElement = ref<HTMLDivElement>()
55+
56+
provide(NodeRef, nodeElement)
57+
58+
const { emit, on } = useNodeHooks(node, emits)
59+
60+
const dragging = useDrag({
61+
id: props.id,
62+
el: nodeElement,
63+
disabled: computed(() => !props.draggable),
64+
selectable: computed(() => props.selectable),
65+
onStart(event, node, nodes) {
66+
emit.dragStart({ event, node, nodes, intersections: getIntersectingNodes(node) })
67+
},
68+
onDrag(event, node, nodes) {
69+
emit.drag({ event, node, nodes, intersections: getIntersectingNodes(node) })
70+
},
71+
onStop(event, node, nodes) {
72+
emit.dragStop({ event, node, nodes, intersections: getIntersectingNodes(node) })
73+
},
74+
})
75+
76+
const getClass = computed(() => (node.class instanceof Function ? node.class(node) : node.class))
77+
78+
const getStyle = computed(() => {
79+
const styles = (node.style instanceof Function ? node.style(node) : node.style) || {}
80+
81+
const width = node.width instanceof Function ? node.width(node) : node.width
82+
const height = node.height instanceof Function ? node.height(node) : node.height
83+
84+
if (width) styles.width = typeof width === 'string' ? width : `${width}px`
85+
86+
if (height) styles.height = typeof height === 'string' ? height : `${height}px`
87+
88+
return styles
89+
})
90+
91+
const zIndex = computed(() => Number(node.zIndex ?? getStyle.value.zIndex ?? 0))
92+
93+
onUpdateNodeInternals((updateIds) => {
94+
if (updateIds.includes(props.id)) {
95+
updateInternals()
96+
}
97+
})
98+
99+
onMounted(() => {
100+
props.resizeObserver.observe(nodeElement.value as HTMLDivElement)
101+
})
102+
103+
onBeforeUnmount(() => {
104+
props.resizeObserver.unobserve(nodeElement.value as HTMLDivElement)
105+
})
106+
107+
watch(
108+
[() => node.type, () => node.sourcePosition, () => node.targetPosition],
109+
() => {
110+
updateNodeDimensions([{ id: props.id, nodeElement: nodeElement.value as HTMLDivElement, forceUpdate: true }])
111+
},
112+
{ flush: 'pre' },
113+
)
114+
115+
/** this watcher only updates XYZPosition (when dragging a parent etc) */
116+
watch(
117+
[
118+
() => node.position.x,
119+
() => node.position.y,
120+
() => parentNode?.computedPosition.x,
121+
() => parentNode?.computedPosition.y,
122+
() => parentNode?.computedPosition.z,
123+
() => node.selected,
124+
() => node.dimensions.height,
125+
() => node.dimensions.width,
126+
() => parentNode?.dimensions.height,
127+
() => parentNode?.dimensions.width,
128+
zIndex,
129+
],
130+
([newX, newY, parentX, parentY, parentZ]) => {
131+
const xyzPos = {
132+
x: newX,
133+
y: newY,
134+
z: zIndex.value + (elevateNodesOnSelect ? (node.selected ? 1000 : 0) : 0),
135+
}
136+
137+
if (isNumber(parentX) && isNumber(parentY)) {
138+
node.computedPosition = getXYZPos({ x: parentX, y: parentY, z: parentZ! }, xyzPos)
139+
} else {
140+
node.computedPosition = xyzPos
141+
}
142+
},
143+
{ flush: 'pre', immediate: true },
144+
)
145+
146+
watch([() => node.extent, () => nodeExtent], ([nodeExtent, globalExtent], [oldNodeExtent, oldGlobalExtent]) => {
147+
// update position if extent has actually changed
148+
if (nodeExtent !== oldNodeExtent || globalExtent !== oldGlobalExtent) {
149+
clampPosition()
150+
}
151+
})
152+
153+
// clamp initial position to nodes' extent
154+
// if extent is parent, we need dimensions to properly clamp the position
155+
if (
156+
node.extent === 'parent' ||
157+
(typeof node.extent === 'object' && 'range' in node.extent && node.extent.range === 'parent')
158+
) {
159+
until(() => node.initialized)
160+
.toBe(true)
161+
.then(clampPosition)
162+
}
163+
// if extent is not parent, we can clamp it immediately
164+
else {
165+
clampPosition()
166+
}
167+
168+
return () =>
169+
h(
170+
'div',
171+
{
172+
'ref': nodeElement,
173+
'data-id': node.id,
174+
'class': [
175+
'vue-flow__node',
176+
`vue-flow__node-${props.type === false ? 'default' : props.name}`,
177+
{
178+
[noPanClassName]: props.draggable,
179+
dragging,
180+
selected: node.selected,
181+
selectable: props.selectable,
182+
},
183+
getClass,
184+
],
185+
'style': {
186+
zIndex: node.computedPosition.z ?? zIndex,
187+
transform: `translate(${node.computedPosition.x}px,${node.computedPosition.y}px)`,
188+
pointerEvents: props.selectable || props.draggable ? 'all' : 'none',
189+
visibility: node.initialized ? 'visible' : 'hidden',
190+
...getStyle,
191+
},
192+
'tabIndex': props.focusable ? 0 : undefined,
193+
'role': props.focusable ? 'button' : undefined,
194+
'aria-describedby': disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${vueFlowId}`,
195+
'aria-label': node.ariaLabel,
196+
'onMouseenter': onMouseEnter,
197+
'onMousemove': onMouseMove,
198+
'onMouseleave': onMouseLeave,
199+
'onContextmenu': onContextMenu,
200+
'onClick': onSelectNode,
201+
'onDblclick': onDoubleClick,
202+
'onKeydown': onKeyDown,
203+
},
204+
[
205+
h(props.type === false ? getNodeTypes.default : props.type, {
206+
id: node.id,
207+
type: node.type,
208+
data: node.data,
209+
events: { ...node.events, ...on },
210+
selected: !!node.selected,
211+
resizing: !!node.resizing,
212+
dragging,
213+
connectable: props.connectable,
214+
position: node.position,
215+
dimensions: node.dimensions,
216+
isValidTargetPos: node.isValidTargetPos,
217+
isValidSourcePos: node.isValidSourcePos,
218+
parentNode: node.parentNode,
219+
zIndex: node.computedPosition.z,
220+
targetPosition: node.targetPosition,
221+
sourcePosition: node.sourcePosition,
222+
label: node.label,
223+
dragHandle: node.dragHandle,
224+
onUpdateNodeInternals: updateInternals,
225+
}),
226+
],
227+
)
228+
229+
/** this re-calculates the current position, necessary for clamping by a node's extent */
230+
function clampPosition() {
231+
const nextPos = node.computedPosition
232+
233+
if (snapToGrid) {
234+
nextPos.x = snapGrid[0] * Math.round(nextPos.x / snapGrid[0])
235+
nextPos.y = snapGrid[1] * Math.round(nextPos.y / snapGrid[1])
236+
}
237+
238+
const { computedPosition, position } = calcNextPosition(node, nextPos, nodeExtent, parentNode)
239+
240+
// only overwrite positions if there are changes when clamping
241+
if (node.computedPosition.x !== computedPosition.x || node.computedPosition.y !== computedPosition.y) {
242+
node.computedPosition = { ...node.computedPosition, ...computedPosition }
243+
}
244+
245+
if (node.position.x !== position.x || node.position.y !== position.y) {
246+
node.position = position
247+
}
248+
}
249+
250+
function updateInternals() {
251+
if (nodeElement.value) updateNodeDimensions([{ id: props.id, nodeElement: nodeElement.value, forceUpdate: true }])
252+
}
253+
254+
function onMouseEnter(event: MouseEvent) {
255+
if (!dragging?.value) {
256+
emit.mouseEnter({ event, node, connectedEdges })
257+
}
258+
}
259+
260+
function onMouseMove(event: MouseEvent) {
261+
if (!dragging?.value) {
262+
emit.mouseMove({ event, node, connectedEdges })
263+
}
264+
}
265+
266+
function onMouseLeave(event: MouseEvent) {
267+
if (!dragging?.value) {
268+
emit.mouseLeave({ event, node, connectedEdges })
269+
}
270+
}
271+
272+
function onContextMenu(event: MouseEvent) {
273+
return emit.contextMenu({ event, node, connectedEdges })
274+
}
275+
276+
function onDoubleClick(event: MouseEvent) {
277+
return emit.doubleClick({ event, node, connectedEdges })
278+
}
279+
280+
function onSelectNode(event: MouseEvent) {
281+
if (props.selectable && (!selectNodesOnDrag || !props.draggable)) {
282+
handleNodeClick(
283+
node,
284+
multiSelectionActive,
285+
addSelectedNodes,
286+
removeSelectedNodes,
287+
$$(nodesSelectionActive),
288+
false,
289+
nodeElement.value!,
290+
)
291+
}
292+
293+
emit.click({ event, node, connectedEdges })
294+
}
295+
296+
function onKeyDown(event: KeyboardEvent) {
297+
if (isInputDOMNode(event)) return
298+
299+
if (elementSelectionKeys.includes(event.key) && props.selectable) {
300+
const unselect = event.key === 'Escape'
301+
302+
if (unselect) {
303+
nodeElement.value?.blur()
304+
}
305+
306+
handleNodeClick(
307+
node,
308+
multiSelectionActive,
309+
addSelectedNodes,
310+
removeSelectedNodes,
311+
$$(nodesSelectionActive),
312+
unselect,
313+
nodeElement.value!,
314+
)
315+
} else if (!disableKeyboardA11y && props.draggable && node.selected && arrowKeyDiffs[event.key]) {
316+
$$(ariaLiveMessage).value = `Moved selected node ${event.key
317+
.replace('Arrow', '')
318+
.toLowerCase()}. New position, x: ${~~node.position.x}, y: ${~~node.position.y}`
319+
320+
updateNodePositions(
321+
{
322+
x: arrowKeyDiffs[event.key].x,
323+
y: arrowKeyDiffs[event.key].y,
324+
},
325+
event.shiftKey,
326+
)
327+
}
328+
}
329+
},
330+
})
331+
332+
export default NodeWrapper
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export { default as DefaultNode } from './DefaultNode'
22
export { default as InputNode } from './InputNode'
33
export { default as OutputNode } from './OutputNode'
4-
export { default as NodeWrapper } from './NodeWrapper.vue'
4+
export { default as NodeWrapper } from './NodeWrapper'

0 commit comments

Comments
 (0)