From d9dea4eb7567195a84ce1030c7b5b2b9d3ba9385 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 23 May 2026 15:04:46 +0800 Subject: [PATCH] fix: handle local edit prompt keyboard input --- components/canvas/nodes/ImageNode.tsx | 139 ++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index f46e294b8..8a5faad57 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -235,6 +235,12 @@ type LocalEditRegionInteractionState = { startRect: NormalizedCropRect; }; +type LocalEditPromptCompositionSnapshot = { + end: number; + start: number; + value: string; +}; + type AnglePreviewDragState = { pointerId: number; startX: number; @@ -2066,6 +2072,9 @@ export function ImageNode({ Record >({}); const localEditPromptDraftRefs = useRef>({}); + const localEditPromptCompositionStartRefs = useRef< + Record + >({}); const localEditRegionsRef = useRef(localEditRegions); const updateNode = useCanvasStore((state) => state.updateNode); @@ -2673,6 +2682,103 @@ export function ImageNode({ [] ); + const restoreLocalEditPromptSelection = useCallback( + (regionId: string, position: number) => { + if (typeof window === "undefined") return; + window.requestAnimationFrame(() => { + const input = localEditPromptInputRefs.current[regionId]; + if (!input) return; + const safePosition = clampNumber(position, 0, input.value.length); + input.setSelectionRange(safePosition, safePosition); + }); + }, + [] + ); + + const applyLocalEditPromptKeyEdit = useCallback( + ( + regionId: string, + input: HTMLInputElement, + event: React.KeyboardEvent + ) => { + if ( + event.nativeEvent.isComposing || + event.key === "Process" || + event.metaKey || + event.ctrlKey || + event.altKey + ) { + return false; + } + + const currentValue = localEditPromptDraftRefs.current[regionId] ?? input.value; + const selectionStart = input.selectionStart ?? currentValue.length; + const selectionEnd = input.selectionEnd ?? selectionStart; + const start = clampNumber(selectionStart, 0, currentValue.length); + const end = clampNumber(selectionEnd, start, currentValue.length); + let nextValue: string | null = null; + let nextCursor = start; + + if (event.key.length === 1) { + nextValue = `${currentValue.slice(0, start)}${event.key}${currentValue.slice(end)}`; + nextCursor = start + event.key.length; + } else if (event.key === "Backspace") { + if (start !== end) { + nextValue = `${currentValue.slice(0, start)}${currentValue.slice(end)}`; + nextCursor = start; + } else if (start > 0) { + nextValue = `${currentValue.slice(0, start - 1)}${currentValue.slice(end)}`; + nextCursor = start - 1; + } + } else if (event.key === "Delete") { + if (start !== end) { + nextValue = `${currentValue.slice(0, start)}${currentValue.slice(end)}`; + } else if (start < currentValue.length) { + nextValue = `${currentValue.slice(0, start)}${currentValue.slice(start + 1)}`; + } + nextCursor = start; + } + + if (nextValue === null) { + return false; + } + + event.preventDefault(); + updateLocalEditPromptDraft(regionId, nextValue); + restoreLocalEditPromptSelection(regionId, nextCursor); + return true; + }, + [restoreLocalEditPromptSelection, updateLocalEditPromptDraft] + ); + + const finishLocalEditPromptComposition = useCallback( + (regionId: string, input: HTMLInputElement, compositionText: string) => { + const snapshot = localEditPromptCompositionStartRefs.current[regionId]; + delete localEditPromptCompositionStartRefs.current[regionId]; + if (!snapshot) { + updateLocalEditPromptDraft(regionId, input.value); + return; + } + + if (input.value !== snapshot.value) { + updateLocalEditPromptDraft(regionId, input.value); + return; + } + + if (!compositionText) { + return; + } + + const nextValue = `${snapshot.value.slice(0, snapshot.start)}${compositionText}${snapshot.value.slice(snapshot.end)}`; + updateLocalEditPromptDraft(regionId, nextValue); + restoreLocalEditPromptSelection( + regionId, + snapshot.start + compositionText.length + ); + }, + [restoreLocalEditPromptSelection, updateLocalEditPromptDraft] + ); + const refreshBoxCutoutMetrics = useCallback(() => { const imageElement = displayImageRef.current; if (!imageElement) { @@ -3765,6 +3871,7 @@ export function ImageNode({ const deleteLocalEditRegion = useCallback( (regionId: string) => { delete localEditPromptDraftRefs.current[regionId]; + delete localEditPromptCompositionStartRefs.current[regionId]; setLocalEditPromptDrafts((current) => { if (!(regionId in current)) return current; const next = { ...current }; @@ -3872,6 +3979,7 @@ export function ImageNode({ const clearLocalEditRegions = useCallback(() => { localEditPromptDraftRefs.current = {}; + localEditPromptCompositionStartRefs.current = {}; setLocalEditPromptDrafts({}); commitLocalEditRegions([]); setEditingLocalEditRegionId(null); @@ -8135,6 +8243,28 @@ export function ImageNode({ onClick={(event) => { event.stopPropagation(); }} + onCompositionEnd={(event) => { + finishLocalEditPromptComposition( + region.id, + event.currentTarget, + event.data + ); + }} + onCompositionStart={(event) => { + localEditPromptCompositionStartRefs.current[ + region.id + ] = { + end: + event.currentTarget.selectionEnd ?? + event.currentTarget.value.length, + start: + event.currentTarget.selectionStart ?? + event.currentTarget.value.length, + value: + localEditPromptDraftRefs.current[region.id] ?? + event.currentTarget.value, + }; + }} onFocus={(event) => { event.stopPropagation(); setEditingLocalEditRegionId((current) => @@ -8154,6 +8284,15 @@ export function ImageNode({ if (isComposing) { return; } + if ( + applyLocalEditPromptKeyEdit( + region.id, + event.currentTarget, + event + ) + ) { + return; + } if ( event.key === "Enter" && (event.metaKey || event.ctrlKey)