Whole-form htmx + Alpine morph for transaction edit

Re-render the entire #wizard-form on each field edit and swap with
hx-swap="morph" so the focused input keeps focus/caret/value while typing.

- Field-level routes return the full form and target #wizard-form
- Key state-owning wrappers (account rows, simple-mode wrapper, vendor
  typeahead) so server-driven value changes re-init across the morph
- Guard tippy/$refs access in typeahead against stale post-morph state
- Round-trip simple/advanced mode via step-params[mode]
- Add e2e/transaction-edit-morph.spec.ts covering focus/caret preservation,
  vendor->account population, and repeated vendor changes
- Seed a second vendor/account for test isolation

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 07:40:30 -07:00
parent b6649a3d1d
commit cdb6bb6fe3
8 changed files with 681 additions and 185 deletions

View File

@@ -416,4 +416,64 @@ htmx.onLoad(function(content) {
console.error('Failed to copy text to clipboard:', err);
}
}
/*
(function() {
var lastFocusedSelector = null;
var lastCursorPosition = null;
document.addEventListener('htmx:beforeSwap', function(evt) {
var active = document.activeElement;
if (active && active !== document.body) {
// Build a selector to find this element after swap
if (active.id) {
lastFocusedSelector = '#' + active.id;
} else if (active.name) {
lastFocusedSelector = '[name="' + active.name + '"]';
} else {
lastFocusedSelector = null;
}
// Save cursor position for text inputs. selectionStart is null on
// inputs that don't support selection (number, date, select, etc.),
// and calling setSelectionRange on those throws, so only capture it
// when it's an actual numeric caret position.
if (typeof active.selectionStart === 'number') {
lastCursorPosition = {
start: active.selectionStart,
end: active.selectionEnd,
direction: active.selectionDirection
};
} else {
lastCursorPosition = null;
}
}
});
document.addEventListener('htmx:afterSwap', function(evt) {
if (lastFocusedSelector) {
setTimeout(function() {
var el = document.querySelector(lastFocusedSelector);
// If morph already kept focus on the right element there's nothing
// to do; only restore when focus was actually lost by the swap.
if (el && el.focus && document.activeElement !== el) {
el.focus();
if (lastCursorPosition && el.setSelectionRange) {
try {
el.setSelectionRange(
lastCursorPosition.start,
lastCursorPosition.end,
lastCursorPosition.direction
);
} catch (e) { }
}
}
lastFocusedSelector = null;
lastCursorPosition = null;
}, 10);
}
});
})();
*/