get group resize, enter and exit working

This commit is contained in:
matei jordache
2026-03-27 14:13:09 -07:00
parent 98d36eb327
commit 1eda4030d1
11 changed files with 362 additions and 24 deletions

View File

@@ -89,8 +89,19 @@ function getConnectionHandleType(handleId) {
}
function getNodeDimension(node, axis) {
if (axis === 'width') return node.measured?.width || node.width || node.style?.width || 200;
return node.measured?.height || node.height || node.style?.height || 120;
if (axis === 'width') return node.measured?.width || node.style?.width || node.width || 200;
return node.measured?.height || node.style?.height || node.height || 120;
}
function applyNodeSize(node, width, height) {
const nextWidth = Math.round(Number(width) || 0);
const nextHeight = Math.round(Number(height) || 0);
return {
...node,
width: nextWidth,
height: nextHeight,
style: { ...(node.style || {}), width: nextWidth, height: nextHeight },
};
}
function getNodeAbsolutePosition(node, nodeMap) {
@@ -195,6 +206,17 @@ function getNodeRect(node, nodeMap) {
};
}
function getAbsoluteRectForNodePosition(node, absolutePosition) {
const width = Number(getNodeDimension(node, 'width')) || 200;
const height = Number(getNodeDimension(node, 'height')) || 120;
return {
left: absolutePosition.x,
top: absolutePosition.y,
right: absolutePosition.x + width,
bottom: absolutePosition.y + height,
};
}
function rectContainsPoint(rect, point) {
return point.x >= rect.left
&& point.x <= rect.right
@@ -202,6 +224,13 @@ function rectContainsPoint(rect, point) {
&& point.y <= rect.bottom;
}
function rectContainsRect(outerRect, innerRect) {
return innerRect.left >= outerRect.left
&& innerRect.top >= outerRect.top
&& innerRect.right <= outerRect.right
&& innerRect.bottom <= outerRect.bottom;
}
function getEventClientPosition(event) {
if (!event) return null;
const point = 'changedTouches' in event && event.changedTouches?.[0]
@@ -829,10 +858,11 @@ function Flow() {
};
const collapsedHeight = Math.max(74, 38 + Math.max(proxyData.proxyInputs.length, proxyData.proxyOutputs.length, 1) * 24 + 26);
return {
...node,
style: collapsed
? { ...(node.style || {}), width: 260, height: collapsedHeight }
: { ...(node.style || {}), width: expandedSize.width, height: expandedSize.height },
...applyNodeSize(
node,
collapsed ? 260 : expandedSize.width,
collapsed ? collapsedHeight : expandedSize.height,
),
data: {
...node.data,
collapsed,
@@ -1014,6 +1044,8 @@ function Flow() {
type: 'custom',
className: 'group-shell',
position: groupPosition,
width: groupWidth,
height: groupHeight,
dragHandle: '.drag-handle',
style: { width: groupWidth, height: groupHeight },
data: {
@@ -1726,14 +1758,39 @@ function Flow() {
setNodes,
]);
const resizeGroup = useCallback((groupId, size) => {
const nextWidth = Math.round(Number(size?.width) || 0);
const nextHeight = Math.round(Number(size?.height) || 0);
if (!nextWidth || !nextHeight) return;
setNodes((existing) => existing.map((node) => {
if (String(node.id) !== String(groupId) || node.data?.className !== 'Group') return node;
const sameSize = Math.abs((Number(node.style?.width) || 0) - nextWidth) < 0.5
&& Math.abs((Number(node.style?.height) || 0) - nextHeight) < 0.5;
if (sameSize) return node;
return {
...applyNodeSize(node, nextWidth, nextHeight),
data: {
...node.data,
expandedSize: { width: nextWidth, height: nextHeight },
},
};
}));
setTimeout(() => reactFlow.updateNodeInternals(String(groupId)), 0);
}, [reactFlow, setNodes]);
const contextValue = useMemo(() => ({
onWidgetChange,
onRuntimeValuesChange,
openFileBrowser,
onManualTrigger,
onToggleGroupCollapse: toggleGroupCollapse,
onResizeGroup: resizeGroup,
onUngroup: ungroupGroup,
}), [onRuntimeValuesChange, onWidgetChange, openFileBrowser, onManualTrigger, toggleGroupCollapse, ungroupGroup]);
}), [onRuntimeValuesChange, onWidgetChange, openFileBrowser, onManualTrigger, resizeGroup, toggleGroupCollapse, ungroupGroup]);
const clearGraph = useCallback(() => {
setNodes([]);
@@ -2000,6 +2057,8 @@ function Flow() {
anchorId: String(node.id),
anchorStartAbsolute: anchorAbsolute,
absolutePositions,
releasedNodeIds: new Set(),
touchedGroupIds: new Set(),
pointerOffset: {
x: pointerFlowPos.x - anchorAbsolute.x,
y: pointerFlowPos.y - anchorAbsolute.y,
@@ -2077,7 +2136,7 @@ function Flow() {
initializeDynamicNodes(duplicated.nodes);
}, [initializeDynamicNodes, reactFlow, setEdges, setNodes]);
const onNodeDrag = useCallback((_event, node) => {
const onNodeDrag = useCallback((event, node) => {
if (String(node.id) !== activeDragNodeIdRef.current) return;
const duplicateState = duplicateDragRef.current;
@@ -2121,7 +2180,103 @@ function Flow() {
}));
return;
}
}, [setNodes]);
const dragState = dragStateRef.current;
if (!dragState || node.data?.className === 'Group') return;
const currentNodes = reactFlow.getNodes();
const draggedNodes = node.selected
? currentNodes.filter((candidate) => candidate.selected && candidate.data?.className !== 'Group')
: currentNodes.filter((candidate) => candidate.id === node.id);
if (draggedNodes.length === 0) return;
const dragIntent = getDragIntent(event, reactFlow, dragState);
if (!dragIntent?.pointerFlowPos) return;
const draggedIdSet = new Set(draggedNodes.map((candidate) => String(candidate.id)));
const nodeMap = new Map(currentNodes.map((candidate) => [String(candidate.id), candidate]));
const releasedNodeIds = dragState.releasedNodeIds instanceof Set
? new Set(dragState.releasedNodeIds)
: new Set();
const touchedGroupIds = dragState.touchedGroupIds instanceof Set
? new Set(dragState.touchedGroupIds)
: new Set();
let nextNodes = currentNodes;
let changed = false;
let structureChanged = false;
nextNodes = nextNodes.map((candidate) => {
const candidateId = String(candidate.id);
if (!draggedIdSet.has(candidateId)) return candidate;
const absolute = dragIntent.absolutePositions.get(candidateId)
|| getNodeAbsolutePosition(candidate, nodeMap);
if (!absolute) return candidate;
if (candidate.parentId) {
const parentId = String(candidate.parentId);
const parentNode = nodeMap.get(parentId);
if (parentNode?.data?.className === 'Group') {
const parentRect = getGroupWorkspaceBounds(parentNode, nodeMap);
const parentAbsolute = getNodeAbsolutePosition(parentNode, nodeMap);
const nextPosition = {
x: absolute.x - parentAbsolute.x,
y: absolute.y - parentAbsolute.y,
};
const candidateRect = getAbsoluteRectForNodePosition(candidate, absolute);
const samePosition = Math.abs((Number(candidate.position?.x) || 0) - nextPosition.x) < 0.5
&& Math.abs((Number(candidate.position?.y) || 0) - nextPosition.y) < 0.5;
if (!releasedNodeIds.has(candidateId) && !rectContainsRect(parentRect, candidateRect)) {
releasedNodeIds.add(candidateId);
changed = true;
return {
...candidate,
extent: undefined,
hidden: false,
position: nextPosition,
};
}
if (releasedNodeIds.has(candidateId)) {
if (!candidate.parentId && !candidate.extent && candidate.hidden !== true && samePosition) {
return candidate;
}
changed = true;
return {
...candidate,
extent: undefined,
hidden: false,
position: nextPosition,
};
}
}
}
if (!releasedNodeIds.has(candidateId)) return candidate;
return candidate;
});
if (!changed) return;
dragStateRef.current = {
...dragState,
releasedNodeIds,
touchedGroupIds,
};
setNodes(structureChanged ? sortNodesForParentOrder(nextNodes) : nextNodes);
if (structureChanged) {
setTimeout(() => {
touchedGroupIds.forEach((groupId) => {
if (groupId) refreshGroupNode(groupId, nextNodes, reactFlow.getEdges());
});
}, 0);
}
}, [reactFlow, refreshGroupNode, setNodes]);
const onNodeDragStop = useCallback((event, node) => {
if (String(node.id) !== activeDragNodeIdRef.current) return;
@@ -2183,7 +2338,9 @@ function Flow() {
const currentNodes = reactFlow.getNodes();
const dragIntent = getDragIntent(event, reactFlow, dragState);
const touchedGroupIds = new Set();
const touchedGroupIds = dragState?.touchedGroupIds instanceof Set
? new Set(dragState.touchedGroupIds)
: new Set();
let nextNodes = currentNodes;
let changed = false;
@@ -2237,7 +2394,7 @@ function Flow() {
const alreadyInTarget = String(candidate.parentId || '') === String(targetGroup.id);
const samePosition = Math.abs((Number(candidate.position?.x) || 0) - nextPosition.x) < 0.5
&& Math.abs((Number(candidate.position?.y) || 0) - nextPosition.y) < 0.5;
if (alreadyInTarget && samePosition) return candidate;
if (alreadyInTarget && candidate.extent === 'parent' && samePosition) return candidate;
if (candidate.parentId) {
touchedGroupIds.add(String(candidate.parentId));
@@ -2261,7 +2418,6 @@ function Flow() {
});
}
} else {
const pointerFlowPos = dragIntent?.pointerFlowPos || getEventFlowPosition(event, reactFlow);
let removedCount = 0;
nextNodes = nextNodes.map((candidate) => {
@@ -2270,13 +2426,20 @@ function Flow() {
const parentId = String(candidate.parentId);
const parentNode = nodeMap.get(parentId);
if (!parentNode || parentNode.data?.className !== 'Group') return candidate;
if (!pointerFlowPos) return candidate;
if (rectContainsPoint(getNodeRect(parentNode, nodeMap), pointerFlowPos)) {
return candidate;
}
const absolute = dragIntent?.absolutePositions.get(String(candidate.id))
|| getNodeAbsolutePosition(candidate, nodeMap);
const parentWorkspaceRect = getGroupWorkspaceBounds(parentNode, nodeMap);
const candidateRect = getAbsoluteRectForNodePosition(candidate, absolute);
if (rectContainsRect(parentWorkspaceRect, candidateRect)) {
if (candidate.extent === 'parent') return candidate;
changed = true;
return {
...candidate,
extent: 'parent',
hidden: false,
};
}
touchedGroupIds.add(parentId);
removedCount += 1;
changed = true;
@@ -2298,7 +2461,16 @@ function Flow() {
}
}
if (!changed) return;
if (!changed) {
const releasedCount = dragState?.releasedNodeIds instanceof Set ? dragState.releasedNodeIds.size : 0;
if (releasedCount > 0) {
setStatus({
text: `Removed ${releasedCount} node${releasedCount === 1 ? '' : 's'} from group.`,
level: 'info',
});
}
return;
}
setNodes(sortNodesForParentOrder(nextNodes));
setTimeout(() => {