fix: portal local edit prompt inputs
This commit is contained in:
parent
1dd1251880
commit
7421e158d1
@ -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 (
|
||||
<Fragment key={region.id}>
|
||||
<div
|
||||
className="pointer-events-none absolute z-[160] rounded-[10px] border-[3px] bg-transparent shadow-[0_0_0_1px_rgba(255,255,255,0.92),0_0_22px_rgba(0,0,0,0.2)]"
|
||||
key={region.id}
|
||||
style={{
|
||||
left,
|
||||
top,
|
||||
width: Math.max(width, 1),
|
||||
height: Math.max(height, 1),
|
||||
borderColor: region.color,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="pointer-events-none absolute z-[160] rounded-[10px] border-[3px] bg-transparent shadow-[0_0_0_1px_rgba(255,255,255,0.92),0_0_22px_rgba(0,0,0,0.2)]"
|
||||
style={{
|
||||
left,
|
||||
top,
|
||||
width: Math.max(width, 1),
|
||||
height: Math.max(height, 1),
|
||||
borderColor: region.color,
|
||||
}}
|
||||
className="-left-3 -top-3 absolute flex h-7 w-7 items-center justify-center rounded-full border-2 border-white text-[12px] font-black text-white shadow-[0_8px_18px_rgba(0,0,0,0.22)]"
|
||||
style={{ backgroundColor: region.color }}
|
||||
>
|
||||
<div
|
||||
className="-left-3 -top-3 absolute flex h-7 w-7 items-center justify-center rounded-full border-2 border-white text-[12px] font-black text-white shadow-[0_8px_18px_rgba(0,0,0,0.22)]"
|
||||
style={{ backgroundColor: region.color }}
|
||||
>
|
||||
{region.index}
|
||||
</div>
|
||||
{region.index}
|
||||
</div>
|
||||
<div
|
||||
className="absolute z-[175] flex items-center gap-1.5"
|
||||
data-canvas-no-drag="true"
|
||||
onClick={(event) => 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 ? (
|
||||
<input
|
||||
autoFocus={editingLocalEditRegionId === region.id}
|
||||
className="nodrag nopan nowheel h-9 min-w-0 flex-1 rounded-full border border-slate-300 bg-white/95 px-3 text-[12px] font-medium text-slate-950 caret-slate-950 shadow-[0_10px_24px_rgba(15,23,42,0.14)] outline-none ring-2 ring-transparent transition placeholder:text-slate-400 focus:border-slate-950 focus:ring-slate-950/15 dark:border-white/20 dark:bg-slate-950/88 dark:text-white dark:caret-white dark:placeholder:text-white/42 dark:focus:border-white/40 dark:focus:ring-white/18"
|
||||
data-canvas-no-drag="true"
|
||||
onBlur={(event) => {
|
||||
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}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
className="h-9 min-w-0 flex-1 truncate rounded-full border border-slate-200 bg-white/95 px-3 text-left text-[12px] font-semibold text-slate-900 shadow-[0_10px_24px_rgba(15,23,42,0.14)] hover:bg-slate-50 dark:border-white/18 dark:bg-slate-950/82 dark:text-white dark:shadow-[0_10px_24px_rgba(0,0,0,0.24)] dark:hover:bg-slate-900"
|
||||
onClick={() => setEditingLocalEditRegionId(region.id)}
|
||||
title={region.prompt}
|
||||
type="button"
|
||||
>
|
||||
{region.index}. {region.prompt}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-slate-200 bg-white/95 text-slate-500 shadow-[0_10px_24px_rgba(15,23,42,0.12)] hover:bg-red-500 hover:text-white dark:border-white/18 dark:bg-slate-950/82 dark:text-white/72 dark:shadow-[0_10px_24px_rgba(0,0,0,0.24)]"
|
||||
data-local-edit-control="true"
|
||||
onClick={() => deleteLocalEditRegion(region.id)}
|
||||
title="删除区域"
|
||||
type="button"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{localEditDraftRectStyle ? (
|
||||
@ -8200,6 +8136,113 @@ export function ImageNode({
|
||||
</Dialog>
|
||||
|
||||
{/* 工具栏使用 Portal 渲染到 body,避免被裁剪 */}
|
||||
{localEditMode &&
|
||||
typeof document !== "undefined" &&
|
||||
localEditPromptPortalOverlays.map(({ region, isEditing, style }) =>
|
||||
createPortal(
|
||||
<div
|
||||
className="fixed z-[10000] flex items-center gap-1.5"
|
||||
data-canvas-no-drag="true"
|
||||
data-local-edit-control="true"
|
||||
key={region.id}
|
||||
onClick={(event) => 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 ? (
|
||||
<input
|
||||
autoFocus={editingLocalEditRegionId === region.id}
|
||||
className="nodrag nopan nowheel h-9 min-w-0 flex-1 rounded-full border border-slate-300 bg-white/95 px-3 text-[12px] font-medium text-slate-950 caret-slate-950 shadow-[0_10px_24px_rgba(15,23,42,0.14)] outline-none ring-2 ring-transparent transition placeholder:text-slate-400 focus:border-slate-950 focus:ring-slate-950/15 dark:border-white/20 dark:bg-slate-950/88 dark:text-white dark:caret-white dark:placeholder:text-white/42 dark:focus:border-white/40 dark:focus:ring-white/18"
|
||||
data-canvas-no-drag="true"
|
||||
data-local-edit-control="true"
|
||||
onBlur={(event) => {
|
||||
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}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
className="h-9 min-w-0 flex-1 truncate rounded-full border border-slate-200 bg-white/95 px-3 text-left text-[12px] font-semibold text-slate-900 shadow-[0_10px_24px_rgba(15,23,42,0.14)] hover:bg-slate-50 dark:border-white/18 dark:bg-slate-950/82 dark:text-white dark:shadow-[0_10px_24px_rgba(0,0,0,0.24)] dark:hover:bg-slate-900"
|
||||
data-local-edit-control="true"
|
||||
onClick={() => setEditingLocalEditRegionId(region.id)}
|
||||
title={region.prompt}
|
||||
type="button"
|
||||
>
|
||||
{region.index}. {region.prompt}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-slate-200 bg-white/95 text-slate-500 shadow-[0_10px_24px_rgba(15,23,42,0.12)] hover:bg-red-500 hover:text-white dark:border-white/18 dark:bg-slate-950/82 dark:text-white/72 dark:shadow-[0_10px_24px_rgba(0,0,0,0.24)]"
|
||||
data-local-edit-control="true"
|
||||
onClick={() => deleteLocalEditRegion(region.id)}
|
||||
title="删除区域"
|
||||
type="button"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
)}
|
||||
|
||||
{isSelected &&
|
||||
!isMultiSelected &&
|
||||
data.src &&
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user