Custom operations are not added to undo stack
OS: Windows 11 · Device: Desktop or Laptop Any · Browser: Chrome 120.0 · Keyboard: US
Open case →Scenario
The undo/redo stack in contenteditable elements behaves inconsistently across browsers. Programmatic DOM changes may or may not be added to the undo stack, and the stack may be cleared unexpectedly. Custom undo/redo implementation is often necessary.
The undo/redo stack in contenteditable elements behaves inconsistently across browsers. Programmatic DOM changes may or may not be added to the undo stack, and the stack may be cleared unexpectedly. Custom undo/redo implementation is often necessary.
Implement custom undo/redo:
class UndoRedoManager {
constructor(element) {
this.element = element;
this.undoStack = [];
this.redoStack = [];
this.maxStackSize = 50;
}
saveState() {
const state = {
html: this.element.innerHTML,
selection: this.saveSelection()
};
this.undoStack.push(state);
if (this.undoStack.length > this.maxStackSize) {
this.undoStack.shift();
}
this.redoStack = []; // Clear redo stack on new action
}
undo() {
if (this.undoStack.length === 0) return;
const currentState = {
html: this.element.innerHTML,
selection: this.saveSelection()
};
this.redoStack.push(currentState);
const previousState = this.undoStack.pop();
this.restoreState(previousState);
}
redo() {
if (this.redoStack.length === 0) return;
const currentState = {
html: this.element.innerHTML,
selection: this.saveSelection()
};
this.undoStack.push(currentState);
const nextState = this.redoStack.pop();
this.restoreState(nextState);
}
restoreState(state) {
this.element.innerHTML = state.html;
this.restoreSelection(state.selection);
}
saveSelection() {
const selection = window.getSelection();
if (selection.rangeCount === 0) return null;
const range = selection.getRangeAt(0);
return {
startContainer: range.startContainer,
startOffset: range.startOffset,
endContainer: range.endContainer,
endOffset: range.endOffset
};
}
restoreSelection(saved) {
if (!saved) return;
try {
const range = document.createRange();
range.setStart(saved.startContainer, saved.startOffset);
range.setEnd(saved.endContainer, saved.endOffset);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
} catch (e) {
// Selection invalid, ignore
}
}
}
const undoRedo = new UndoRedoManager(element);
element.addEventListener('input', () => {
undoRedo.saveState();
});
// Handle Ctrl+Z and Ctrl+Y
element.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey) {
if (e.key === 'z' && !e.shiftKey) {
e.preventDefault();
undoRedo.undo();
} else if ((e.key === 'y') || (e.key === 'z' && e.shiftKey)) {
e.preventDefault();
undoRedo.redo();
}
}
});
Visual view of how this scenario connects to its concrete cases and environments. Nodes can be dragged and clicked.
Each row is a concrete case for this scenario, with a dedicated document and playground.
| Case | OS | Device | Browser | Keyboard | Status |
|---|---|---|---|---|---|
| ce-0113-undo-redo-custom-ops | Windows 11 | Desktop or Laptop Any | Chrome 120.0 | US | draft |
| ce-0129-undo-redo-stack-cleared | Windows 11 | Desktop or Laptop Any | Chrome 120.0 | US | draft |
| ce-0141-undo-redo-multiple-ops | Windows 11 | Desktop or Laptop Any | Chrome 120.0 | US | draft |
| ce-0150-undo-redo-custom-formatting | Windows 11 | Desktop or Laptop Any | Chrome 120.0 | US | draft |
| ce-0173-undo-redo-custom-text-insert | Windows 11 | Desktop or Laptop Any | Chrome 120.0 | US | draft |
This matrix shows which browser and OS combinations have documented cases for this scenario. Click on a cell to view the specific case.
| Browser | Windows |
|---|---|
| Chrome |
Open a case to see the detailed description and its dedicated playground.
OS: Windows 11 · Device: Desktop or Laptop Any · Browser: Chrome 120.0 · Keyboard: US
Open case →OS: Windows 11 · Device: Desktop or Laptop Any · Browser: Chrome 120.0 · Keyboard: US
Open case →OS: Windows 11 · Device: Desktop or Laptop Any · Browser: Chrome 120.0 · Keyboard: US
Open case →OS: Windows 11 · Device: Desktop or Laptop Any · Browser: Chrome 120.0 · Keyboard: US
Open case →OS: Windows 11 · Device: Desktop or Laptop Any · Browser: Chrome 120.0 · Keyboard: US
Open case →Other scenarios that share similar tags or category.
In Firefox, programmatic DOM changes during typing (auto-formatting, spellcheck fixes, framework reconciliation) can desynchronize the internal undo stack. Undo/redo may jump to wrong snapshots or truncate history.
Pressing Undo or Redo while IME composition is active can cancel composition, leave partial syllables, or corrupt the undo stack. Behavior differs by browser and by whether the editor uses native undo or a custom history layer.
The undo and redo functionality (Ctrl+Z / Ctrl+Y or Cmd+Z / Cmd+Shift+Z) behaves differently across browsers. Some browsers undo individual keystrokes, while others undo larger operations. The undo stack may also be cleared unexpectedly.
Programmatic inserts or execCommand during typing can split undo transactions or clear the stack—browser-specific rules differ from custom editor history.
Have questions, suggestions, or want to share your experience? Join the discussion below.