303 lines
13 KiB
JavaScript
303 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();
|
||
|
|
});
|
||
|
|
}
|