Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,303 @@
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user