- The editor keeps unsaved changes in the browser and offers them after a crash, a closed tab or an expired session. No timed save on the server: every save is a full revision. - The save status stays visible in narrow windows. - Settings get a sticky section bar, two folded groups and a save bar that stays in view. - The admin post list fetches terms and images once, not per post. - Archive post titles are second-level headings, the archive head keeps full text contrast, the skip link sits above a sticky header, a linked image without alternative text names its link. - The editor check starts through the dev router like the others. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
73 lines
3.6 KiB
JavaScript
73 lines
3.6 KiB
JavaScript
/**
|
|
* Rückfrage im Stil der Verwaltung - statt confirm() des Browsers, und statt gar keiner.
|
|
*
|
|
* Ein Formular meldet sich mit data-confirm-title an (dazu optional data-confirm-text,
|
|
* data-confirm-label und data-confirm-variant="danger"). Sein Absenden wird angehalten, bis der
|
|
* Dialog bestätigt ist. Ohne JavaScript bleibt es ein gewöhnliches Formular.
|
|
*
|
|
* Skripte rufen kantConfirm({ title, message, confirmLabel, variant, onConfirm }) auf.
|
|
*/
|
|
(() => {
|
|
let dialog = null;
|
|
let pending = null;
|
|
let opener = null;
|
|
|
|
const build = () => {
|
|
dialog = document.createElement('dialog');
|
|
dialog.className = 'confirm-dialog';
|
|
dialog.setAttribute('aria-labelledby', 'confirm-dialog-title');
|
|
dialog.setAttribute('aria-describedby', 'confirm-dialog-text');
|
|
const title = document.createElement('h2'); title.id = 'confirm-dialog-title';
|
|
const text = document.createElement('p'); text.id = 'confirm-dialog-text';
|
|
const actions = document.createElement('div'); actions.className = 'dialog-actions';
|
|
const cancel = document.createElement('button'); cancel.type = 'button'; cancel.className = 'secondary'; cancel.textContent = 'Abbrechen'; cancel.dataset.confirmCancel = '';
|
|
const ok = document.createElement('button'); ok.type = 'button'; ok.dataset.confirmOk = '';
|
|
actions.append(cancel, ok);
|
|
dialog.append(title, text, actions);
|
|
cancel.addEventListener('click', () => dialog.close());
|
|
ok.addEventListener('click', () => { const run = pending; pending = null; dialog.close(); run?.(); });
|
|
// Escape und der Klick neben den Dialog schließen ohne Folgen.
|
|
dialog.addEventListener('click', event => { if (event.target === dialog) dialog.close(); });
|
|
dialog.addEventListener('close', () => { pending = null; opener?.focus?.({ preventScroll: true }); opener = null; });
|
|
document.body.append(dialog);
|
|
};
|
|
|
|
const ask = ({ title, message = '', confirmLabel = 'Bestätigen', variant = 'primary', onConfirm }) => {
|
|
if (!dialog) build();
|
|
if (dialog.open) dialog.close();
|
|
opener = document.activeElement;
|
|
dialog.querySelector('h2').textContent = title;
|
|
const text = dialog.querySelector('p'); text.textContent = message; text.hidden = '' === message;
|
|
const ok = dialog.querySelector('[data-confirm-ok]');
|
|
ok.textContent = confirmLabel;
|
|
ok.className = 'danger' === variant ? 'primary danger' : 'primary';
|
|
pending = onConfirm;
|
|
dialog.showModal();
|
|
// Der Fokus liegt auf „Abbrechen“: Die Eingabetaste darf nichts Endgültiges auslösen.
|
|
dialog.querySelector('[data-confirm-cancel]').focus();
|
|
};
|
|
window.kantConfirm = ask;
|
|
|
|
// Ein ungültiges Feld in einem zugeklappten Bereich kann der Browser nicht anspringen - das
|
|
// Absenden bliebe ohne jede Meldung stehen. Der Bereich klappt deshalb auf.
|
|
document.addEventListener('invalid', event => { event.target.closest?.('details:not([open])')?.setAttribute('open', ''); }, true);
|
|
|
|
document.addEventListener('submit', event => {
|
|
const form = event.target;
|
|
if (!(form instanceof HTMLFormElement) || !form.dataset.confirmTitle || form.dataset.confirmed) return;
|
|
event.preventDefault();
|
|
const submitter = event.submitter;
|
|
ask({
|
|
title: form.dataset.confirmTitle,
|
|
message: form.dataset.confirmText || '',
|
|
confirmLabel: form.dataset.confirmLabel || submitter?.textContent.trim() || 'Bestätigen',
|
|
variant: form.dataset.confirmVariant || 'primary',
|
|
onConfirm: () => {
|
|
form.dataset.confirmed = '1';
|
|
// requestSubmit nimmt den Knopf mit, falls er einen Namen und Wert trägt.
|
|
submitter && submitter.form === form ? form.requestSubmit(submitter) : form.requestSubmit();
|
|
},
|
|
});
|
|
});
|
|
})();
|