Skip to content

Commit 4d30195

Browse files
authored
Add undo/redo handlers
2 parents fb7d11f + cac45a3 commit 4d30195

File tree

3 files changed

+62
-1
lines changed

3 files changed

+62
-1
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ export default App
6969
| onFocus | ✔️ | `(e) => void` | Method that emits the focus event |
7070
| onBlur | ✔️ | `(e) => void` | Method that emits the blur event |
7171

72+
### Keyboard shortcuts
73+
74+
- Undo: `Ctrl + Z`
75+
- Redo: `Ctrl + Y` / `Ctrl + Shift + Z`
76+
7277
## Contribution
7378

7479
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.\

lib/ContentEditable.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ const ContentEditable: React.FC<ContentEditableProps> = ({
4646
}) => {
4747
const [content, setContent] = useState("")
4848
const divRef = useRef<HTMLDivElement | null>(null)
49+
const undoStack = useRef<string[]>([])
50+
const redoStack = useRef<string[]>([])
4951

5052
useEffect(() => {
5153
if (updatedContent !== null && updatedContent !== undefined) {
@@ -70,6 +72,60 @@ const ContentEditable: React.FC<ContentEditableProps> = ({
7072
}
7173
}, [autoFocus])
7274

75+
useEffect(() => {
76+
undoStack.current.push(content)
77+
}, [content])
78+
79+
/**
80+
* Handles undo and redo keyboard shortcuts for content editable div.
81+
* - Undo with `Ctrl + Z`
82+
* - Redo with `Ctrl + Y` or `Ctrl + Shift + Z`
83+
*
84+
* Prevents the default action.
85+
* Pops the last content from the undo/redo stack and pushes it to the redo/undo stack.
86+
* Sets the content to the previous state and updates
87+
* the content of the div and sets the caret at the end.
88+
*/
89+
const handleUndoRedo = useCallback(
90+
(e: KeyboardEvent) => {
91+
// Undo
92+
if (e.ctrlKey && e.key === "z") {
93+
e.preventDefault()
94+
if (undoStack.current.length > 1) {
95+
redoStack.current.push(undoStack.current.pop() as string)
96+
const previousContent =
97+
undoStack.current[undoStack.current.length - 1]
98+
setContent(previousContent)
99+
if (divRef.current) {
100+
divRef.current.innerText = previousContent
101+
setCaretAtTheEnd(divRef.current)
102+
}
103+
}
104+
// Redo
105+
} else if (
106+
(e.ctrlKey && e.key === "y") ||
107+
(e.ctrlKey && e.shiftKey && e.key === "Z")
108+
) {
109+
e.preventDefault()
110+
if (redoStack.current.length > 0) {
111+
const nextContent = redoStack.current.pop() as string
112+
undoStack.current.push(nextContent)
113+
setContent(nextContent)
114+
if (divRef.current) {
115+
divRef.current.innerText = nextContent
116+
setCaretAtTheEnd(divRef.current)
117+
}
118+
}
119+
}
120+
},
121+
[setContent]
122+
)
123+
124+
useEffect(() => {
125+
document.addEventListener("keydown", handleUndoRedo)
126+
return () => document.removeEventListener("keydown", handleUndoRedo)
127+
}, [handleUndoRedo])
128+
73129
/**
74130
* Checks if the caret is on the last line of a contenteditable element
75131
* @param element - The HTMLDivElement to check

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "react-basic-contenteditable",
33
"description": "React contenteditable component. Super-customizable!",
4-
"version": "1.0.5",
4+
"version": "1.0.6",
55
"type": "module",
66
"main": "dist/main.js",
77
"types": "dist/main.d.ts",

0 commit comments

Comments
 (0)