// Progressive enhancement only. With JavaScript off the form posts, the page // reloads, and everything still works; this saves in the background instead, // so ticking a box does not cost a page load first thing in the morning. (function () { "use strict"; var form = document.querySelector("[data-log]"); if (!form) { confirmDestructiveForms(); return; } document.body.classList.add("has-sync"); var status = form.querySelector("[data-status]"); var todayTile = document.querySelector(".tile-today"); var timer = null; var dirty = false; // ------------------------------------------------------------- painting // The meters redraw from the values already on the page rather than from // the response, so the band moves as you type instead of after a round trip. function paintMeter(band) { var input = band.querySelector(".band-input"); var target = parseInt(band.getAttribute("data-target"), 10); var ceiling = band.getAttribute("data-mode") === "ceiling"; var raw = input.value.trim(); var value = raw === "" ? null : parseInt(raw, 10); if (value === null || isNaN(value) || !target) { band.style.setProperty("--fill", "0%"); band.classList.remove("is-met", "is-over"); return false; } band.style.setProperty("--fill", Math.min(100, (value * 100) / target) + "%"); var met = ceiling ? value <= target : value >= target; band.classList.toggle("is-met", met); band.classList.toggle("is-over", ceiling && value > target); return met; } function gratitudeMet() { var lines = form.querySelectorAll(".gratitude-input"); for (var i = 0; i < lines.length; i++) { if (lines[i].value.trim() === "") return false; } return true; } function repaint() { var score = 0; var workout = form.querySelector(".band-check"); if (workout) { form.querySelector(".band-toggle").classList.toggle("is-met", workout.checked); if (workout.checked) score++; } var bands = form.querySelectorAll(".band-meter"); for (var i = 0; i < bands.length; i++) { if (paintMeter(bands[i])) score++; } var gratitude = form.querySelector(".gratitude"); var gratitudeOK = gratitudeMet(); if (gratitude) gratitude.classList.toggle("is-met", gratitudeOK); if (gratitudeOK) score++; // Only the day being edited is on screen, so only its tile can change. if (todayTile && todayTile.getAttribute("data-date") === currentDate()) { todayTile.style.setProperty("--score", score); todayTile.classList.toggle("tile-full", score === 5); } } function currentDate() { return form.getAttribute("action").replace("/d/", ""); } // -------------------------------------------------------------- saving function say(message, bad) { if (!status) return; status.textContent = message; status.classList.toggle("is-bad", !!bad); } function save() { dirty = false; say("Saving"); fetch(form.getAttribute("action"), { method: "POST", headers: { "X-Sync": "1" }, body: new FormData(form), keepalive: true }) .then(function (response) { return response.json().then(function (body) { if (!response.ok) throw new Error(body.error || "That did not save."); return body; }); }) .then(function () { say("Saved"); }) .catch(function (err) { // Leaving the entry on screen means nothing typed is lost; the button // is still there to retry with. say(err.message || "Not saved — still on this device only", true); dirty = true; }); } function queue() { dirty = true; say(""); clearTimeout(timer); timer = setTimeout(save, 600); } form.addEventListener("input", function () { repaint(); queue(); }); // A checkbox is a decision, not a draft: write it straight away. form.addEventListener("change", function (event) { repaint(); if (event.target.type === "checkbox") { clearTimeout(timer); save(); } }); // Backgrounding the tab on a phone is how most edits end. Flush first. document.addEventListener("visibilitychange", function () { if (document.visibilityState === "hidden" && dirty) { clearTimeout(timer); save(); } }); form.addEventListener("submit", function () { clearTimeout(timer); }); repaint(); confirmDestructiveForms(); function confirmDestructiveForms() { document.addEventListener("submit", function (event) { var message = event.target.getAttribute("data-confirm"); if (message && !window.confirm(message)) { event.preventDefault(); } }); } })();