/* ==========================================================================
   Vantera — behaviour
   --------------------------------------------------------------------------
   Four independent blocks. Each one starts by looking for its elements and
   returns early when they are missing, so any section of the HTML can be
   deleted without breaking the rest.

     1. revealOnScroll   play a reveal when a block enters the screen
     2. navigation       burger menu and the "Pages" dropdown
     3. filterPickers    the three hero filters, and only one open at a time
     4. reviewsCarousel  arrows, disabled ends and the spoken position

   Nothing here is required to read the page: every hidden state lives under
   `html.js`, a class added by enhance.js.
   ========================================================================== */

(function () {
  "use strict";

  var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  /* ========================================================================
     1. Reveal on scroll

     Why a negative rootMargin instead of a threshold: a threshold is a
     percentage OF THE ELEMENT, so a block taller than the screen — the dark
     about panel, a full-width banner — would never reach it and would stay
     hidden forever. A margin is measured on the screen, so it holds at every
     size.

     The 15% bottom margin means a block starts its reveal once it has come a
     good sixth of the way up, not at its very first pixel: it is seen playing
     instead of being already over. The reference triggers at the first pixel,
     which is the one thing about its motion worth changing.
     ======================================================================== */

  function revealOnScroll() {
    var targets = document.querySelectorAll("[data-reveal]");
    if (!targets.length) return;

    if (reduceMotion || !("IntersectionObserver" in window)) {
      Array.prototype.forEach.call(targets, function (el) {
        el.classList.add("is-in");
      });
      return;
    }

    var observer = new IntersectionObserver(
      function (entries) {
        entries.forEach(function (entry) {
          if (!entry.isIntersecting) return;
          entry.target.classList.add("is-in");
          observer.unobserve(entry.target);
        });
      },
      { rootMargin: "0px 0px -15% 0px", threshold: 0 }
    );

    Array.prototype.forEach.call(targets, function (el) {
      observer.observe(el);
    });

    // Safety net: whatever is still hidden when the bottom of the page is
    // reached gets revealed. Without it, a block sitting in the last 15% of
    // the document could never satisfy the margin above.
    var onScroll = function () {
      if (window.scrollY + window.innerHeight < document.documentElement.scrollHeight - 4) return;
      Array.prototype.forEach.call(targets, function (el) {
        el.classList.add("is-in");
      });
      window.removeEventListener("scroll", onScroll);
    };
    window.addEventListener("scroll", onScroll, { passive: true });
  }

  /* ========================================================================
     2. Navigation

     The burger collapses the menu below 992px. The "Pages" entry opens on
     hover on a pointer device and on click everywhere else.

     A hover menu needs two delays, not one: a short one on the way in so a
     cursor crossing the label does not flash the panel open, and a longer one
     on the way out that covers the trip across the empty gap between the
     label and the panel.
     ======================================================================== */

  function navigation() {
    var toggle = document.querySelector("[data-nav-toggle]");
    var nav = document.querySelector("[data-nav]");
    if (!toggle || !nav) return;

    toggle.addEventListener("click", function () {
      var open = nav.classList.toggle("is-open");
      toggle.setAttribute("aria-expanded", open ? "true" : "false");
      toggle.setAttribute("aria-label", open ? "Close menu" : "Open menu");
    });

    // Following a link closes the menu: every href on this page is an anchor,
    // so the page never reloads and the panel would stay open over it.
    nav.addEventListener("click", function (event) {
      if (!event.target.closest("a")) return;
      nav.classList.remove("is-open");
      toggle.setAttribute("aria-expanded", "false");
      toggle.setAttribute("aria-label", "Open menu");
    });

    var dropdowns = document.querySelectorAll("[data-dropdown]");
    Array.prototype.forEach.call(dropdowns, function (item) {
      var button = item.querySelector("[data-dropdown-toggle]");
      if (!button) return;
      var timer = null;

      var open = function (state) {
        item.classList.toggle("is-open", state);
        button.setAttribute("aria-expanded", state ? "true" : "false");
      };

      button.addEventListener("click", function () {
        open(!item.classList.contains("is-open"));
      });

      var hoverable = window.matchMedia("(hover: hover) and (min-width: 992px)");
      item.addEventListener("mouseenter", function () {
        if (!hoverable.matches) return;
        window.clearTimeout(timer);
        timer = window.setTimeout(function () {
          open(true);
        }, 80);
      });
      item.addEventListener("mouseleave", function () {
        if (!hoverable.matches) return;
        window.clearTimeout(timer);
        timer = window.setTimeout(function () {
          open(false);
        }, 260);
      });

      item.addEventListener("keydown", function (event) {
        if (event.key !== "Escape") return;
        open(false);
        button.focus();
      });
    });
  }

  /* ========================================================================
     3. Filter pickers

     The three hero filters are native <details>: they already open with a
     click and with the keyboard, script or no script. Two things are added
     here.

     First, the opening animation. A closed <details> is taken out of
     rendering, so a keyframe or a @starting-style would only ever play once.
     Adding a class one frame after the element opens gives the panel a start
     state to move away from, every time.

     Second, only one panel at a time: three overlapping menus on a 1210px row
     is not a menu, it is a stack.
     ======================================================================== */

  function filterPickers() {
    var pickers = document.querySelectorAll("[data-picker]");
    if (!pickers.length) return;

    Array.prototype.forEach.call(pickers, function (picker) {
      picker.addEventListener("toggle", function () {
        if (!picker.open) {
          picker.classList.remove("is-open");
          return;
        }
        Array.prototype.forEach.call(pickers, function (other) {
          if (other !== picker) {
            other.open = false;
            other.classList.remove("is-open");
          }
        });
        // One frame later: the panel needs its start state painted first.
        window.requestAnimationFrame(function () {
          picker.classList.add("is-open");
        });
      });

      // Picking a value writes it back on the toggle, the way a real filter
      // would, and closes the panel.
      picker.addEventListener("click", function (event) {
        var option = event.target.closest(".picker__option");
        if (!option) return;
        var label = picker.querySelector(".picker__label");
        if (label) label.textContent = option.textContent;
        picker.open = false;
      });
    });

    document.addEventListener("click", function (event) {
      if (event.target.closest("[data-picker]")) return;
      Array.prototype.forEach.call(pickers, function (picker) {
        picker.open = false;
      });
    });

    document.addEventListener("keydown", function (event) {
      if (event.key !== "Escape") return;
      Array.prototype.forEach.call(pickers, function (picker) {
        picker.open = false;
      });
    });
  }

  /* ========================================================================
     4. Reviews carousel

     The track is a scroll-snap container: it slides, it swipes and it takes
     the arrow keys on its own. What is added here is the pair of buttons, the
     disabled state at both ends, and the position spoken to a screen reader.
     ======================================================================== */

  function reviewsCarousel() {
    var track = document.querySelector("[data-carousel]");
    var prev = document.querySelector("[data-carousel-prev]");
    var next = document.querySelector("[data-carousel-next]");
    var status = document.querySelector("[data-carousel-status]");
    if (!track || !prev || !next) return;

    var cards = track.children;
    if (!cards.length) return;

    var step = function () {
      // Card width plus the gap, read from the layout rather than hard-coded:
      // the card narrows below 480px and the step has to follow.
      if (cards.length < 2) return cards[0].getBoundingClientRect().width;
      return cards[1].getBoundingClientRect().left - cards[0].getBoundingClientRect().left;
    };

    var update = function () {
      var max = track.scrollWidth - track.clientWidth;
      prev.disabled = track.scrollLeft <= 1;
      next.disabled = track.scrollLeft >= max - 1;
      if (!status) return;
      var index = Math.round(track.scrollLeft / step()) + 1;
      status.textContent = "Review " + Math.min(index, cards.length) + " of " + cards.length;
    };

    var scrollBy = function (direction) {
      track.scrollBy({ left: direction * step(), behavior: reduceMotion ? "auto" : "smooth" });
    };

    prev.addEventListener("click", function () {
      scrollBy(-1);
    });
    next.addEventListener("click", function () {
      scrollBy(1);
    });
    track.addEventListener("scroll", update, { passive: true });
    window.addEventListener("resize", update);
    update();
  }

  revealOnScroll();
  navigation();
  filterPickers();
  reviewsCarousel();
})();
