/* ========================================================================== Brew Log — page behaviour Small, dependency-free helpers. Charts live in charts.js. ========================================================================== */ (function () { "use strict"; /** Auto-focus the first empty field on a form. */ function focusFirstEmpty() { var fields = document.querySelectorAll("input:not([type=hidden]), textarea, select"); for (var i = 0; i < fields.length; i++) { if (!fields[i].value && !fields[i].readOnly) { fields[i].focus(); return; } } } /** Default the reading date to today when the field is left empty. */ function defaultDateFields() { var today = new Date().toISOString().slice(0, 10); var dateFields = document.querySelectorAll('input[type="date"][data-default-today]'); for (var i = 0; i < dateFields.length; i++) { if (!dateFields[i].value) { dateFields[i].value = today; } } } /** Show/hide blocks driven by a checkbox. */ function wireToggles() { var toggles = document.querySelectorAll("[data-toggle-target]"); for (var i = 0; i < toggles.length; i++) { (function (toggle) { var target = document.getElementById(toggle.getAttribute("data-toggle-target")); if (!target) { return; } function apply() { target.classList.toggle("hidden", !toggle.checked); } toggle.addEventListener("change", apply); apply(); })(toggles[i]); } } /** Confirm destructive actions. */ function wireConfirms() { var forms = document.querySelectorAll("[data-confirm]"); for (var i = 0; i < forms.length; i++) { forms[i].addEventListener("submit", function (event) { if (!window.confirm(this.getAttribute("data-confirm"))) { event.preventDefault(); } }); } } /** * The brewing advice pop-out. * * On a desktop the panel opens on hover, which the CSS does. A * phone has no hover, so the trigger is a button that toggles it - * and it is a real button, so it works with a keyboard and with a * screen reader. */ /** * The brewing advice pop-out. * * The advice runs to several hundred characters. In a table cell it * pushed the table past the width of a phone, so it is a compact * trigger instead and the text is shown in a floating panel. * * The panel is position:fixed so it escapes the table's scroll * container - absolutely positioned inside the cell it was clipped * by the container and sliced in half by the card's edge. * * It opens on hover on a desktop, and on tap on a phone, which has * no hover. The trigger is a real button, so it also works with a * keyboard. */ function wireAdvicePopouts() { var triggers = document.querySelectorAll(".advice-trigger"); for (var i = 0; i < triggers.length; i++) { (function (trigger) { var panel = trigger.parentNode.querySelector(".advice-pop"); if (!panel) { return; } var closeTimer = null; function isPhone() { return window.matchMedia("(max-width: 720px)").matches; } function place() { if (isPhone()) { /* The CSS centres it on a phone - clear any inline position left over from a desktop resize. */ panel.style.top = ""; panel.style.left = ""; return; } var box = trigger.getBoundingClientRect(); var gap = 10; var height = panel.offsetHeight; var width = panel.offsetWidth; var spaceBelow = window.innerHeight - box.bottom - gap; var spaceAbove = box.top - gap; /* Prefer below; flip above when there is not enough room and there is more room above. */ var top; if (height <= spaceBelow || spaceBelow >= spaceAbove) { top = box.bottom + gap; } else { top = Math.max(gap, box.top - height - gap); } /* Keep it on screen horizontally. */ var left = Math.min(box.left, window.innerWidth - width - gap); left = Math.max(gap, left); panel.style.top = Math.round(top) + "px"; panel.style.left = Math.round(left) + "px"; } function open() { if (closeTimer) { clearTimeout(closeTimer); closeTimer = null; } panel.classList.add("is-open"); trigger.setAttribute("aria-expanded", "true"); place(); } function close() { panel.classList.remove("is-open"); trigger.setAttribute("aria-expanded", "false"); } function closeSoon() { /* A short delay, so the mouse can travel from the trigger into the panel without it closing. */ if (closeTimer) { clearTimeout(closeTimer); } closeTimer = setTimeout(function () { closeTimer = null; if (!panel.matches(":hover") && !trigger.matches(":hover")) { close(); } }, 180); } trigger.addEventListener("click", function (event) { event.preventDefault(); event.stopPropagation(); if (panel.classList.contains("is-open")) { close(); } else { open(); } }); trigger.addEventListener("mouseenter", function () { if (!isPhone()) { open(); } }); trigger.addEventListener("mouseleave", closeSoon); panel.addEventListener("mouseenter", function () { if (closeTimer) { clearTimeout(closeTimer); closeTimer = null; } }); panel.addEventListener("mouseleave", closeSoon); var closeButton = panel.querySelector(".advice-pop__close"); if (closeButton) { closeButton.addEventListener("click", function (event) { event.preventDefault(); event.stopPropagation(); close(); trigger.focus(); }); } document.addEventListener("keydown", function (event) { if (event.key === "Escape" && panel.classList.contains("is-open")) { close(); trigger.focus(); } }); document.addEventListener("click", function (event) { if (!panel.contains(event.target) && event.target !== trigger) { close(); } }); window.addEventListener("resize", function () { if (panel.classList.contains("is-open")) { place(); } }); window.addEventListener("scroll", function () { if (panel.classList.contains("is-open")) { close(); } }, true); })(triggers[i]); } } function init() { focusFirstEmpty(); defaultDateFields(); wireToggles(); wireConfirms(); wireAdvicePopouts(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();