From 7421e158d1c32684407a59a27fb1224dfdee2420 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 15:49:32 +0800 Subject: [PATCH] fix: portal local edit prompt inputs --- components/canvas/nodes/ImageNode.tsx | 281 +++++++++++++++----------- 1 file changed, 162 insertions(+), 119 deletions(-) diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index b637c96b2..f7b14d2b7 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -2352,6 +2352,46 @@ export function ImageNode({ style: getNormalizedRectOverlayStyle(region.rect, boxCutoutMetrics), })); }, [boxCutoutMetrics, localEditRegions]); + const localEditPromptPortalOverlays = useMemo(() => { + if ( + !localEditMode || + !boxCutoutMetrics || + typeof window === "undefined" || + !containerRef.current + ) { + return []; + } + + const nodeRect = containerRef.current.getBoundingClientRect(); + return localEditRegionOverlays.map(({ region, style }) => { + const left = Number(style.left || 0); + const top = Number(style.top || 0); + const width = Number(style.width || 0); + const height = Number(style.height || 0); + const imageBottom = boxCutoutMetrics.offsetY + boxCutoutMetrics.renderedHeight; + const placeAbove = top + height + 58 > imageBottom; + const promptTop = placeAbove ? top - 46 : top + height + 8; + const promptWidth = Math.min(Math.max(width, 176), 300); + + return { + region, + isEditing: editingLocalEditRegionId === region.id || !region.prompt.trim(), + style: { + left: nodeRect.left + left * nodeContentScale, + top: nodeRect.top + Math.max(4, promptTop) * nodeContentScale, + width: promptWidth, + transform: `scale(${nodeContentScale})`, + transformOrigin: "top left", + } satisfies React.CSSProperties, + }; + }); + }, [ + boxCutoutMetrics, + editingLocalEditRegionId, + localEditMode, + localEditRegionOverlays, + nodeContentScale, + ]); const currentPickerClickStyle = useMemo(() => { if (!boxCutoutMetrics || !currentClick) return null; @@ -7513,130 +7553,26 @@ export function ImageNode({ const top = Number(style.top || 0); const width = Number(style.width || 0); const height = Number(style.height || 0); - const imageBottom = boxCutoutMetrics - ? boxCutoutMetrics.offsetY + boxCutoutMetrics.renderedHeight - : node.size.height; - const placeAbove = top + height + 58 > imageBottom; - const promptTop = placeAbove ? top - 46 : top + height + 8; - const promptWidth = Math.min(Math.max(width, 176), 300); - const isEditing = - editingLocalEditRegionId === region.id || !region.prompt.trim(); return ( - +
-
- {region.index} -
+ {region.index}
-
event.stopPropagation()} - onClickCapture={(event) => event.stopPropagation()} - onMouseDown={(event) => event.stopPropagation()} - onMouseDownCapture={(event) => event.stopPropagation()} - onPointerDown={(event) => event.stopPropagation()} - onPointerDownCapture={(event) => event.stopPropagation()} - style={{ - left, - top: Math.max(4, promptTop), - width: promptWidth, - }} - > - {isEditing ? ( - { - const relatedTarget = - event.relatedTarget instanceof HTMLElement - ? event.relatedTarget - : null; - const nextPrompt = event.currentTarget.value.trim(); - if ( - !nextPrompt && - editingLocalEditRegionId === region.id && - !relatedTarget?.closest("[data-local-edit-control='true']") - ) { - focusLocalEditPromptInput(region.id); - } - }} - onChange={(event) => - updateLocalEditRegionPrompt(region.id, event.target.value) - } - onClick={(event) => { - event.stopPropagation(); - focusLocalEditPromptInput(region.id); - }} - onFocus={() => setEditingLocalEditRegionId(region.id)} - onKeyDown={(event) => { - event.stopPropagation(); - if (event.key === "Enter") { - event.preventDefault(); - persistLocalEditRegionPrompt( - region.id, - event.currentTarget.value - ); - setEditingLocalEditRegionId(null); - } - if (event.key === "Escape") { - event.preventDefault(); - persistLocalEditRegionPrompt( - region.id, - event.currentTarget.value - ); - setEditingLocalEditRegionId(null); - } - }} - onKeyUp={(event) => event.stopPropagation()} - onMouseDown={(event) => event.stopPropagation()} - onMouseDownCapture={(event) => event.stopPropagation()} - onPointerDown={(event) => event.stopPropagation()} - onPointerDownCapture={(event) => { - event.stopPropagation(); - focusLocalEditPromptInput(region.id); - }} - placeholder={`区域 ${region.index} 提示词`} - ref={(input) => { - localEditPromptInputRefs.current[region.id] = input; - }} - value={region.prompt} - /> - ) : ( - - )} - -
- +
); })} {localEditDraftRectStyle ? ( @@ -8200,6 +8136,113 @@ export function ImageNode({ {/* 工具栏使用 Portal 渲染到 body,避免被裁剪 */} + {localEditMode && + typeof document !== "undefined" && + localEditPromptPortalOverlays.map(({ region, isEditing, style }) => + createPortal( +
event.stopPropagation()} + onClickCapture={(event) => event.stopPropagation()} + onMouseDown={(event) => event.stopPropagation()} + onMouseDownCapture={(event) => event.stopPropagation()} + onPointerDown={(event) => event.stopPropagation()} + onPointerDownCapture={(event) => event.stopPropagation()} + style={style} + > + {isEditing ? ( + { + const relatedTarget = + event.relatedTarget instanceof HTMLElement + ? event.relatedTarget + : null; + const nextPrompt = event.currentTarget.value; + if (nextPrompt.trim()) { + persistLocalEditRegionPrompt(region.id, nextPrompt); + setEditingLocalEditRegionId(null); + return; + } + if ( + editingLocalEditRegionId === region.id && + !relatedTarget?.closest("[data-local-edit-control='true']") + ) { + focusLocalEditPromptInput(region.id); + } + }} + onChange={(event) => + updateLocalEditRegionPrompt(region.id, event.target.value) + } + onClick={(event) => { + event.stopPropagation(); + focusLocalEditPromptInput(region.id); + }} + onFocus={() => setEditingLocalEditRegionId(region.id)} + onKeyDown={(event) => { + event.stopPropagation(); + if (event.key === "Enter") { + event.preventDefault(); + persistLocalEditRegionPrompt( + region.id, + event.currentTarget.value + ); + setEditingLocalEditRegionId(null); + } + if (event.key === "Escape") { + event.preventDefault(); + persistLocalEditRegionPrompt( + region.id, + event.currentTarget.value + ); + setEditingLocalEditRegionId(null); + } + }} + onKeyUp={(event) => event.stopPropagation()} + onMouseDown={(event) => event.stopPropagation()} + onMouseDownCapture={(event) => event.stopPropagation()} + onPointerDown={(event) => event.stopPropagation()} + onPointerDownCapture={(event) => { + event.stopPropagation(); + focusLocalEditPromptInput(region.id); + }} + placeholder={`区域 ${region.index} 提示词`} + ref={(input) => { + localEditPromptInputRefs.current[region.id] = input; + }} + value={region.prompt} + /> + ) : ( + + )} + +
, + document.body + ) + )} + {isSelected && !isMultiSelected && data.src &&