fix: handle local edit prompt keyboard input
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-05-23 15:04:46 +08:00
parent 9ebac8b079
commit d9dea4eb75

View File

@ -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<string, HTMLInputElement | null>
>({});
const localEditPromptDraftRefs = useRef<Record<string, string>>({});
const localEditPromptCompositionStartRefs = useRef<
Record<string, LocalEditPromptCompositionSnapshot | undefined>
>({});
const localEditRegionsRef = useRef<LocalEditRegion[]>(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<HTMLInputElement>
) => {
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)