| 1 | // booking.js — the availability pane, and the zone preselect. |
| 2 | // |
| 3 | // Two enhancements, neither load-bearing. The pane asks the server what |
| 4 | // the availability sentence means while the host types, and swaps the |
| 5 | // answer in; it parses nothing, for the same reason quickadd.js parses |
| 6 | // nothing — the server's parser is the one that decides what is saved. |
| 7 | // The zone preselect picks the browser's zone on a public booking page |
| 8 | // when nobody has chosen one yet. Without this file the form still |
| 9 | // saves, the pane still fills in on the way back, and the page still |
| 10 | // offers the host's zone. |
| 11 | (function () { |
| 12 | "use strict"; |
| 13 | |
| 14 | var DEBOUNCE_MS = 220; |
| 15 | |
| 16 | function setupPane(root) { |
| 17 | var text = root.querySelector("[cal-book-text]"); |
| 18 | var pane = root.querySelector("[cal-book-pane]"); |
| 19 | if (!text || !pane) return; |
| 20 | |
| 21 | var timer = null; |
| 22 | var inflight = null; |
| 23 | var lastSent = null; |
| 24 | |
| 25 | function ask() { |
| 26 | var value = text.value.trim(); |
| 27 | if (value === lastSent) return; |
| 28 | lastSent = value; |
| 29 | if (inflight) inflight.abort(); |
| 30 | var ctrl = new AbortController(); |
| 31 | inflight = ctrl; |
| 32 | |
| 33 | var body = new URLSearchParams(); |
| 34 | body.set("text", value); |
| 35 | fetch("/settings/booking/parse", { |
| 36 | method: "POST", |
| 37 | credentials: "same-origin", |
| 38 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, |
| 39 | body: body.toString(), |
| 40 | signal: ctrl.signal |
| 41 | }) |
| 42 | .then(function (res) { |
| 43 | if (!res.ok) throw new Error("parse " + res.status); |
| 44 | return res.text(); |
| 45 | }) |
| 46 | .then(function (html) { |
| 47 | if (ctrl !== inflight) return; |
| 48 | pane.innerHTML = html; |
| 49 | }) |
| 50 | .catch(function (err) { |
| 51 | if (err && err.name === "AbortError") return; |
| 52 | pane.innerHTML = '<p cal-book-blank>Could not check that just now. Saving will still work.</p>'; |
| 53 | }); |
| 54 | } |
| 55 | |
| 56 | text.addEventListener("input", function () { |
| 57 | if (timer) clearTimeout(timer); |
| 58 | timer = setTimeout(ask, DEBOUNCE_MS); |
| 59 | }); |
| 60 | } |
| 61 | |
| 62 | // The zone select on a public page: preselect the browser's zone, but |
| 63 | // only when it is one the page offers and nobody has chosen one yet |
| 64 | // (data-chosen is set when the URL carried a zone). A guess about a |
| 65 | // device, offered once; never a silent re-timing of a chosen value. |
| 66 | function setupZone() { |
| 67 | var select = document.querySelector("[cal-book-zone]"); |
| 68 | if (!select || select.hasAttribute("data-chosen")) return; |
| 69 | var guess; |
| 70 | try { |
| 71 | guess = Intl.DateTimeFormat().resolvedOptions().timeZone; |
| 72 | } catch (e) { |
| 73 | return; |
| 74 | } |
| 75 | if (!guess) return; |
| 76 | if (select.value === guess) return; |
| 77 | for (var i = 0; i < select.options.length; i++) { |
| 78 | if (select.options[i].value === guess) { |
| 79 | // Reload THROUGH the form, so the server re-renders every time |
| 80 | // on the page in the guessed zone. Changing the select alone |
| 81 | // would relabel the selector while the times stayed in the |
| 82 | // host's zone — a page that says one zone and means another. |
| 83 | select.value = guess; |
| 84 | var form = select.form; |
| 85 | if (form && typeof form.requestSubmit === "function") form.requestSubmit(); |
| 86 | else if (form) form.submit(); |
| 87 | return; |
| 88 | } |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | function init() { |
| 93 | var forms = document.querySelectorAll("[cal-book-form]"); |
| 94 | for (var i = 0; i < forms.length; i++) setupPane(forms[i]); |
| 95 | setupZone(); |
| 96 | } |
| 97 | |
| 98 | if (document.readyState === "loading") { |
| 99 | document.addEventListener("DOMContentLoaded", init); |
| 100 | } else { |
| 101 | init(); |
| 102 | } |
| 103 | })(); |
| 104 | |