Skip to content
Coastal Healthcare Advocates

Source: secondary.js

/**
 * @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