// draft.js — a draft survives a submit that did not land.
//
// Two ways a filled form used to vanish: a submit whose session had
// lapsed is a 303 to /login, and a refusal renders a whole "Not done"
// page in place of the form. Both are the same loss — the reader's words
// were only ever in the textarea, and the textarea is gone (0457b6e6,
// G-82/G-88).
//
// The rule that makes this safe: a draft is RESTORED only after the
// SERVER has said the submit did not go through. The login page and
// every page rendered with a 4xx/5xx status carry data-draft-keep on
// <body>; this script, seeing it, marks the stash "kept". Any other page
// after a submit means it landed, and the stash is dropped. Guessing
// from the client alone is wrong in the common case — a ticket comment
// posts and redirects back to the same page, and restoring there would
// put the just-posted comment back in the box, one click from a
// duplicate.
//
// sessionStorage, not localStorage: a draft belongs to the sitting that
// typed it and must not turn up in someone else's session on a shared
// machine tomorrow. An hour's life at most. First-party, dependency-free,
// delegated from the document; a page without it is exactly the page it
// was.
(function () {
"use strict";
var KEY = "quarry.draft", LIFE_MS = 60 * 60 * 1000;
var store;
try { store = window.sessionStorage; } catch (e) { return; }
if (!store) return;
function read() {
try {
var s = JSON.parse(store.getItem(KEY) || "null");
if (s && Date.now() - s.at < LIFE_MS) return s;
} catch (e) {}
drop();
return null;
}
function write(s) { try { store.setItem(KEY, JSON.stringify(s)); } catch (e) {} }
function drop() { try { store.removeItem(KEY); } catch (e) {} }
// Only forms that WRITE something, and never one that carries a
// credential. A GET form is a search or a filter; a form with a
// password field is a login, whose contents belong nowhere but the
// request.
function eligible(form) {
if (!(form instanceof HTMLFormElement) || form.hasAttribute("data-no-draft")) return false;
if ((form.getAttribute("method") || "get").toLowerCase() !== "post") return false;
return !form.querySelector('input[type="password"]');
}
function fields(form) {
var out = [], els = form.elements;
for (var i = 0; i < els.length; i++) {
var el = els[i];
if (!el.name || el.disabled) continue;
if (el.tagName === "TEXTAREA" || (el.tagName === "INPUT" && el.type === "text")) out.push(el);
}
return out;
}
document.addEventListener("submit", function (ev) {
var form = ev.target;
if (!eligible(form)) return;
var draft = {}, any = false;
fields(form).forEach(function (el) {
if (el.value !== "") { draft[el.name] = el.value; any = true; }
});
if (any) write({ path: location.pathname, draft: draft, state: "pending", at: Date.now() });
else drop();
}, true);
// pageshow rather than DOMContentLoaded: it also fires when Back
// returns a page from the bfcache, which is how most readers leave a
// refusal page.
window.addEventListener("pageshow", function () {
var s = read();
if (!s) return;
if (document.body && document.body.hasAttribute("data-draft-keep")) {
if (s.state !== "kept") { s.state = "kept"; write(s); }
return;
}
if (s.state !== "kept") { drop(); return; } // the submit landed
if (location.pathname !== s.path) return; // wandered; keep it for the way back
var used = false, forms = document.forms;
for (var f = 0; f < forms.length && !used; f++) {
if (!eligible(forms[f])) continue;
fields(forms[f]).forEach(function (el) {
if (el.value === "" && Object.prototype.hasOwnProperty.call(s.draft, el.name)) {
el.value = s.draft[el.name];
el.dispatchEvent(new Event("input", { bubbles: true }));
el.setAttribute("data-draft-restored", "");
used = true;
}
});
}
drop();
});
})();