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.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
// Tiny DOM helpers — no framework, just ergonomic element creation.
|
// Tiny DOM helpers — no framework, just ergonomic element creation.
|
||||||
|
import { groupCode, normalizeCode } from './format.js';
|
||||||
|
|
||||||
// el('div', { class: 'x', onClick: fn }, child, child, ...)
|
// el('div', { class: 'x', onClick: fn }, child, child, ...)
|
||||||
// Attrs: class, dataset(obj), style(obj), html(innerHTML), on<Event>(fn),
|
// Attrs: class, dataset(obj), style(obj), html(innerHTML), on<Event>(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;
|
let toastHost = null;
|
||||||
export function toast(message, type = 'error', ms = 4200) {
|
export function toast(message, type = 'error', ms = 4200) {
|
||||||
if (!toastHost) {
|
if (!toastHost) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Two panels: create a new account (one-time token reveal) or log in with an
|
// Two panels: create a new account (one-time token reveal) or log in with an
|
||||||
// existing account number.
|
// existing account number.
|
||||||
import { api } from '../api.js';
|
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';
|
import { groupCode, normalizeCode } from '../format.js';
|
||||||
|
|
||||||
export function renderAuth(container, ctx) {
|
export function renderAuth(container, ctx) {
|
||||||
@@ -123,8 +123,10 @@ export function renderAuth(container, ctx) {
|
|||||||
autocapitalize: 'characters',
|
autocapitalize: 'characters',
|
||||||
spellcheck: 'false',
|
spellcheck: 'false',
|
||||||
placeholder: 'XXXX-XXXX-XXXX-XXXX',
|
placeholder: 'XXXX-XXXX-XXXX-XXXX',
|
||||||
|
maxlength: '19',
|
||||||
'aria-label': 'Account number',
|
'aria-label': 'Account number',
|
||||||
});
|
});
|
||||||
|
wireCodeMask(input, 16);
|
||||||
const submitBtn = el('button', { class: 'btn btn-primary btn-block', type: 'submit' }, 'Log in');
|
const submitBtn = el('button', { class: 'btn btn-primary btn-block', type: 'submit' }, 'Log in');
|
||||||
|
|
||||||
async function onSubmit(e) {
|
async function onSubmit(e) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Trip list dashboard: cards, a "new trip" form, and a "join a trip" form.
|
// Trip list dashboard: cards, a "new trip" form, and a "join a trip" form.
|
||||||
import { api } from '../api.js';
|
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 { formatRange, ymd, parseYMD, pluralize, normalizeCode } from '../format.js';
|
||||||
import { enableTripReorder } from './dragdrop.js';
|
import { enableTripReorder } from './dragdrop.js';
|
||||||
|
|
||||||
@@ -201,8 +201,10 @@ export function renderTrips(container, ctx) {
|
|||||||
autocapitalize: 'characters',
|
autocapitalize: 'characters',
|
||||||
spellcheck: 'false',
|
spellcheck: 'false',
|
||||||
placeholder: 'XXXX-XXXX',
|
placeholder: 'XXXX-XXXX',
|
||||||
|
maxlength: '9',
|
||||||
'aria-label': 'Join code',
|
'aria-label': 'Join code',
|
||||||
});
|
});
|
||||||
|
wireCodeMask(codeInput, 8);
|
||||||
const errorEl = el('p', { class: 'form-error' });
|
const errorEl = el('p', { class: 'form-error' });
|
||||||
const submitBtn = el('button', { class: 'btn btn-primary', type: 'submit' }, 'Join trip');
|
const submitBtn = el('button', { class: 'btn btn-primary', type: 'submit' }, 'Join trip');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user