feat(post-form): text-only draft persistence (U6)
Mirror text field values (incl. EasyMDE content) to localStorage on every edit and restore them on load. Clear the draft only when the server confirms a successful post (.notices.success) — the invariant that guarantees text survives validation failures, save errors, and session expiry (login form shown → form absent → draft left intact for post-reauth restore). Photos are not persisted (File/Blob can't serialize); on restore an inline hint says they need re-selecting. Refs R19 (preserve-on-failure), R20, KTD6.
This commit is contained in:
@@ -401,11 +401,97 @@ function initValidation() {
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Draft persistence (U6, R19 preserve-on-failure + R20, KTD6) ──
|
||||
* Text field values are mirrored to localStorage so a failed submit or an
|
||||
* expired session mid-compose never loses the writing. Photos can't be
|
||||
* serialized (File/Blob) so they are NOT persisted — an inline hint says so
|
||||
* on restore. The draft is cleared ONLY when the server confirms a successful
|
||||
* post (<div class="notices success">), which is exactly what guarantees the
|
||||
* text survives every non-success outcome, independent of failure-type sniffing.
|
||||
*/
|
||||
var DRAFT_KEY = 'intotheeast:new-entry-draft';
|
||||
|
||||
function draftEls(form) {
|
||||
return Array.prototype.slice.call(form.querySelectorAll('[name^="data["]'))
|
||||
.filter(function (el) {
|
||||
if (el.type === 'file') return false;
|
||||
var n = el.name;
|
||||
return n.indexOf('data[_json') !== 0 && n.indexOf('data[photos') !== 0;
|
||||
});
|
||||
}
|
||||
|
||||
function showReauthHint() {
|
||||
var picker = document.querySelector('.photo-picker');
|
||||
if (!picker || picker.querySelector('.photo-picker__reauth-hint')) return;
|
||||
var p = document.createElement('p');
|
||||
p.className = 'photo-picker__reauth-hint is-shown';
|
||||
p.textContent = 'Your text was restored — photos need re-selecting (they can’t be saved in a draft).';
|
||||
picker.appendChild(p);
|
||||
}
|
||||
|
||||
function initDraft() {
|
||||
var form = document.querySelector('form[name="new-entry"]');
|
||||
if (!form) return; // e.g. session expired → login form shown; draft is left intact
|
||||
|
||||
// A confirmed successful post: clear the draft and do NOT restore onto the
|
||||
// freshly reset form. This runs before any save, so process.reset can't
|
||||
// round-trip blank fields back into storage.
|
||||
if (document.querySelector('.notices.success')) {
|
||||
try { localStorage.removeItem(DRAFT_KEY); } catch (e) { /* ignore */ }
|
||||
return;
|
||||
}
|
||||
|
||||
function save() {
|
||||
var data = {};
|
||||
draftEls(form).forEach(function (el) {
|
||||
if (el.type === 'radio') { if (el.checked) { data[el.name] = el.value; } }
|
||||
else { data[el.name] = el.value; }
|
||||
});
|
||||
try { localStorage.setItem(DRAFT_KEY, JSON.stringify(data)); } catch (e) { /* quota / private mode */ }
|
||||
}
|
||||
|
||||
var raw = null;
|
||||
try { raw = localStorage.getItem(DRAFT_KEY); } catch (e) { raw = null; }
|
||||
if (raw) {
|
||||
var data = null;
|
||||
try { data = JSON.parse(raw); } catch (e) { data = null; }
|
||||
if (data) {
|
||||
var restoredSomething = false;
|
||||
Object.keys(data).forEach(function (name) {
|
||||
var val = data[name];
|
||||
if (val != null && String(val).trim()) { restoredSomething = true; }
|
||||
if (name === 'data[content]') { return; } // restored via the editor below
|
||||
var radios = form.querySelectorAll('input[type="radio"][name="' + name + '"]');
|
||||
if (radios.length) {
|
||||
radios.forEach(function (r) { r.checked = (r.value === val); });
|
||||
return;
|
||||
}
|
||||
var el = form.querySelector('[name="' + name + '"]');
|
||||
if (el && el.type !== 'file') { el.value = val; }
|
||||
});
|
||||
if (window.postFormEditor && data['data[content]'] != null) {
|
||||
window.postFormEditor.value(data['data[content]']);
|
||||
}
|
||||
if (restoredSomething) { showReauthHint(); }
|
||||
}
|
||||
}
|
||||
|
||||
// Save text on every edit (change covers selects/toggles; input covers text).
|
||||
form.addEventListener('input', save);
|
||||
form.addEventListener('change', save);
|
||||
if (window.postFormEditor) {
|
||||
window.postFormEditor.codemirror.on('change', save);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Boot ────────────────────────────────────────────────── */
|
||||
function boot() {
|
||||
// Exposed for later units (U6 draft restore) and the Playwright specs;
|
||||
// null when this bundle loads on a page without the content field.
|
||||
// Exposed for U6 draft restore and the Playwright specs; null when this
|
||||
// bundle loads on a page without the content field.
|
||||
window.postFormEditor = initEditor();
|
||||
// Restore before disclosure/geo so their on-load checks (auto-open,
|
||||
// weather-button enable) see the restored values.
|
||||
initDraft();
|
||||
initPhotoPicker();
|
||||
initDisclosure();
|
||||
initGeo();
|
||||
|
||||
Reference in New Issue
Block a user