Back up unsaved editor work locally, structure the settings, fix archive accessibility
- 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>
This commit is contained in:
1 parent
5e46c98825
commit
b08862b139
17 files changed
+182
-26
No files matched your search
@@ -88,6 +88,51 @@ function playPreview() {
|
||||
|
||||
$('#preview-motion').addEventListener('click', () => { previewPlayback = !previewPlayback; playPreview(); });
|
||||
|
||||
// ---- Lokale Sicherung --------------------------------------------------------------------------
|
||||
// Ungespeicherte Änderungen liegen zusätzlich im Browser dieses Geräts. Stürzt der Browser ab,
|
||||
// wird der Tab geschlossen oder läuft die Sitzung ab, bietet der Editor sie beim nächsten Öffnen
|
||||
// zum Wiederherstellen an. Bewusst kein Speichern auf dem Server im Takt: Jedes Speichern legt
|
||||
// dort eine vollständige Fassung an, und die Liste der Fassungen soll zeigen, was jemand
|
||||
// gespeichert hat - nicht jeden Tastendruck.
|
||||
const backupKey = `kant-editor:${root.dataset.api}`;
|
||||
let backupTimer = null, backupOffer = null;
|
||||
function readBackup() {
|
||||
try {
|
||||
const entry = JSON.parse(localStorage.getItem(backupKey) || 'null');
|
||||
return entry && Array.isArray(entry.document?.blocks) ? entry : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
function writeBackup() {
|
||||
// Solange ein Angebot offen ist, bleibt die Sicherung unangetastet: Der eben geladene Stand
|
||||
// ist unverändert und würde sie sonst löschen, bevor jemand entschieden hat.
|
||||
if (!history || backupOffer) return;
|
||||
try {
|
||||
if (history.dirty) localStorage.setItem(backupKey, JSON.stringify({ at: Date.now(), revisionId, document: history.document }));
|
||||
else localStorage.removeItem(backupKey);
|
||||
} catch { /* Speicher voll oder gesperrt: dann ohne Sicherung, der Editor arbeitet weiter. */ }
|
||||
}
|
||||
function scheduleBackup() { clearTimeout(backupTimer); backupTimer = setTimeout(writeBackup, 600); }
|
||||
function closeBackupOffer(remove) {
|
||||
backupOffer = null; $('#local-backup').hidden = true;
|
||||
if (remove) { try { localStorage.removeItem(backupKey); } catch { /* nichts zu tun */ } }
|
||||
}
|
||||
function offerBackup() {
|
||||
const entry = readBackup();
|
||||
if (!entry || JSON.stringify(entry.document) === JSON.stringify(history.document)) { closeBackupOffer(Boolean(entry)); return; }
|
||||
backupOffer = entry;
|
||||
const when = new Date(entry.at).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||
$('#local-backup-text').textContent = `Auf diesem Gerät liegen ungespeicherte Änderungen vom ${when} Uhr.`
|
||||
+ (entry.revisionId && entry.revisionId !== revisionId ? ' Seitdem wurde die Seite auf dem Server gespeichert – beim Wiederherstellen ersetzt dein Stand den neueren, sobald du speicherst.' : '');
|
||||
$('#local-backup').hidden = false;
|
||||
}
|
||||
function restoreBackup() {
|
||||
const entry = backupOffer; if (!entry || !history || busy) return;
|
||||
const wanted = repairSchema(clone(entry.document));
|
||||
// Als ein Schritt im Verlauf: „Rückgängig“ führt zum geladenen Stand zurück.
|
||||
history.change(doc => { for (const key of Object.keys(doc)) delete doc[key]; Object.assign(doc, wanted); });
|
||||
closeBackupOffer(false); selected = null; changed();
|
||||
}
|
||||
|
||||
function notify(message = '', recover = false) {
|
||||
$('#message').textContent = message;
|
||||
$('#message').hidden = !message;
|
||||
@@ -118,6 +163,10 @@ async function request(path, body, signal) {
|
||||
function controls() {
|
||||
const ready = Boolean(history);
|
||||
$('#save-status').textContent = busy ? 'Wird gespeichert …' : conflict ? 'Speicherkonflikt' : !ready ? 'Wird geladen …' : history.dirty ? 'Ungespeicherte Änderungen' : editingArea ? 'Bereich gespeichert' : revisionId === publishedRevisionId ? 'Veröffentlicht' : 'Entwurf gespeichert';
|
||||
// Der Zustand als Kennung: Auf schmalen Fenstern zeigt die Kopfzeile statt des ganzen Satzes
|
||||
// nur ein kurzes Wort dazu (editor.css) - vorher verschwand die Anzeige dort ganz.
|
||||
$('#save-status').dataset.state = busy ? 'saving' : conflict ? 'conflict' : !ready ? 'loading' : history.dirty ? 'dirty' : 'saved';
|
||||
if (ready && !busy) scheduleBackup();
|
||||
$('#save').disabled = !ready || busy || conflict || !history.dirty;
|
||||
if ($('#publish')) $('#publish').disabled = !ready || busy || conflict || history.dirty || revisionId === publishedRevisionId;
|
||||
$('#undo').disabled = !ready || busy || !history.past.length;
|
||||
@@ -1451,6 +1500,7 @@ async function load() {
|
||||
registerPlugins(data.plugins || [], data.presets || []); buildLibrary(); buildPluginPresets();
|
||||
selected = null; propertyPanel = 'content'; collapsed.clear(); conflict = false; notify();
|
||||
if (language) setView('translate');
|
||||
offerBackup();
|
||||
changed(); resizePreview();
|
||||
} catch (error) { notify(error.message, true); }
|
||||
finally { busy = false; controls(); }
|
||||
@@ -1952,6 +2002,10 @@ function onKey(event) {
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', onKey);
|
||||
$('#local-backup-restore').addEventListener('click', restoreBackup);
|
||||
$('#local-backup-discard').addEventListener('click', () => closeBackupOffer(true));
|
||||
// Beim Verlassen sofort schreiben, nicht erst nach der Wartezeit.
|
||||
window.addEventListener('pagehide', () => { clearTimeout(backupTimer); writeBackup(); });
|
||||
window.addEventListener('beforeunload', event => { if (history?.dirty || busy) { event.preventDefault(); event.returnValue = ''; } });
|
||||
buildLibrary();
|
||||
// The workspace is a fixed frame; a scroll position restored from an earlier visit would push
|
||||
|
||||
Reference in new issue
Block a user