From 65480fd892b33019b7ef679bb72e769805bff121 Mon Sep 17 00:00:00 2001 From: grabowski Date: Mon, 20 Jul 2026 10:46:13 +0700 Subject: [PATCH] Auto-format token and join-code inputs with dashes while typing Shared wireCodeMask helper: uppercase, dash every 4 chars, raw-length cap, caret preserved, backspace over a dash deletes the char before it. --- public/js/dom.js | 22 ++++++++++++++++++++++ public/js/views/auth.js | 4 +++- public/js/views/trips.js | 4 +++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/public/js/dom.js b/public/js/dom.js index ce9e573..dd4e883 100644 --- a/public/js/dom.js +++ b/public/js/dom.js @@ -1,4 +1,5 @@ // Tiny DOM helpers — no framework, just ergonomic element creation. +import { groupCode, normalizeCode } from './format.js'; // el('div', { class: 'x', onClick: fn }, child, child, ...) // Attrs: class, dataset(obj), style(obj), html(innerHTML), on(fn), @@ -75,6 +76,27 @@ export function emptyState(title, subtitle, action) { ); } +// Live code mask for token/join-code inputs: uppercase + a dash after every 4 +// characters as you type, capped at rawMax raw characters, keeping the caret +// anchored to the same raw character. Backspacing onto a dash deletes the +// character before it (otherwise the dash would just reappear and the key +// would look dead). +export function wireCodeMask(input, rawMax) { + input.addEventListener('keydown', (e) => { + const pos = input.selectionStart; + if (e.key === 'Backspace' && pos === input.selectionEnd && input.value[pos - 1] === '-') { + input.setSelectionRange(pos - 1, pos - 1); + } + }); + input.addEventListener('input', () => { + const rawBefore = Math.min(rawMax, normalizeCode(input.value.slice(0, input.selectionStart)).length); + const raw = normalizeCode(input.value).slice(0, rawMax); + input.value = groupCode(raw, 4); + const caret = rawBefore + (rawBefore > 0 ? Math.floor((rawBefore - 1) / 4) : 0); + input.setSelectionRange(caret, caret); + }); +} + let toastHost = null; export function toast(message, type = 'error', ms = 4200) { if (!toastHost) { diff --git a/public/js/views/auth.js b/public/js/views/auth.js index 193085c..433c714 100644 --- a/public/js/views/auth.js +++ b/public/js/views/auth.js @@ -2,7 +2,7 @@ // Two panels: create a new account (one-time token reveal) or log in with an // existing account number. import { api } from '../api.js'; -import { el, mount, toast } from '../dom.js'; +import { el, mount, toast, wireCodeMask } from '../dom.js'; import { groupCode, normalizeCode } from '../format.js'; export function renderAuth(container, ctx) { @@ -123,8 +123,10 @@ export function renderAuth(container, ctx) { autocapitalize: 'characters', spellcheck: 'false', placeholder: 'XXXX-XXXX-XXXX-XXXX', + maxlength: '19', 'aria-label': 'Account number', }); + wireCodeMask(input, 16); const submitBtn = el('button', { class: 'btn btn-primary btn-block', type: 'submit' }, 'Log in'); async function onSubmit(e) { diff --git a/public/js/views/trips.js b/public/js/views/trips.js index cbe035e..25d35cc 100644 --- a/public/js/views/trips.js +++ b/public/js/views/trips.js @@ -1,6 +1,6 @@ // Trip list dashboard: cards, a "new trip" form, and a "join a trip" form. import { api } from '../api.js'; -import { el, mount, clear, loading, errorBox, emptyState, toast } from '../dom.js'; +import { el, mount, clear, loading, errorBox, emptyState, toast, wireCodeMask } from '../dom.js'; import { formatRange, ymd, parseYMD, pluralize, normalizeCode } from '../format.js'; import { enableTripReorder } from './dragdrop.js'; @@ -201,8 +201,10 @@ export function renderTrips(container, ctx) { autocapitalize: 'characters', spellcheck: 'false', placeholder: 'XXXX-XXXX', + maxlength: '9', 'aria-label': 'Join code', }); + wireCodeMask(codeInput, 8); const errorEl = el('p', { class: 'form-error' }); const submitBtn = el('button', { class: 'btn btn-primary', type: 'submit' }, 'Join trip');