This commit is contained in:
呈祥
2026-01-07 00:43:57 +08:00
parent aa123e5cf2
commit 4e5a8a1b47
+71 -10
View File
@@ -7,8 +7,10 @@ import { getDemoServerInstance } from "./ot/server";
function Editor({ name }: { name: string }) {
const clientRef = useRef<Client | null>(null);
const [doc, setDoc] = useState<string>("");
const [inputValue, setInputValue] = useState<string>("");
const prevDocRef = useRef<string>("");
const [enableSync, setEnableSync] = useState<boolean>(true);
const isComposingRef = useRef<boolean>(false);
// 初始化client和文档状态
useLayoutEffect(() => {
@@ -18,6 +20,7 @@ function Editor({ name }: { name: string }) {
const initialDoc = clientRef.current.document || "";
prevDocRef.current = initialDoc;
setDoc(initialDoc);
setInputValue(initialDoc);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -35,9 +38,19 @@ function Editor({ name }: { name: string }) {
return () => clearInterval(timer);
}, []);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
// 当doc变化时,同步到inputValue
useEffect(() => {
setInputValue(doc);
}, [doc]);
const handleChange = (newValue: string) => {
if (!clientRef.current) return;
const newValue = e.target.value;
// 立即更新inputValue,让输入框响应更快
setInputValue(newValue);
// 如果正在输入CJK字符,不触发onChange逻辑
if (isComposingRef.current) {
return;
}
// 使用client当前文档状态作为旧值
const oldValue = clientRef.current.document || "";
if (newValue !== oldValue) {
@@ -50,6 +63,16 @@ function Editor({ name }: { name: string }) {
}
};
const handleCompositionStart = () => {
isComposingRef.current = true;
};
const handleCompositionEnd = (e: React.CompositionEvent<HTMLTextAreaElement>) => {
isComposingRef.current = false;
// composition结束后,手动触发一次change处理
handleChange(e.currentTarget.value);
};
const handleToggleSync = () => {
const newValue = !enableSync;
setEnableSync(newValue);
@@ -59,7 +82,9 @@ function Editor({ name }: { name: string }) {
};
return (
<div style={{ border: "1px solid #ccc", padding: 10 }}>
<div
style={{ border: "1px solid #ccc", padding: 10, display: "flex", flexDirection: "column", paddingRight: 16 }}
>
<div
style={{
display: "flex",
@@ -74,27 +99,63 @@ function Editor({ name }: { name: string }) {
<span></span>
</label>
</div>
<textarea value={doc} onChange={handleChange} rows={5} />
<textarea
value={inputValue}
onChange={(e) => handleChange(e.target.value)}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
style={{ width: "100%", height: 100 }}
/>
</div>
);
}
function App() {
export function Server() {
const [serverDoc, setServerDoc] = useState<string>("");
const [operations, setOperations] = useState<string[]>([]);
useEffect(() => {
const timer = setInterval(() => {
setServerDoc(getDemoServerInstance().document || "");
const server = getDemoServerInstance();
setServerDoc(server.document || "");
setOperations(server.operations.map((op) => op.toString()));
}, 100);
return () => clearInterval(timer);
}, []);
return (
<div>
<div style={{ border: "1px solid #ccc", padding: 10, height: 100 }}>
<p>Server Document:</p>
<p>{serverDoc}</p>
<div style={{ border: "1px solid #ccc", padding: 10, height: 150 }}>
<div style={{ display: "flex", gap: 20 }}>
<div style={{ flex: 1 }}>
<p style={{ margin: "0 0 10px 0" }}>Server Document:</p>
<p style={{ margin: 0 }}>{serverDoc}</p>
</div>
<div style={{ flex: 1 }}>
<p style={{ margin: "0 0 10px 0" }}>Operations:</p>
<div style={{ maxHeight: 100, overflowY: "auto" }}>
{operations.length === 0 ? (
<p style={{ margin: 0, color: "#999", fontSize: 14 }}></p>
) : (
operations.map((opStr, index) => (
<div
key={index}
style={{ marginBottom: 4, fontFamily: "monospace", fontSize: "12px" }}
>
{index + 1}. {opStr}
</div>
))
)}
</div>
</div>
</div>
</div>
);
}
function App() {
return (
<div>
<Server />
<div style={{ display: "flex", gap: 10, marginTop: 20 }}>
<Editor name="A" />
<Editor name="B" />