107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||
import "./App.css";
|
||
import { Client } from "./ot/client";
|
||
import { generateOperations } from "./ot/helpers";
|
||
import { getDemoServerInstance } from "./ot/server";
|
||
|
||
function Editor({ name }: { name: string }) {
|
||
const clientRef = useRef<Client | null>(null);
|
||
const [doc, setDoc] = useState<string>("");
|
||
const prevDocRef = useRef<string>("");
|
||
const [enableSync, setEnableSync] = useState<boolean>(true);
|
||
|
||
// 初始化client和文档状态
|
||
useLayoutEffect(() => {
|
||
if (!clientRef.current) {
|
||
clientRef.current = new Client();
|
||
clientRef.current.name = name;
|
||
const initialDoc = clientRef.current.document || "";
|
||
prevDocRef.current = initialDoc;
|
||
setDoc(initialDoc);
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!clientRef.current) return;
|
||
// 定期同步client的文档状态到UI
|
||
const timer = setInterval(() => {
|
||
const currentDoc = clientRef.current!.document || "";
|
||
if (currentDoc !== prevDocRef.current) {
|
||
setDoc(currentDoc);
|
||
prevDocRef.current = currentDoc;
|
||
}
|
||
}, 100);
|
||
return () => clearInterval(timer);
|
||
}, []);
|
||
|
||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||
if (!clientRef.current) return;
|
||
const newValue = e.target.value;
|
||
// 使用client当前文档状态作为旧值
|
||
const oldValue = clientRef.current.document || "";
|
||
if (newValue !== oldValue) {
|
||
// 生成操作集合
|
||
const operations = generateOperations(oldValue, newValue);
|
||
// 按顺序应用操作
|
||
operations.forEach((op) => {
|
||
clientRef.current!.applyClient(op);
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleToggleSync = () => {
|
||
const newValue = !enableSync;
|
||
setEnableSync(newValue);
|
||
if (clientRef.current) {
|
||
clientRef.current.setEnableSync(newValue);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div style={{ border: "1px solid #ccc", padding: 10 }}>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
alignItems: "center",
|
||
marginBottom: 10,
|
||
}}
|
||
>
|
||
<p style={{ margin: 0 }}>Document:</p>
|
||
<label style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }}>
|
||
<input type="checkbox" checked={enableSync} onChange={handleToggleSync} />
|
||
<span>同步到服务端</span>
|
||
</label>
|
||
</div>
|
||
<textarea value={doc} onChange={handleChange} rows={5} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function App() {
|
||
const [serverDoc, setServerDoc] = useState<string>("");
|
||
|
||
useEffect(() => {
|
||
const timer = setInterval(() => {
|
||
setServerDoc(getDemoServerInstance().document || "");
|
||
}, 100);
|
||
return () => clearInterval(timer);
|
||
}, []);
|
||
|
||
return (
|
||
<div>
|
||
<div style={{ border: "1px solid #ccc", padding: 10, height: 100 }}>
|
||
<p>Server Document:</p>
|
||
<p>{serverDoc}</p>
|
||
</div>
|
||
<div style={{ display: "flex", gap: 10, marginTop: 20 }}>
|
||
<Editor name="A" />
|
||
<Editor name="B" />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default App;
|