Skip to content

Commit

Permalink
feat: can undo and redo changes
Browse files Browse the repository at this point in the history
  • Loading branch information
lawvs committed Apr 12, 2024
1 parent 14da774 commit fe8fa81
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 4 deletions.
41 changes: 40 additions & 1 deletion src/components/config-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { useConfig, useYDoc } from "../state";
import { Redo, Undo } from "lucide-react";
import { useConfig, useUndoManager, useYDoc } from "../state";
import { fileToYDoc } from "../utils";
import { ConnectButton } from "./connect-button";
import { ExportButton } from "./export-button";
import { FullScreenDropZone } from "./full-screen-drop-zone";
import { LoadButton } from "./load-button";
import { Button } from "./ui/button";
import { Label } from "./ui/label";
import { Switch } from "./ui/switch";
import { useToast } from "./ui/use-toast";
Expand All @@ -12,6 +14,8 @@ export function ConfigPanel() {
const [yDoc, setYDoc] = useYDoc();
const { toast } = useToast();
const [config, setConfig] = useConfig();
const { undoManager, canRedo, canUndo, undoStackSize, redoStackSize } =
useUndoManager();

return (
<div className="flex w-64 flex-col gap-4">
Expand Down Expand Up @@ -77,6 +81,41 @@ export function ConfigPanel() {
<Label htmlFor="editable-switch">Editable</Label>
</div>

{config.editable && (
<div className="flex items-center space-x-2">
<Button
className="flex-1"
variant="outline"
disabled={!canUndo}
onClick={() => {
if (!undoManager.canUndo()) {
console.warn("Cannot undo", undoManager);
return;
}
undoManager.undo();
}}
>
<Undo className="mr-2 h-4 w-4" />
Undo({undoStackSize})
</Button>
<Button
className="flex-1"
variant="outline"
disabled={!canRedo}
onClick={() => {
if (!undoManager.canRedo()) {
console.warn("Cannot redo", undoManager);
return;
}
undoManager.redo();
}}
>
<Redo className="mr-2 h-4 w-4" />
Redo({redoStackSize})
</Button>
</div>
)}

<ExportButton />

<FullScreenDropZone
Expand Down
74 changes: 72 additions & 2 deletions src/state.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,82 @@
import { atom, useAtom } from "jotai";
import { atom, useAtom, useAtomValue } from "jotai";
import { useEffect, useState } from "react";
import * as Y from "yjs";

const yDocAtom = atom(new Y.Doc());
const TRACK_ALL_ORIGINS = Symbol();

function createUndoManager(doc: Y.Doc) {
const undoManager = new Y.UndoManager([], {
doc,
trackedOrigins: new Set([TRACK_ALL_ORIGINS]),
});
doc.on("update", () => {
// The UndoManager can only track shared types that are created
// See https://discuss.yjs.dev/t/global-document-undo-manager/2555
const keys = Array.from(doc.share.keys());
if (!keys.length) return;
const scope = keys.map((key) => doc.get(key));
undoManager.addToScope(scope);
// undoManager.addTrackedOrigin(origin);
});
doc.on("beforeTransaction", (transaction) => {
// Try to track all origins
// Workaround for https://github.com/yjs/yjs/issues/624
transaction.origin = TRACK_ALL_ORIGINS;
});
return undoManager;
}

const defaultYDoc = new Y.Doc();
const defaultUndoManager = createUndoManager(defaultYDoc);

const undoManagerAtom = atom<Y.UndoManager>(defaultUndoManager);

const yDocAtom = atom(defaultYDoc, (get, set, newDoc: Y.Doc) => {
get(undoManagerAtom).destroy();
const undoManager = createUndoManager(newDoc);
set(undoManagerAtom, undoManager);
get(yDocAtom).destroy();
set(yDocAtom, newDoc);
});

export const useYDoc = () => {
return useAtom(yDocAtom);
};

export const useUndoManager = () => {
const undoManager = useAtomValue(undoManagerAtom);
const [state, setState] = useState({
canUndo: undoManager.canUndo(),
canRedo: undoManager.canRedo(),
undoStackSize: undoManager.undoStack.length,
redoStackSize: undoManager.redoStack.length,
});

useEffect(() => {
const callback = () => {
setState({
canUndo: undoManager.canUndo(),
canRedo: undoManager.canRedo(),
undoStackSize: undoManager.undoStack.length,
redoStackSize: undoManager.redoStack.length,
});
};
callback();

undoManager.on("stack-item-added", callback);
undoManager.on("stack-item-popped", callback);
return () => {
undoManager.off("stack-item-added", callback);
undoManager.off("stack-item-popped", callback);
};
}, [state, undoManager]);

return {
undoManager,
...state,
};
};

export type Config = {
parseYDoc: boolean;
showDelta: boolean;
Expand Down
4 changes: 3 additions & 1 deletion src/y-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export function guessType(abstractType: Y.AbstractType<unknown>) {
return Y.Map;
}
if (abstractType._length > 0) {
return Y.Array;
// TODO distinguish between Y.Text and Y.Array
return Y.Text;
// return Y.Array;
}
return Y.AbstractType;
}
Expand Down

0 comments on commit fe8fa81

Please sign in to comment.