From b970be11ee895c34e019231022e9e5244e4aec2f Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sun, 20 Sep 2026 01:32:44 +0000 Subject: Stop per-store add forms jumping to the bottom of the page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inline "Add to ..." forms submitted with a bare hx-swap="innerHTML" against #shopping-items-list. Those forms live *inside* that target, so the swap destroys the form that submitted it and the page ends up scrolled to the bottom rather than staying on the store being added to. The quick-add form at the top never had this because it sits outside the target and already carried an explicit scroll: modifier. Each store section now has a stable id and its inline form scrolls back to it, so adding to a store keeps that store in view. Adds a `slug` template function for the id: store names are user data ("Trader Joe's", "Hardware Store") going straight into element ids, so anything outside [a-z0-9-] is collapsed to a dash and an empty result becomes "unnamed" rather than an invalid, shared anchor. Tests render the real template with the real func map, and assert every inline add form has a scroll target — the bug is trivially reintroduced by copying a section. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B --- internal/handlers/handlers.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) (limited to 'internal/handlers/handlers.go') diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index e70650d..c5df00d 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -1251,6 +1251,34 @@ func templateFuncMap() template.FuncMap { return template.FuncMap{ "subtract": func(a, b int) int { return a - b }, "add": func(a, b int) int { return a + b }, + // slug turns a free-text name into an id-safe fragment, so a template + // can give a section a stable anchor and target it by selector. Store + // names are user data ("Trader Joe's", "Hardware Store") and go + // straight into element ids, so anything outside [a-z0-9-] has to go. + "slug": func(s string) string { + var b strings.Builder + lastDash := true // leading dashes are suppressed + for _, r := range strings.ToLower(s) { + switch { + case (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'): + b.WriteRune(r) + lastDash = false + default: + // Collapse any run of separators/punctuation into one dash. + if !lastDash { + b.WriteByte('-') + lastDash = true + } + } + } + out := strings.Trim(b.String(), "-") + if out == "" { + // An empty id would make every such section share the same + // (invalid) anchor, so give it something addressable. + return "unnamed" + } + return out + }, // multiDayLabel returns the "(starts/ends HH:MM)" suffix for a // multi-day calendar event's row on the day it's rendered, or "" // for a normal item or a "spans" (pass-through) day, which shows -- cgit v1.2.3