/**
* @file Behavior for the secondary pages.
*
* The secondary pages (pricing, for professionals, the legal pages, about
* this site and the 404 page) carry the shared icon sprite, header and footer
* as real HTML: `scripts/build-partials.js` writes them in from
* `src/partials/`, and marks the footer link to the current page there too.
* This script adds only what that markup can't do on its own:
* 1. runs the mobile-menu open/close button (a trimmed copy of the handler in
* `main.js`, which secondary pages don't load);
* 2. slides the header away while the footer is on screen;
* 3. turns "Return to the homepage" into "Go back" for visitors who came
* straight from the homepage;
* 4. keeps table semantics on tables that CSS restacks on phones.
*
* A classic (non-module) `<script>` at the end of `<body>`, after the markup
* it wires. ES5 inside an IIFE so nothing leaks globally.
*
* @module secondary
*/
(function () { // start a private scope that runs immediately
'use strict'; // opt into strict-mode JavaScript
// -------------------------------------------------------------------------
// Mobile navigation disclosure
// -------------------------------------------------------------------------
// A trimmed copy of the handler in main.js, which the secondary pages don't
// load. The button's aria-expanded is the single source of truth for state.
/** The hamburger button. @type {HTMLElement|null} */
var navToggle = document.getElementById('nav-toggle'); // find the hamburger button
/** The mobile nav panel it controls. @type {HTMLElement|null} */
var mobileNav = document.getElementById('mobile-nav'); // find the mobile nav panel
if (navToggle && mobileNav) { // only wire the menu if both exist
/**
* Opens or closes the mobile menu and updates the button to match.
*
* Opening: un-hide the panel first, then add `is-open` on the next frame
* so the CSS entrance transition has a starting state to animate from.
* Closing: hide immediately.
*
* @param {boolean} open `true` to open, `false` to close.
* @returns {void}
*/
var setNav = function (open) { // define setNav(open)
navToggle.setAttribute('aria-expanded', String(open)); // tell assistive tech whether the menu is open
navToggle.setAttribute('aria-label', open ? 'Close menu' : 'Open menu'); // update the button's accessible name
navToggle.setAttribute('title', open ? 'Close menu' : 'Open menu'); // and its tooltip
if (open) { // opening:
mobileNav.hidden = false; // un-hide the panel
mobileNav.classList.remove('hidden'); // remove the utility class that hides it
requestAnimationFrame(function () { mobileNav.classList.add('is-open'); }); // next frame, add is-open so the entrance transition runs
} else { // closing:
mobileNav.classList.remove('is-open'); // drop the open state
mobileNav.hidden = true; // hide the panel
mobileNav.classList.add('hidden'); // re-add the utility class that hides it
}
};
// Button toggles the menu.
navToggle.addEventListener('click', function () { // when the menu button is clicked:
setNav(navToggle.getAttribute('aria-expanded') !== 'true'); // flip the menu to the opposite state
});
// Choosing a link inside the menu closes it.
mobileNav.addEventListener('click', function (e) { // when anything in the menu is clicked:
if (e.target.closest('a')) setNav(false); // if it was a link, close the menu
});
// Escape closes an open menu and returns focus to the button, so keyboard
// users don't lose their place.
document.addEventListener('keydown', function (e) { // on any key press in the page:
if (e.key === 'Escape' && navToggle.getAttribute('aria-expanded') === 'true') { // if it's Escape and the menu is open:
setNav(false); // close the menu
navToggle.focus(); // put focus back on the menu button
}
});
// Close the menu once the desktop nav takes over: widening the window past
// the mobile breakpoint (930px, the Tailwind `nav:` screen), or leaving the
// Larger / Largest text size on a wide window (those keep the mobile menu
// at any width).
var wide = window.matchMedia('(min-width: 930px)'); // the desktop-nav width
function closeIfDesktop() { // close the menu if the desktop nav is showing
if (wide.matches && !/^(xl|xxl)$/.test(document.documentElement.getAttribute('data-text') || '')) setNav(false);
}
wide.addEventListener('change', closeIfDesktop); // window crossed 930px
document.addEventListener('cha:prefs', closeIfDesktop); // text size changed
}
// -------------------------------------------------------------------------
// Hide the header while the footer is visible
// -------------------------------------------------------------------------
// Slide the header out of the way once the footer scrolls into view, and
// bring it back when the footer leaves — the same gesture main.js gives
// index.html (keyed there to the closing #schedule section). The fade and
// transform live in CSS: .site-header / .site-header.header-hidden.
// Skipped in browsers without IntersectionObserver (the header just stays).
var siteHeader = document.querySelector('.site-header'); // find the header
var siteFooter = document.querySelector('.site-footer'); // find the footer
if (siteHeader && siteFooter && 'IntersectionObserver' in window) { // only if both exist and the browser supports IntersectionObserver
new IntersectionObserver( // create an observer that…
function (entries) { // …whenever the footer's visibility changes…
siteHeader.classList.toggle('header-hidden', entries[0].isIntersecting); // …hides the header while the footer is on screen, shows it otherwise
},
{ threshold: 0 } // fire as soon as any pixel of the footer is visible
).observe(siteFooter); // start watching the footer
}
// -------------------------------------------------------------------------
// "Return to the homepage" → "Go back" for visitors who came from it
// -------------------------------------------------------------------------
// Each page's closing link is authored as
// <a class="link-quiet" href="index.html" data-back-link>Return to the homepage</a>
// If the page before this one in the tab's history is the homepage, the link
// becomes "← Go back" and steps back through history instead, so the browser
// returns the visitor to the spot they had scrolled to. Visitors who arrived
// any other way (a search result, a bookmark, a new tab) keep the original
// link and wording.
/**
* Whether a URL path is the homepage: `/`, `/index.html`, or `/index`
* (Netlify's pretty URL).
*
* @param {string} path A URL pathname.
* @returns {boolean} `true` for the homepage.
*/
var isHome = function (path) { // define isHome(path)
return /^\/(index(\.html)?)?$/.test(path); // match the three homepage paths exactly
};
/**
* Whether this page was opened from the homepage, according to the
* referrer. The site's Referrer-Policy (strict-origin-when-cross-origin)
* sends the full URL for same-origin navigations.
* @type {boolean}
*/
var referredFromHome = (function () { // work it out once, now
try { // an empty or odd referrer makes URL() throw
var ref = new URL(document.referrer); // parse the referring URL
return ref.origin === location.origin && isHome(ref.pathname); // same site, and the homepage?
} catch (e) { // no usable referrer
return false; // not from the homepage
}
})();
/**
* History length when this page loaded; the fallback below uses it to count
* entries added since (e.g. by table-of-contents `#` links).
* @type {number}
*/
var startLength = history.length; // entries in this tab's history at load
/**
* Works out how many steps back through history the homepage is, if the
* visitor reached this page straight from it.
*
* Where the Navigation API exists, it reads the tab's real history: it
* skips back over entries for this same page (in-page `#` jumps), then
* checks that the entry before them is the homepage. Elsewhere it falls
* back to the referrer, counting the entries added since this page loaded.
*
* @returns {number} Steps to go back (1 or more), or `0` if the previous
* page wasn't the homepage.
*/
var stepsToHome = function () { // define stepsToHome()
var nav = window.navigation; // the Navigation API, if supported
if (nav && nav.currentEntry && typeof nav.entries === 'function') { // supported:
var entries = nav.entries(); // this tab's same-site history entries
var current = nav.currentEntry.index; // where we are in that list
var here = location.pathname; // this page's path
var i = current; // start from the current entry…
while (i > 0 && entries[i - 1].url && new URL(entries[i - 1].url).pathname === here) i--; // …and step back over entries for this same page
if (i === 0 || !entries[i - 1].url) return 0; // nothing before this page (or unreadable): not from the homepage
return isHome(new URL(entries[i - 1].url).pathname) ? current - (i - 1) : 0; // the page before is home: that many steps back
}
if (!referredFromHome || startLength < 2) return 0; // fallback: not referred by home, or no page before this one
return history.length - startLength + 1; // one step for this page, plus any entries added since it loaded
};
// Only rewrite the links when the homepage really is one step (plus any
// same-page jumps) behind this page.
if (stepsToHome() > 0) { // came straight from the homepage:
document.querySelectorAll('a[data-back-link]').forEach(function (a) { // for each homepage link on the page:
a.textContent = ''; // clear "Return to the homepage"
// Leading left arrow from the icon sprite, hidden from screen readers.
a.insertAdjacentHTML('afterbegin', '<svg class="icon icon-back" aria-hidden="true"><use href="#i-arrow-left"/></svg>');
a.appendChild(document.createTextNode('Go back')); // the new visible label
a.setAttribute('aria-label', 'Go back to the homepage'); // fuller name for screen readers (starts with the visible text)
a.addEventListener('click', function (e) { // when it's clicked:
// Leave modified clicks (new tab, new window) to open the homepage normally.
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
var steps = stepsToHome(); // recount, in case history changed since load
if (!steps) return; // homepage no longer behind us: follow the link instead
e.preventDefault(); // don't load the homepage fresh
history.go(-steps); // go back to it, where the browser restores the scroll position
});
});
}
// -------------------------------------------------------------------------
// Keep table semantics on restacked tables
// -------------------------------------------------------------------------
// On phones the pricing table's rows and cells are set to display:block so
// each service reads as a card. Some browsers then stop announcing it as a
// table. Explicit ARIA roles restore that, but HTML doesn't allow authoring
// them on table elements, so they're added here instead.
/**
* ARIA role for each table part, keyed by tag name. Header cells are
* handled separately, since their role depends on `scope`.
* @type {Object<string, string>}
*/
var TABLE_ROLES = { TABLE: 'table', THEAD: 'rowgroup', TBODY: 'rowgroup', TFOOT: 'rowgroup', TR: 'row', TD: 'cell' }; // tag → role
document.querySelectorAll('table.price-table').forEach(function (table) { // for each restacked table:
table.querySelectorAll('thead, tbody, tfoot, tr, th, td').forEach(function (el) { // for each part of it:
var scope = el.getAttribute('scope'); // a header cell's scope (col, row or rowgroup)
var role = el.tagName === 'TH' ? (scope === 'col' ? 'columnheader' : 'rowheader') : TABLE_ROLES[el.tagName]; // pick its role
if (role) el.setAttribute('role', role); // and set it
});
table.setAttribute('role', 'table'); // the table itself last, once its parts carry roles
});
})(); // end of the private scope; run it now