71 lines
2.4 KiB
JavaScript
71 lines
2.4 KiB
JavaScript
// Tiny DOM helpers: element builder, toasts and a confirm dialog.
|
|
|
|
/** Build an element. Props: class, on<Event> handlers, booleans, attributes. */
|
|
export function h(tag, props, ...kids) {
|
|
const el = document.createElement(tag);
|
|
for (const [k, v] of Object.entries(props ?? {})) {
|
|
if (v == null || v === false) continue;
|
|
if (k === 'class') el.className = v;
|
|
else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2).toLowerCase(), v);
|
|
else if (v === true) el.setAttribute(k, '');
|
|
else el.setAttribute(k, v);
|
|
}
|
|
el.append(...kids.flat(Infinity).filter((c) => c != null && c !== false));
|
|
return el;
|
|
}
|
|
|
|
export function clear(el) {
|
|
el.replaceChildren();
|
|
return el;
|
|
}
|
|
|
|
export function toast(message, kind = 'info') {
|
|
const host = document.getElementById('toasts');
|
|
const t = h('div', { class: `toast ${kind}`, role: kind === 'err' ? 'alert' : null }, message);
|
|
host.append(t);
|
|
setTimeout(() => {
|
|
t.classList.add('out');
|
|
setTimeout(() => t.remove(), 400);
|
|
}, kind === 'err' ? 8000 : 4500);
|
|
}
|
|
|
|
/** Resolve true if the user confirms. */
|
|
export function confirmDialog({ title, body, confirmText = 'CONFIRM', cancelText = 'ABORT', danger = false }) {
|
|
const dlg = document.getElementById('dialog');
|
|
return new Promise((resolve) => {
|
|
const finish = (v) => {
|
|
dlg.close();
|
|
resolve(v);
|
|
};
|
|
clear(dlg).append(
|
|
h('div', { class: `panel dialog ${danger ? 'danger' : ''}` },
|
|
h('div', { class: 'panel-title' }, title),
|
|
h('div', { class: 'panel-body' }, ...[].concat(body).map((p) => h('p', null, p))),
|
|
h('div', { class: 'actions' },
|
|
h('button', { class: 'btn', type: 'button', onclick: () => finish(false) }, cancelText),
|
|
h('button', { class: `btn ${danger ? 'danger' : 'primary'}`, type: 'button', onclick: () => finish(true) }, confirmText),
|
|
),
|
|
),
|
|
);
|
|
dlg.oncancel = () => resolve(false);
|
|
dlg.showModal();
|
|
});
|
|
}
|
|
|
|
export function download(filename, bytes, type = 'application/octet-stream') {
|
|
const url = URL.createObjectURL(new Blob([bytes], { type }));
|
|
const a = h('a', { href: url, download: filename });
|
|
document.body.append(a);
|
|
a.click();
|
|
a.remove();
|
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
}
|
|
|
|
export function pad(n, width, ch = '0') {
|
|
return String(n).padStart(width, ch);
|
|
}
|
|
|
|
export function plural(n, one, many = `${one}S`) {
|
|
return `${n} ${n === 1 ? one : many}`;
|
|
}
|