Files
grabowski 65480fd892 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.
2026-07-20 10:46:13 +07:00

114 lines
3.9 KiB
JavaScript

// 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<Event>(fn),
// boolean true -> present attribute, anything else -> setAttribute.
export function el(tag, attrs = {}, ...children) {
const node = document.createElement(tag);
for (const [key, val] of Object.entries(attrs || {})) {
if (val == null || val === false) continue;
if (key === 'class') node.className = val;
else if (key === 'dataset') Object.assign(node.dataset, val);
else if (key === 'style' && typeof val === 'object') {
// Custom properties (--x) must go through setProperty; plain assignment
// silently drops them.
for (const [prop, pv] of Object.entries(val)) {
if (pv == null) continue;
if (prop.startsWith('--')) node.style.setProperty(prop, pv);
else node.style[prop] = pv;
}
}
else if (key === 'html') node.innerHTML = val;
else if (key === 'value') node.value = val;
else if (key.startsWith('on') && typeof val === 'function')
node.addEventListener(key.slice(2).toLowerCase(), val);
else if (val === true) node.setAttribute(key, '');
else node.setAttribute(key, val);
}
appendAll(node, children);
return node;
}
function appendAll(node, children) {
for (const child of children.flat(Infinity)) {
if (child == null || child === false || child === true) continue;
node.appendChild(
typeof child === 'string' || typeof child === 'number'
? document.createTextNode(String(child))
: child,
);
}
}
export function clear(node) {
while (node.firstChild) node.removeChild(node.firstChild);
return node;
}
export function mount(container, ...children) {
clear(container);
appendAll(container, children);
return container;
}
export function loading(text = 'Loading…') {
return el('div', { class: 'loading' }, el('span', { class: 'spinner' }), text);
}
export function errorBox(message, onRetry) {
return el(
'div',
{ class: 'error-box' },
el('p', {}, message || 'Something went wrong.'),
onRetry ? el('button', { class: 'btn', onClick: onRetry }, 'Retry') : null,
);
}
export function emptyState(title, subtitle, action) {
return el(
'div',
{ class: 'empty-state' },
el('div', { class: 'empty-icon' }, '🧭'),
el('h3', {}, title),
subtitle ? el('p', {}, subtitle) : null,
action || null,
);
}
// 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) {
toastHost = el('div', { class: 'toast-host' });
document.body.appendChild(toastHost);
}
const node = el('div', { class: `toast toast-${type}` }, message);
toastHost.appendChild(node);
requestAnimationFrame(() => node.classList.add('show'));
setTimeout(() => {
node.classList.remove('show');
setTimeout(() => node.remove(), 300);
}, ms);
}