176 lines
5.0 KiB
JavaScript
176 lines
5.0 KiB
JavaScript
/* =========================================================================
|
|
age-gate.js — non-dismissible age verification interstitial.
|
|
The <html> class "age-gate-active" is set as early as possible by an inline
|
|
script in baseof.html to avoid a flash for returning (confirmed) visitors.
|
|
This module wires the confirm/exit buttons, syncs the displayed age with the
|
|
active locale (set by the locale switcher) and — because the dialog is
|
|
role="dialog" aria-modal="true" — manages focus: it moves focus into the
|
|
dialog, traps Tab/Shift+Tab, marks the rest of the page inert while active,
|
|
and restores focus on confirm.
|
|
|
|
Deliberately dependency-free (no Alpine, no eval/new Function): the gate is
|
|
driven purely by the CSS "age-gate-active" class, so it still fails safe
|
|
(gate shows, confirm works) even if Alpine never loads.
|
|
========================================================================= */
|
|
(function () {
|
|
"use strict";
|
|
|
|
var STORAGE_KEY = "ageConfirmed";
|
|
var INERT_SELECTORS = ["header", "main", "footer"];
|
|
var FOCUSABLE =
|
|
'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])';
|
|
|
|
var gate = null;
|
|
var lastFocused = null;
|
|
var trapActive = false;
|
|
|
|
function hasConfirmed() {
|
|
try {
|
|
return localStorage.getItem(STORAGE_KEY) === "1";
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function focusableIn(el) {
|
|
return Array.prototype.filter.call(
|
|
el.querySelectorAll(FOCUSABLE),
|
|
function (node) {
|
|
return (
|
|
node.offsetWidth > 0 ||
|
|
node.offsetHeight > 0 ||
|
|
node === document.activeElement
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
function onKeydown(e) {
|
|
if (!trapActive || e.key !== "Tab" || !gate) return;
|
|
var f = focusableIn(gate);
|
|
if (!f.length) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
var first = f[0];
|
|
var last = f[f.length - 1];
|
|
if (e.shiftKey && document.activeElement === first) {
|
|
e.preventDefault();
|
|
last.focus();
|
|
} else if (!e.shiftKey && document.activeElement === last) {
|
|
e.preventDefault();
|
|
first.focus();
|
|
}
|
|
}
|
|
|
|
function onFocusIn(e) {
|
|
if (trapActive && gate && !gate.contains(e.target)) {
|
|
var f = focusableIn(gate);
|
|
if (f.length) f[0].focus();
|
|
}
|
|
}
|
|
|
|
function setBackgroundInert(on) {
|
|
INERT_SELECTORS.forEach(function (sel) {
|
|
var el = document.querySelector(sel);
|
|
if (!el) return;
|
|
if (on) {
|
|
el.setAttribute("inert", "");
|
|
el.setAttribute("aria-hidden", "true");
|
|
} else {
|
|
el.removeAttribute("inert");
|
|
el.removeAttribute("aria-hidden");
|
|
}
|
|
});
|
|
}
|
|
|
|
function activateTrap() {
|
|
if (trapActive || !gate) return;
|
|
trapActive = true;
|
|
lastFocused = document.activeElement;
|
|
setBackgroundInert(true);
|
|
document.addEventListener("keydown", onKeydown, true);
|
|
document.addEventListener("focusin", onFocusIn);
|
|
var confirmBtn = gate.querySelector("[data-age-confirm]");
|
|
if (confirmBtn) {
|
|
// Defer to ensure the element is focusable after the first paint.
|
|
window.setTimeout(function () {
|
|
confirmBtn.focus();
|
|
}, 0);
|
|
}
|
|
}
|
|
|
|
function releaseTrap() {
|
|
trapActive = false;
|
|
setBackgroundInert(false);
|
|
document.removeEventListener("keydown", onKeydown, true);
|
|
document.removeEventListener("focusin", onFocusIn);
|
|
}
|
|
|
|
function restoreFocus() {
|
|
var target =
|
|
(lastFocused &&
|
|
document.contains(lastFocused) &&
|
|
lastFocused !== document.body &&
|
|
lastFocused) ||
|
|
document.querySelector("header a, header button") ||
|
|
document.getElementById("main");
|
|
if (!target) return;
|
|
if (target.id === "main" && !target.hasAttribute("tabindex")) {
|
|
target.setAttribute("tabindex", "-1");
|
|
}
|
|
try {
|
|
target.focus();
|
|
} catch (e) {
|
|
/* focus can fail on detached nodes — ignore */
|
|
}
|
|
}
|
|
|
|
function confirmAge() {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, "1");
|
|
} catch (e) {
|
|
/* storage unavailable — still allow through for this session */
|
|
}
|
|
document.documentElement.classList.remove("age-gate-active");
|
|
releaseTrap();
|
|
restoreFocus();
|
|
}
|
|
|
|
function exitSite() {
|
|
window.location.href = "https://www.google.com/";
|
|
}
|
|
|
|
function init() {
|
|
gate = document.getElementById("age-gate");
|
|
if (!gate) {
|
|
document.documentElement.classList.remove("age-gate-active");
|
|
return;
|
|
}
|
|
|
|
var confirmBtn = gate.querySelector("[data-age-confirm]");
|
|
if (confirmBtn) {
|
|
confirmBtn.addEventListener("click", confirmAge);
|
|
}
|
|
|
|
var exitBtn = gate.querySelector("[data-age-exit]");
|
|
if (exitBtn) {
|
|
exitBtn.addEventListener("click", exitSite);
|
|
}
|
|
|
|
if (hasConfirmed()) {
|
|
document.documentElement.classList.remove("age-gate-active");
|
|
setBackgroundInert(false);
|
|
} else {
|
|
document.documentElement.classList.add("age-gate-active");
|
|
activateTrap();
|
|
}
|
|
}
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|