[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
156
components/notes/NoteTitleInput.tsx
Normal file
156
components/notes/NoteTitleInput.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
resolveSupersededImeInputEvent,
|
||||
shouldAdoptExternalImeControlledValue,
|
||||
shouldCommitImeControlledChange,
|
||||
} from "../../domain/imeControlledInput";
|
||||
|
||||
type NoteTitleInputProps = {
|
||||
noteId: string;
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
/** Commit into parent draft state (may rewrite controlled value). Idle IME only. */
|
||||
onCommit: (title: string) => void;
|
||||
/**
|
||||
* Stash title for crash/teardown flush without updating controlled React state.
|
||||
* Called during IME composition so pagehide/note-switch can persist without
|
||||
* fighting the composition buffer.
|
||||
*/
|
||||
onLiveDraft?: (title: string) => void;
|
||||
onBlur?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Controlled note-title field that does not push parent updates during CJK IME
|
||||
* composition. Immediate `value={external}` writes mid-composition break Windows
|
||||
* IMEs such as Sogou Wubi (candidate dismiss / no committed text).
|
||||
*/
|
||||
export const NoteTitleInput: React.FC<NoteTitleInputProps> = ({
|
||||
noteId,
|
||||
value,
|
||||
placeholder,
|
||||
className,
|
||||
onCommit,
|
||||
onLiveDraft,
|
||||
onBlur,
|
||||
}) => {
|
||||
const [draft, setDraft] = useState(value);
|
||||
const composingRef = useRef(false);
|
||||
const valueAtComposeStartRef = useRef(value);
|
||||
const supersededRef = useRef(false);
|
||||
const noteIdRef = useRef(noteId);
|
||||
|
||||
const onLiveDraftRef = useRef(onLiveDraft);
|
||||
onLiveDraftRef.current = onLiveDraft;
|
||||
|
||||
useEffect(() => {
|
||||
if (noteIdRef.current !== noteId) {
|
||||
noteIdRef.current = noteId;
|
||||
composingRef.current = false;
|
||||
supersededRef.current = false;
|
||||
setDraft(value);
|
||||
return;
|
||||
}
|
||||
|
||||
let adoptedExternal: string | null = null;
|
||||
setDraft((draftValue) => {
|
||||
const composing = composingRef.current;
|
||||
const shouldAdopt = shouldAdoptExternalImeControlledValue({
|
||||
isComposingSession: composing,
|
||||
draftValue,
|
||||
externalValue: value,
|
||||
valueAtComposeStart: composing ? valueAtComposeStartRef.current : undefined,
|
||||
});
|
||||
if (shouldAdopt && composing && value !== valueAtComposeStartRef.current) {
|
||||
supersededRef.current = true;
|
||||
adoptedExternal = value;
|
||||
}
|
||||
return shouldAdopt ? value : draftValue;
|
||||
});
|
||||
if (adoptedExternal !== null) {
|
||||
onLiveDraftRef.current?.(adoptedExternal);
|
||||
}
|
||||
}, [noteId, value]);
|
||||
|
||||
const commit = (next: string) => {
|
||||
composingRef.current = false;
|
||||
supersededRef.current = false;
|
||||
setDraft(next);
|
||||
onCommit(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
data-note-title-input="true"
|
||||
className={className}
|
||||
value={draft}
|
||||
placeholder={placeholder}
|
||||
onBlur={(event) => {
|
||||
// Blur finalizes IME. Sync parent before flushNoteDraft so composition-only
|
||||
// titles and superseded external adoptions both land in draftTitleRef.
|
||||
composingRef.current = false;
|
||||
if (supersededRef.current) {
|
||||
supersededRef.current = false;
|
||||
setDraft(value);
|
||||
onCommit(value);
|
||||
} else {
|
||||
const next = event.currentTarget.value;
|
||||
setDraft(next);
|
||||
onCommit(next);
|
||||
}
|
||||
onBlur?.();
|
||||
}}
|
||||
onChange={(event) => {
|
||||
const superseded = resolveSupersededImeInputEvent({
|
||||
compositionExternallySuperseded: supersededRef.current,
|
||||
isComposingSession: composingRef.current,
|
||||
nativeEventIsComposing: event.nativeEvent.isComposing,
|
||||
});
|
||||
if (superseded.ignoreEventValue) {
|
||||
if (superseded.clearSupersedeLatch) {
|
||||
supersededRef.current = false;
|
||||
}
|
||||
setDraft(value);
|
||||
return;
|
||||
}
|
||||
|
||||
const next = event.target.value;
|
||||
setDraft(next);
|
||||
// Always stash for teardown/note-switch flush; do not fight IME via onCommit.
|
||||
onLiveDraft?.(next);
|
||||
if (
|
||||
shouldCommitImeControlledChange({
|
||||
isComposingSession: composingRef.current,
|
||||
nativeEventIsComposing: event.nativeEvent.isComposing,
|
||||
compositionExternallySuperseded: supersededRef.current,
|
||||
})
|
||||
) {
|
||||
onCommit(next);
|
||||
}
|
||||
}}
|
||||
onCompositionStart={() => {
|
||||
composingRef.current = true;
|
||||
supersededRef.current = false;
|
||||
valueAtComposeStartRef.current = value;
|
||||
}}
|
||||
onCompositionEnd={(event) => {
|
||||
composingRef.current = false;
|
||||
if (value !== valueAtComposeStartRef.current || supersededRef.current) {
|
||||
supersededRef.current = true;
|
||||
setDraft(value);
|
||||
// Drop any live-stashed composed text so teardown flush cannot persist
|
||||
// the rejected IME draft over the authoritative external title.
|
||||
onLiveDraft?.(value);
|
||||
window.setTimeout(() => {
|
||||
if (supersededRef.current && !composingRef.current) {
|
||||
supersededRef.current = false;
|
||||
}
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
commit(event.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user