PageSourceSearch

https://cubbes.app/js/consent.js

js cubbes.app collected 2026-09-24 12:55:48 UTC 7,459 bytes, 232 lines download raw bytes

1(function () {
2  const CONSENT_KEY = "cubbes_consent_v1";
3  const ANALYTICS_KEYS = [
4    "cubbes_session_id",
5    "cubbes_session_started_at",
6    "cubbes_visitor_id",
7    "cubbes_last_visit_at",
8  ];
9
10  let consent = readConsent();
11  let modal;
12  let settingsButton;
13  let analyticsToggle;
14  let settingsPanel;
15  let previouslyFocused;
16
17  function readConsent() {
18    try {
19      const value = JSON.parse(localStorage.getItem(CONSENT_KEY));
20      return value && value.version === 1 && typeof value.analytics === "boolean"
21        ? value
22        : null;
23    } catch (_) {
24      return null;
25    }
26  }
27
28  function clearAnalyticsStorage() {
29    ANALYTICS_KEYS.forEach((key) => {
30      try {
31        localStorage.removeItem(key);
32        sessionStorage.removeItem(key);
33      } catch (_) {
34        // Storage can be unavailable in strict browser privacy modes.
35      }
36    });
37  }
38
39  function saveConsent(analytics) {
40    consent = {
41      version: 1,
42      essential: true,
43      analytics: Boolean(analytics),
44      updatedAt: new Date().toISOString(),
45    };
46
47    try {
48      localStorage.setItem(CONSENT_KEY, JSON.stringify(consent));
49    } catch (_) {
50      // The choice still applies for the current page if storage is unavailable.
51    }
52
53    if (!consent.analytics) {
54      clearAnalyticsStorage();
55    }
56
57    window.dispatchEvent(
58      new CustomEvent("cubbes:consent-changed", {
59        detail: { analytics: consent.analytics },
60      }),
61    );
62    closeModal();
63  }
64
65  function showMainPanel() {
66    settingsPanel.hidden = true;
67    modal.querySelector("[data-consent-main]").hidden = false;
68  }
69
70  function showSettingsPanel() {
71    modal.querySelector("[data-consent-main]").hidden = true;
72    settingsPanel.hidden = false;
73    analyticsToggle.checked = Boolean(consent?.analytics);
74    analyticsToggle.focus();
75  }
76
77  function openModal() {
78    previouslyFocused = document.activeElement;
79    showMainPanel();
80    modal.hidden = false;
81    settingsButton.hidden = true;
82    document.body.classList.add("cookie-consent-open");
83    modal.querySelector(".cookie-consent__close").focus();
84  }
85
86  function closeModal() {
87    if (!modal) return;
88    modal.hidden = true;
89    settingsButton.hidden = false;
90    document.body.classList.remove("cookie-consent-open");
91    if (previouslyFocused && typeof previouslyFocused.focus === "function") {
92      previouslyFocused.focus();
93    }
94  }
95
96  function closeWithNecessaryOnly() {
97    if (consent) {
98      closeModal();
99    } else {
100      saveConsent(false);
101    }
102  }
103
104  function keepFocusInside(event) {
105    if (event.key === "Escape") {
106      closeWithNecessaryOnly();
107      return;
108    }
109
110    if (event.key !== "Tab") return;
111    const focusable = Array.from(
112      modal.querySelectorAll(
113        'button:not([hidden]), input:not([disabled]), a[href]:not([hidden])',
114      ),
115    ).filter((element) => element.offsetParent !== null);
116    if (!focusable.length) return;
117
118    const first = focusable[0];
119    const last = focusable[focusable.length - 1];
120    if (event.shiftKey && document.activeElement === first) {
121      event.preventDefault();
122      last.focus();
123    } else if (!event.shiftKey && document.activeElement === last) {
124      event.preventDefault();
125      first.focus();
126    }
127  }
128
129  function render() {
130    document.body.insertAdjacentHTML(
131      "beforeend",
132      `
133        <div class="cookie-consent" role="dialog" aria-modal="true" aria-label="Privacy choices" hidden>
134          <div class="cookie-consent__card">
135            <div class="cookie-consent__header">
136              <img class="cookie-consent__logo" src="/assets/logo.svg" alt="Cubbes" />
137              <button class="cookie-consent__close" type="button" aria-label="Close privacy choices">&times;</button>
138            </div>
139
140            <div data-consent-main>
141              <h2 id="cookieConsentTitle">We value your privacy</h2>
142              <p>
143                We use necessary storage to remember your privacy choice. With your permission,
144                we also use analytics to understand how people use Cubbes and improve the website.
145                Read our <a href="/privacy-policy">privacy policy</a>.
146              </p>
147              <div class="cookie-consent__actions">
148                <button class="btn btn-outline-primary" type="button" data-consent-customise>Customise</button>
149                <button class="btn btn-outline-primary" type="button" data-consent-reject>Reject optional</button>
150                <button class="btn btn-primary" type="button" data-consent-accept>Accept all</button>
151              </div>
152            </div>
153
154            <div data-consent-settings hidden>
155              <h2>Privacy choices</h2>
156              <div class="cookie-consent__option">
157                <div>
158                  <strong>Necessary</strong>
159                  <span>Remembers your privacy choice.</span>
160                </div>
161                <span class="cookie-consent__always-on">Always on</span>
162              </div>
163              <label class="cookie-consent__option" for="cookieAnalyticsConsent">
164                <div>
165                  <strong>Analytics</strong>
166                  <span>Helps us understand visits and improve Cubbes.</span>
167                </div>
168                <input id="cookieAnalyticsConsent" type="checkbox" />
169              </label>
170              <div class="cookie-consent__actions cookie-consent__actions--settings">
171                <button class="btn btn-outline-primary" type="button" data-consent-back>Back</button>
172                <button class="btn btn-primary" type="button" data-consent-save>Save choices</button>
173              </div>
174            </div>
175          </div>
176        </div>
177        <button class="cookie-settings-button" type="button" hidden>Privacy choices</button>
178      `,
179    );
180
181    modal = document.querySelector(".cookie-consent");
182    settingsButton = document.querySelector(".cookie-settings-button");
183    analyticsToggle = document.getElementById("cookieAnalyticsConsent");
184    settingsPanel = modal.querySelector("[data-consent-settings]");
185
186    modal
187      .querySelector(".cookie-consent__close")
188      .addEventListener("click", closeWithNecessaryOnly);
189    modal
190      .querySelector("[data-consent-customise]")
191      .addEventListener("click", showSettingsPanel);
192    modal
193      .querySelector("[data-consent-reject]")
194      .addEventListener("click", () => saveConsent(false));
195    modal
196      .querySelector("[data-consent-accept]")
197      .addEventListener("click", () => saveConsent(true));
198    modal
199      .querySelector("[data-consent-back]")
200      .addEventListener("click", showMainPanel);
201    modal
202      .querySelector("[data-consent-save]")
203      .addEventListener("click", () => saveConsent(analyticsToggle.checked));
204    modal.addEventListener("keydown", keepFocusInside);
205    settingsButton.addEventListener("click", openModal);
206
207    document.querySelectorAll("[data-cookie-settings]").forEach((button) => {
208      button.addEventListener("click", (event) => {
209        event.preventDefault();
210        openModal();
211      });
212    });
213
214    if (consent) {
215      settingsButton.hidden = false;
216    } else {
217      clearAnalyticsStorage();
218      openModal();
219    }
220  }
221
222  window.CubbesConsent = {
223    hasAnalyticsConsent: () => Boolean(consent?.analytics),
224    open: () => openModal(),
225  };
226
227  if (document.readyState === "loading") {
228    document.addEventListener("DOMContentLoaded", render);
229  } else {
230    render();
231  }
232})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.