Files
2026-10-29 11:57:29 +01:00

304 lines
13 KiB
JavaScript

// Generated, do not edit. sha256:26856efc0d14b9a73e26fd078294b6d0c3f7eebff4e23cc52adf3839ca9c9ebe
import { esc } from './escape.js';
const CLOSE = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
const DEFAULT_LABELS = Object.freeze({
ok: 'OK', cancel: 'Cancel', close: 'Close', next: 'Next', back: 'Back', finish: 'Finish', skip: 'Skip',
});
let labels = { ...DEFAULT_LABELS };
let closeIcon = CLOSE;
let onDetail = null;
export function configureDialog({ labels: l = null, closeIcon: icon = null, onDetail: hook } = {}) {
labels = { ...DEFAULT_LABELS, ...(l || {}) };
closeIcon = icon || CLOSE;
onDetail = typeof hook === 'function' ? hook : null;
}
let overlayEl = null;
let activeResolve = null;
let overlayIsDetail = false;
export function isDialogOpen(el = null) {
if (topEl) return true;
if (!overlayEl) return false;
return !(el && overlayEl.contains(el));
}
let prevOverflow = null;
let prevPadding = null;
function lockPage() {
if (prevOverflow !== null) return;
const el = document.documentElement;
const gutter = window.innerWidth - el.clientWidth;
prevOverflow = el.style.overflow;
prevPadding = el.style.paddingRight;
el.style.overflow = 'hidden';
if (gutter > 0) el.style.paddingRight = `${gutter}px`;
}
function unlockPage() {
if (prevOverflow === null) return;
const el = document.documentElement;
el.style.overflow = prevOverflow;
el.style.paddingRight = prevPadding;
prevOverflow = null;
prevPadding = null;
}
function settle(result) {
if (topEl) settleTop(false);
document.removeEventListener('keydown', onKey, true);
if (overlayEl) { overlayEl.remove(); overlayEl = null; }
overlayIsDetail = false;
unlockPage();
const r = activeResolve; activeResolve = null;
if (r) r(result);
}
function onKey(e) {
if (!overlayEl || topEl) return;
if (overlayIsDetail && e.target && typeof e.target.matches === 'function' && overlayEl.contains(e.target)
&& e.target.matches('input, select, textarea')) return;
if (e.key === 'Escape') { e.preventDefault(); settle(false); }
}
let topEl = null;
let topResolve = null;
let topReturn = null;
function settleTop(result) {
document.removeEventListener('keydown', onKeyTop, true);
if (topEl) { topEl.remove(); topEl = null; }
const r = topResolve; topResolve = null;
if (topReturn && topReturn.isConnected) topReturn.focus();
topReturn = null;
if (r) r(result);
}
function onKeyTop(e) {
if (topEl && e.key === 'Escape') { e.preventDefault(); settleTop(false); }
}
export function confirmOver({ title, message, confirmLabel, cancelLabel, danger } = {}) {
if (!overlayEl) return confirm({ title, message, confirmLabel, cancelLabel, danger });
return openTop({ title, bodyHtml: `<div class="dialog-body">${esc(message)}</div>`,
showCancel: true, confirmLabel, cancelLabel, danger });
}
function openTop({ title, bodyHtml, showCancel, confirmLabel, cancelLabel, danger, collect = null }) {
if (topEl) settleTop(false);
return new Promise(resolve => {
topResolve = resolve;
topReturn = document.activeElement;
topEl = document.createElement('div');
topEl.className = 'dialog-overlay dialog-overlay--stacked';
topEl.innerHTML = `
<div class="dialog" role="${collect ? 'dialog' : 'alertdialog'}" aria-modal="true" tabindex="-1">
${title ? `<div class="dialog-title">${esc(title)}</div>` : ''}
${bodyHtml}
<div class="dialog-actions">
${showCancel ? `<button type="button" class="btn btn-ghost btn-md" data-dlg="cancel">${esc(cancelLabel || labels.cancel)}</button>` : ''}
<button type="button" class="btn ${danger ? 'btn-danger' : 'btn-primary'} btn-md" data-dlg="ok">${esc(confirmLabel || labels.ok)}</button>
</div>
</div>`;
const outcome = ok => (collect ? (ok ? collect(topEl) : null) : ok);
topEl.addEventListener('click', e => {
const b = e.target.closest('[data-dlg]');
if (b) settleTop(outcome(b.dataset.dlg === 'ok'));
});
if (collect) {
topEl.addEventListener('keydown', e => {
if (e.key === 'Enter' && e.target.matches('input[data-dlg-field]')) { e.preventDefault(); settleTop(outcome(true)); }
});
}
document.body.appendChild(topEl);
document.addEventListener('keydown', onKeyTop, true);
(topEl.querySelector('[data-dlg-field]') || topEl.querySelector('[data-dlg="ok"]')).focus();
});
}
function open({ title, message, bodyHtml, wide, confirmLabel, cancelLabel, danger, showCancel, showOk = true,
closeCross = false, titleAside = false }) {
if (overlayEl) settle(false);
return new Promise(resolve => {
activeResolve = resolve;
const confirmCls = danger ? 'btn-danger' : 'btn-primary';
overlayEl = document.createElement('div');
overlayEl.className = 'dialog-overlay';
const actions = showCancel || showOk ? `
<div class="dialog-actions">
${showCancel ? `<button type="button" class="btn btn-ghost btn-md" data-dlg="cancel">${esc(cancelLabel || labels.cancel)}</button>` : ''}
${showOk ? `<button type="button" class="btn ${confirmCls} btn-md" data-dlg="ok">${esc(confirmLabel || labels.ok)}</button>` : ''}
</div>` : '';
const body = bodyHtml != null
? `<div class="dialog-body dialog-body--rich">${bodyHtml}</div>`
: `<div class="dialog-body">${esc(message)}</div>`;
const head = title
? `<div class="dialog-title">
<span class="dialog-title-text">${esc(title)}</span>
${titleAside ? '<span class="dialog-title-aside" data-dialog-aside></span>' : ''}
</div>`
: '';
const cross = closeCross
? `<button type="button" class="dialog-close" data-dlg="cancel" aria-label="${esc(labels.close)}"
title="${esc(labels.close)}">${closeIcon}</button>`
: '';
overlayEl.innerHTML = `
<div class="dialog${wide ? ' dialog--wide' : ''}" role="dialog" aria-modal="true" tabindex="-1">
${cross}${head}${body}${actions}
</div>`;
const backdropCloses = !(showOk || showCancel || closeCross);
overlayEl.addEventListener('click', e => {
if (e.target === overlayEl) { if (backdropCloses) settle(false); return; }
const b = e.target.closest('[data-dlg]');
if (b) settle(b.dataset.dlg === 'ok');
});
document.body.appendChild(overlayEl);
lockPage();
document.addEventListener('keydown', onKey, true);
(overlayEl.querySelector('[data-dlg="ok"]') || overlayEl.querySelector('.dialog')).focus();
});
}
export function stepper({ title, steps, state = {}, skipLabel, finishLabel, hero = '' } = {}) {
if (overlayEl) settle(false);
return new Promise(resolve => {
activeResolve = resolve;
const visible = () => steps.filter(s => !(s.skip && s.skip(state)));
let i = 0;
overlayEl = document.createElement('div');
overlayEl.className = 'dialog-overlay dialog-overlay--focus';
overlayEl.innerHTML = `
<div class="dialog dialog--wide dialog-stepper${hero ? ' dialog-stepper--hero' : ''}" role="dialog" aria-modal="true" tabindex="-1">
${hero ? `<div class="stepper-hero" aria-hidden="true">${hero}</div>` : ''}
${title ? `<div class="dialog-title"><span class="dialog-title-text">${esc(title)}</span></div>` : ''}
<div class="dialog-body dialog-body--rich"></div>
<div class="dialog-actions stepper-actions"></div>
</div>`;
const body = overlayEl.querySelector('.dialog-body');
const actions = overlayEl.querySelector('.stepper-actions');
const draw = () => {
const list = visible();
i = Math.min(i, list.length - 1);
const step = list[i];
const last = i === list.length - 1;
const ready = !step.ready || step.ready(state);
body.innerHTML = `${step.title ? `<div class="stepper-title">${esc(step.title)}</div>` : ''}${step.html(state)}`;
actions.innerHTML = `
<button type="button" class="btn btn-ghost btn-md stepper-skip" data-step="skip">${esc(skipLabel || labels.skip)}</button>
${i > 0 ? `<button type="button" class="btn btn-ghost btn-md" data-step="back">${esc(labels.back)}</button>` : ''}
<button type="button" class="btn btn-primary btn-md" data-step="next"${ready ? '' : ' disabled'}>${esc(last ? (finishLabel || labels.finish) : labels.next)}</button>`;
const focus = overlayEl.querySelector('[data-step="next"]:not([disabled])') || overlayEl.querySelector('.dialog');
focus.focus();
};
overlayEl.addEventListener('click', e => {
const set = e.target.closest('[data-step-set]');
if (set) { state[set.dataset.stepSet] = set.dataset.value; draw(); return; }
const toggle = e.target.closest('[data-step-toggle]');
if (toggle) {
const key = toggle.dataset.stepToggle;
const v = toggle.dataset.value;
const list = Array.isArray(state[key]) ? state[key] : [];
state[key] = list.includes(v) ? list.filter(x => x !== v) : [...list, v];
draw();
return;
}
const b = e.target.closest('[data-step]');
if (!b || b.disabled) return;
if (b.dataset.step === 'skip') settle(false);
else if (b.dataset.step === 'back') { i = Math.max(0, i - 1); draw(); }
else if (i === visible().length - 1) settle(state);
else { i += 1; draw(); }
});
document.body.appendChild(overlayEl);
lockPage();
document.addEventListener('keydown', onKey, true);
draw();
});
}
export function confirm({ title, message, confirmLabel, cancelLabel, danger } = {}) {
if (overlayEl && overlayIsDetail) return confirmOver({ title, message, confirmLabel, cancelLabel, danger });
return open({ title, message, confirmLabel, cancelLabel, danger, showCancel: true });
}
export function alert({ title, message, confirmLabel } = {}) {
if (overlayEl && overlayIsDetail) {
return openTop({ title, bodyHtml: `<div class="dialog-body">${esc(message)}</div>`, showCancel: false, confirmLabel });
}
return open({ title, message, confirmLabel, danger: false, showCancel: false });
}
export function notice({ title, message } = {}) {
return open({ title, message, showCancel: false, showOk: false });
}
export function detail({ title, html, size, titleHtml, asideHtml } = {}) {
const done = open({ title, bodyHtml: html ?? '', wide: true, showCancel: false, showOk: false,
closeCross: true, titleAside: true });
overlayIsDetail = true;
const d = overlayEl.querySelector('.dialog');
if (size === 'xl') d.classList.add('dialog--xl');
if (titleHtml) d.querySelector('.dialog-title-text').innerHTML = titleHtml;
if (asideHtml) d.querySelector('[data-dialog-aside]').innerHTML = asideHtml;
if (onDetail) onDetail(d, done);
return done;
}
function fieldsHtml(fields) {
return `<div class="dialog-body">
${fields.map(f => `
<div class="form-group">
<label class="form-label">${esc(f.label)}</label>
${f.type === 'textarea'
? `<textarea class="input" rows="4" data-dlg-field="${esc(f.key)}" placeholder="${esc(f.placeholder ?? '')}" autocomplete="off">${esc(f.value ?? '')}</textarea>`
: f.type === 'select'
? `<select class="input" data-dlg-field="${esc(f.key)}">${(f.options || []).map(o =>
`<option value="${esc(o.value)}"${String(o.value) === String(f.value ?? '') ? ' selected' : ''}>${esc(o.label)}</option>`).join('')}</select>`
: `<input class="input" type="${esc(f.type || 'text')}" data-dlg-field="${esc(f.key)}"
value="${esc(f.value ?? '')}" placeholder="${esc(f.placeholder ?? '')}" autocomplete="off">`}
</div>`).join('')}
</div>`;
}
const fieldValues = host => {
const out = {};
host.querySelectorAll('[data-dlg-field]').forEach(i => { out[i.dataset.dlgField] = i.value; });
return out;
};
export function form({ title, fields = [], confirmLabel, cancelLabel } = {}) {
if (overlayEl && overlayIsDetail) {
return openTop({ title, bodyHtml: fieldsHtml(fields), showCancel: true, confirmLabel, cancelLabel, collect: fieldValues });
}
if (overlayEl) settle(false);
return new Promise(resolve => {
activeResolve = resolve;
overlayEl = document.createElement('div');
overlayEl.className = 'dialog-overlay';
overlayEl.innerHTML = `
<div class="dialog" role="dialog" aria-modal="true">
${title ? `<div class="dialog-title">${esc(title)}</div>` : ''}
${fieldsHtml(fields)}
<div class="dialog-actions">
<button type="button" class="btn btn-ghost btn-md" data-dlg="cancel">${esc(cancelLabel || labels.cancel)}</button>
<button type="button" class="btn btn-primary btn-md" data-dlg="ok">${esc(confirmLabel || labels.ok)}</button>
</div>
</div>`;
const values = () => fieldValues(overlayEl);
overlayEl.addEventListener('click', e => {
if (e.target === overlayEl) return;
const b = e.target.closest('[data-dlg]');
if (b) settle(b.dataset.dlg === 'ok' ? values() : null);
});
overlayEl.addEventListener('keydown', e => {
if (e.key === 'Enter' && e.target.matches('input[data-dlg-field]')) { e.preventDefault(); settle(values()); }
});
document.body.appendChild(overlayEl);
lockPage();
document.addEventListener('keydown', onKey, true);
const first = overlayEl.querySelector('[data-dlg-field]');
if (first) first.focus();
});
}