@@ -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
0 commit comments