Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.\
Expand Down
56 changes: 56 additions & 0 deletions lib/ContentEditable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ const ContentEditable: React.FC<ContentEditableProps> = ({
}) => {
const [content, setContent] = useState("")
const divRef = useRef<HTMLDivElement | null>(null)
const undoStack = useRef<string[]>([])
const redoStack = useRef<string[]>([])

useEffect(() => {
if (updatedContent !== null && updatedContent !== undefined) {
Expand All @@ -70,6 +72,60 @@ const ContentEditable: React.FC<ContentEditableProps> = ({
}
}, [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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down