diff --git a/README.md b/README.md index 6feaa14..19c5f77 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,11 @@ export default App | onFocus | ✔️ | `(e) => void` | Method that emits the focus event | | onBlur | ✔️ | `(e) => void` | Method that emits the blur event | +### Keyboard shortcuts + +- Undo: `Ctrl + Z` +- Redo: `Ctrl + Y` / `Ctrl + Shift + Z` + ## Contribution If you have a suggestion that would make this component better feel free to fork the project and open a pull request or create an issue for any idea or bug you find.\ diff --git a/lib/ContentEditable.tsx b/lib/ContentEditable.tsx index 2d77172..6e9e6a6 100644 --- a/lib/ContentEditable.tsx +++ b/lib/ContentEditable.tsx @@ -46,6 +46,8 @@ const ContentEditable: React.FC = ({ }) => { const [content, setContent] = useState("") const divRef = useRef(null) + const undoStack = useRef([]) + const redoStack = useRef([]) useEffect(() => { if (updatedContent !== null && updatedContent !== undefined) { @@ -70,6 +72,60 @@ const ContentEditable: React.FC = ({ } }, [autoFocus]) + useEffect(() => { + undoStack.current.push(content) + }, [content]) + + /** + * Handles undo and redo keyboard shortcuts for content editable div. + * - Undo with `Ctrl + Z` + * - Redo with `Ctrl + Y` or `Ctrl + Shift + Z` + * + * Prevents the default action. + * Pops the last content from the undo/redo stack and pushes it to the redo/undo stack. + * Sets the content to the previous state and updates + * the content of the div and sets the caret at the end. + */ + const handleUndoRedo = useCallback( + (e: KeyboardEvent) => { + // Undo + if (e.ctrlKey && e.key === "z") { + e.preventDefault() + if (undoStack.current.length > 1) { + redoStack.current.push(undoStack.current.pop() as string) + const previousContent = + undoStack.current[undoStack.current.length - 1] + setContent(previousContent) + if (divRef.current) { + divRef.current.innerText = previousContent + setCaretAtTheEnd(divRef.current) + } + } + // Redo + } else if ( + (e.ctrlKey && e.key === "y") || + (e.ctrlKey && e.shiftKey && e.key === "Z") + ) { + e.preventDefault() + if (redoStack.current.length > 0) { + const nextContent = redoStack.current.pop() as string + undoStack.current.push(nextContent) + setContent(nextContent) + if (divRef.current) { + divRef.current.innerText = nextContent + setCaretAtTheEnd(divRef.current) + } + } + } + }, + [setContent] + ) + + useEffect(() => { + document.addEventListener("keydown", handleUndoRedo) + return () => document.removeEventListener("keydown", handleUndoRedo) + }, [handleUndoRedo]) + /** * Checks if the caret is on the last line of a contenteditable element * @param element - The HTMLDivElement to check diff --git a/package.json b/package.json index af5762e..6b0cf8f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-basic-contenteditable", "description": "React contenteditable component. Super-customizable!", - "version": "1.0.5", + "version": "1.0.6", "type": "module", "main": "dist/main.js", "types": "dist/main.d.ts",