Ctrl+Z After Editing a Property Field Does Nothing: The Before-State Timing Bug
TLDR
Pressing Ctrl+Z after editing a property input (x, y, width, height) restores the diagram to the same state, effectively doing nothing. The root cause is that the undo before-state is captured at change or blur, after the input value has already changed. The fix is capturing the before-state at focus, before the user touches the field.
Symptom
The undo history shows an entry for "Property Edit" after the user changes an element's X position from 100 to 250. Pressing Ctrl+Z does not move the element back to 100. The element stays at 250. Pressing Ctrl+Z a second time undoes the previous operation (a move, a draw), skipping the property edit entirely.
The undo entry exists. Restoring it does nothing because the before and after states it contains are identical.
Why It Happens
HTML inputs update their DOM value as the user types. By the time blur or change fires, the field already shows the new value. If you call captureState() inside the change handler before applying the change to your model, you are reading the new value from the DOM, not the old one.
// wrong: before is captured after the DOM already shows the new value
$('#prop-x').on('change', function() {
const before = captureState(); // reads this.value = "250" (new)
applyToModel(this.value);
const after = captureState(); // also reads "250"
pushHistory('Property Edit', before, after); // before === after
});
The before and after snapshots are identical. Restoring "before" restores the state to itself.
The Fix
// correct: capture before-state on focus, before any typing
let propBeforeState = null;
$(document).on('focus', '#prop-x, #prop-y, #prop-w, #prop-h', function() { propBeforeState = captureState(); // reads "100" (original) });
$(document).on('change', '#prop-x, #prop-y, #prop-w, #prop-h', function() { const before = propBeforeState || captureState(); propBeforeState = null; applyToModel(this.value); pushHistory('Property Edit', before, captureState()); });
The before-state is captured at focus, before the user has typed anything. The input still shows "100". The after-state is captured after the model is updated. These are different. Undo correctly restores the original position.
How to Prevent It
For any UI control that modifies state incrementally, ask: at what moment does the state change begin? That moment is where the before-snapshot belongs.
- Input fields:
focus - Drag handles:
pointerdown - Sliders:
mousedown(first event, before the slider value changes) - Color pickers: the opener click, not the color select
before !== after by comparing a known field. If they are equal, the before-state was captured too late.
The Generalizable Lesson
In a snapshot undo system, "before" means the state at the start of the user's intent, not the state at the moment you write the history entry. These are the same for one-shot operations (click to delete) and different for incremental operations (type a new value). Treat them differently.