Skip to content

Commit

Permalink
refactor into useUndoable
Browse files Browse the repository at this point in the history
  • Loading branch information
loganzartman committed Mar 17, 2024
1 parent e86281e commit 50ea6e1
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 36 deletions.
61 changes: 25 additions & 36 deletions src/app/reacjin/ReacjinEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,66 +29,55 @@ import {Panel} from '@/app/reacjin/Panel';
import {PanelProvider} from '@/app/reacjin/PanelContext';
import {pluginByID} from '@/app/reacjin/plugins/registry';
import {Toolbar} from '@/app/reacjin/Toolbar';
import {useUndoable} from '@/app/reacjin/useUndoable';
import {MotionDiv} from '@/lib/framer-motion';

export default function ReacjinEditor() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const editorAreaRef = useRef<HTMLDivElement>(null);
const [imageSize, setImageSize] = useState([256, 256]);
const [zoom, setZoom] = useState(1);
const [layers, _setLayers] = useState<Layers>(() => [
createLayer('text', {
text: 'Hello, world!',
autoFitText: false,
fontSize: 32,
fontFamily: 'sans-serif',
fillStyle: 'white',
strokeStyle: 'black',
strokeWidth: 2,
textAlign: 'center',
lineHeight: 1.1,
}),
createLayer('image', {src: 'https://picsum.photos/256'}),
]);
const [undoStack, setUndoStack] = useState<Layers[]>([]);
const [redoStack, setRedoStack] = useState<Layers[]>([]);
const {
state: layers,
setState: setLayers,
undo,
redo,
} = useUndoable(
useState<Layers>(() => [
createLayer('text', {
text: 'Hello, world!',
autoFitText: false,
fontSize: 32,
fontFamily: 'sans-serif',
fillStyle: 'white',
strokeStyle: 'black',
strokeWidth: 2,
textAlign: 'center',
lineHeight: 1.1,
}),
createLayer('image', {src: 'https://picsum.photos/256'}),
]),
);
const [selectedLayerID, setSelectedLayerID] = useState<string | null>(null);
const [computedCache] = useState(() => new ComputedCache());
const [computing, setComputing] = useState(false);
const [dropping, setDropping] = useState(false);

const setLayers = useCallback(
(action: React.SetStateAction<Layers>) => {
setUndoStack((undoStack) => [...undoStack, layers]);
setRedoStack([]);
_setLayers((layers) =>
typeof action === 'function' ? action(layers) : action,
);
},
[layers],
);

useEffect(() => {
function handler(event: KeyboardEvent) {
if (event.repeat) return;
if (event.key === 'z' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
if (undoStack.length < 1) return;
_setLayers(undoStack[undoStack.length - 1]);
setUndoStack((undoStack) => undoStack.slice(0, -1));
setRedoStack((redoStack) => [...redoStack, layers]);
undo();
}
if (event.key === 'y' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
if (redoStack.length < 1) return;
_setLayers(redoStack[redoStack.length - 1]);
setRedoStack((redoStack) => redoStack.slice(0, -1));
setUndoStack((undoStack) => [...undoStack, layers]);
redo();
}
}
document.addEventListener('keydown', handler, false);
return () => document.removeEventListener('keydown', handler, false);
}, [layers, redoStack, undoStack]);
}, [layers, redo, undo]);

const setZoomToSize = useCallback(
(size: number) => {
Expand Down
73 changes: 73 additions & 0 deletions src/app/reacjin/useUndoable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, {useCallback, useMemo, useRef, useState} from 'react';

type Undoable<T> = {
state: T;
setState: React.Dispatch<React.SetStateAction<T>>;
undo: () => void;
redo: () => void;
canUndo: boolean;
canRedo: boolean;
};

function isStateUpdater<T>(
action: React.SetStateAction<T>,
): action is (prevState: T) => T {
return typeof action === 'function';
}

export function useUndoable<T>(
stateTuple: [T, React.Dispatch<React.SetStateAction<T>>],
): Undoable<T> {
const [state, _setState] = stateTuple;
const [undoStack, setUndoStack] = useState<T[]>([]);
const [redoStack, setRedoStack] = useState<T[]>([]);
const stateRef = useRef<T>(state);
const undoStackRef = useRef<T[]>([]);
const redoStackRef = useRef<T[]>([]);
const canUndo = undoStack.length > 0;
const canRedo = redoStack.length > 0;

stateRef.current = state;
undoStackRef.current = undoStack;
redoStackRef.current = redoStack;

const setState: React.Dispatch<React.SetStateAction<T>> = useCallback(
(action) => {
const state = stateRef.current;
setUndoStack((undoStack) => [...undoStack, state]);
setRedoStack([]);
_setState((state) => (isStateUpdater(action) ? action(state) : action));
},
[_setState],
);

const undo = useCallback(() => {
const state = stateRef.current;
const undoStack = undoStackRef.current;
if (undoStack.length < 1) return;
_setState(undoStack[undoStack.length - 1]);
setUndoStack((undoStack) => undoStack.slice(0, -1));
setRedoStack((redoStack) => [...redoStack, state]);
}, [_setState]);

const redo = useCallback(() => {
const state = stateRef.current;
const redoStack = redoStackRef.current;
if (redoStack.length < 1) return;
_setState(redoStack[redoStack.length - 1]);
setRedoStack((redoStack) => redoStack.slice(0, -1));
setUndoStack((undoStack) => [...undoStack, state]);
}, [_setState]);

return useMemo(
() => ({
state,
setState,
undo,
redo,
canUndo,
canRedo,
}),
[state, setState, undo, redo, canUndo, canRedo],
);
}

0 comments on commit 50ea6e1

Please sign in to comment.