Keep old pages readable, detect stale settings writes, make backups honest
- Own CSS is parsed on write only. A page whose stored CSS no longer passes a stricter rule answers 200 and opens in the editor; that one block loses its CSS. A document read several times in one request is validated once. - Settings and theme refuse a write from an outdated form. - A backup fails when a media file does not arrive completely and leaves no half directory behind; rotation counts intact backups only. - The archive update takes its backup behind the maintenance page. - A package import puts all media in place before removing the old ones, reports honestly when that fails and keeps a maintenance state that was already on. - Restoring replaces the SQLite file in one move and points at pending migrations. - Six irreversible actions ask first, in a dialog of the admin's own. - Ctrl+S in the editor takes the field being typed in along; Delete no longer removes a block while a dialog is open. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d61aa0f22
commit
ead47cc924
25 files changed
+437
-72
No files matched your search
+10
-4
@@ -1155,7 +1155,13 @@ details.form-card > summary.form-card-summary::before, details.form-card[open] >
|
||||
.update-lauf .secondary { background: transparent; color: #fff; border-color: #ffffff66 }
|
||||
.update-letztes { margin: 0 0 12px }
|
||||
.update-manual { margin-top: 6px }
|
||||
.update-dialog { max-width: 460px; border: 0; border-radius: 12px; padding: 24px 26px; box-shadow: 0 18px 48px #0003 }
|
||||
.update-dialog::backdrop { background: #1f2d2766 }
|
||||
.update-dialog h2 { margin: 0 0 10px; font-size: 18px }
|
||||
.update-dialog .dialog-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px }
|
||||
.update-dialog, .confirm-dialog { max-width: 460px; border: 0; border-radius: 12px; padding: 24px 26px; box-shadow: 0 18px 48px #0003 }
|
||||
.update-dialog::backdrop, .confirm-dialog::backdrop { background: #1f2d2766 }
|
||||
.update-dialog h2, .confirm-dialog h2 { margin: 0 0 10px; font-size: 18px }
|
||||
.update-dialog .dialog-actions, .confirm-dialog .dialog-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px }
|
||||
/* Die Rückfrage vor etwas Endgültigem (confirm.js). */
|
||||
.confirm-dialog { width: calc(100vw - 32px); color: var(--ink) }
|
||||
.confirm-dialog p { margin: 0; color: var(--muted); line-height: 1.5 }
|
||||
.confirm-dialog .dialog-actions .primary, .confirm-dialog .dialog-actions .secondary { margin: 0 }
|
||||
.primary.danger { background: var(--danger); border-color: var(--danger) }
|
||||
.primary.danger:hover:not(:disabled) { background: #6f251b; border-color: #6f251b }
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
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();
|
||||
},
|
||||
});
|
||||
});
|
||||
})();
|
||||
+14
-2
@@ -1920,7 +1920,15 @@ function onKey(event) {
|
||||
if (!history || busy) return;
|
||||
const key = event.key.toLowerCase();
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
if (key === 's') { event.preventDefault(); save(); }
|
||||
if (key === 's') {
|
||||
event.preventDefault();
|
||||
// Viele Felder schreiben erst beim Verlassen ins Dokument (eigenes CSS, Zahlen im Design).
|
||||
// Ohne diesen Schritt sicherte Strg+S den Stand ohne das, was gerade getippt wurde - und
|
||||
// meldete trotzdem „gespeichert“.
|
||||
const field = document.activeElement;
|
||||
if (field && field.ownerDocument === document && inField(field) && !field.isContentEditable) field.blur();
|
||||
save();
|
||||
}
|
||||
// Keep native text-field undo intact; the toolbar always undoes the complete document.
|
||||
if (inField(event.target)) return;
|
||||
if (key === 'z' || key === 'y') { event.preventDefault(); (event.shiftKey || key === 'y') ? history.redo() : history.undo(); changed(); }
|
||||
@@ -1935,7 +1943,11 @@ function onKey(event) {
|
||||
}
|
||||
// Delete removes the selected element - but never while a field or the preview text is
|
||||
// being typed in, and never something the sidebar buttons would refuse.
|
||||
if ((event.key === 'Delete' || event.key === 'Backspace') && selected && !inField(event.target) && !inlineEdit && !$('#delete').disabled) {
|
||||
// Auch nicht, während ein Dialog offen ist oder der Fokus auf einem Bedienelement der
|
||||
// Seitenleiste liegt: Entf auf einer Kachel der Mediathek löschte sonst den gewählten Baustein.
|
||||
const where = event.target;
|
||||
const onStage = !where?.closest || where.ownerDocument !== document || where === document.body || Boolean(where.closest('#structure, .canvas-scroll'));
|
||||
if ((event.key === 'Delete' || event.key === 'Backspace') && selected && onStage && !document.querySelector('dialog[open]') && !inField(event.target) && !inlineEdit && !$('#delete').disabled) {
|
||||
event.preventDefault(); actions.delete(selected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +316,12 @@ if (root) {
|
||||
if (toggle) { const card = toggle.closest('[data-field]'); setOpen(card, !card.classList.contains('is-open')); refresh(); return; }
|
||||
const button = event.target.closest('[data-remove-field]');
|
||||
if (!button) return;
|
||||
button.closest('[data-field]').remove(); refresh(); add.focus();
|
||||
const card = button.closest('[data-field]');
|
||||
const name = card.querySelector('[data-field-label]')?.value.trim();
|
||||
const remove = () => { card.remove(); refresh(); add.focus(); };
|
||||
// Ein eben angelegtes, leeres Feld verschwindet ohne Frage; alles andere fragt nach.
|
||||
if (!name || !window.kantConfirm) { remove(); return; }
|
||||
window.kantConfirm({ title: `Feld „${name}“ entfernen?`, message: 'Bereits eingegangene Anfragen behalten ihre Angaben. Wirksam wird es erst mit „Speichern“.', confirmLabel: 'Feld entfernen', variant: 'danger', onConfirm: remove });
|
||||
});
|
||||
list.addEventListener('input', event => {
|
||||
const card = event.target.closest('[data-field]');
|
||||
|
||||
Reference in new issue
Block a user