/* ==========================================================================
   Augusta Pediatric Associates — headings rise a line at a time

   The theme's "slide type" heading component ships three styles, chosen per
   block by `data-style`:

     style1  the whole entry rises as one piece, out of the mask the theme
             already puts around it
     style2  GSAP SplitText splits the entry into WORDS and each word rises
             from y:200%, 150ms apart
     style3  the same, per character

   Every heading on this site that animates uses style2, so the copy arrives
   one word at a time — which is the thing to fix. Brayden wants whole lines
   coming up from underneath, one after the next.

   Rather than edit the theme's minified bundle, this file claims the style2
   blocks before the theme's own dispatcher reaches them and animates them
   itself. The claim is the same flag the dispatcher sets on anything it has
   already handled:

       !0 !== t.data("thb-animated") && (t.data("thb-animated", !0), …)

   so setting `data-thb-animated="true"` up front makes the theme skip the
   block entirely — no double animation, and nothing to unwind. jQuery reads
   the attribute through its own data store and coerces "true" to true, so the
   attribute alone is enough whether or not jQuery has booted yet.

   THIS MUST RUN BEFORE THE THEME'S READY HANDLER. It is a plain script at the
   end of <body>, which executes during parse and so beats DOMContentLoaded,
   where the theme binds and immediately fires its first pass. Deferring this
   file, or moving it into a ready handler of its own, hands the blocks back to
   the theme and the words return.

   style1 and style3 blocks are left alone. style1 is already a whole-entry
   rise, which is not what was complained about, and nothing here uses style3.

   Lines are grouped the same way redesign.js groups the service blurbs — by
   asking the browser where it put its own breaks — rather than with SplitText,
   which is a licensed GSAP plugin whose presence is the theme's business and
   not something to depend on.
   ========================================================================== */

(function () {
  'use strict';

  var SEL      = '.thb-slidetype[data-style="style2"]';
  var STAGGER  = 0.13;   /* seconds between one line and the next */
  var DURATION = 0.7;    /* seconds for a single line to arrive   */

  /* Claimed synchronously, at parse time, before anything is measured: the
     only job that has a deadline. Everything else can wait for layout. */
  var blocks = document.querySelectorAll(SEL);
  if (!blocks.length) return;

  for (var b = 0; b < blocks.length; b++) {
    blocks[b].setAttribute('data-thb-animated', 'true');
  }

  function injectStyle() {
    if (document.getElementById('apa-slidetype-lines')) return;

    var css =
      /* The theme parks the whole block at visibility:hidden and lets its own
         timeline reveal it. We took the block off it, so we reveal it — but
         only once the lines are grouped and sitting below their masks, or the
         heading would flash complete for a frame first. */
      '.thb-slidetype.apa-st { visibility: visible; }' +

      /* The theme's own mask is around the entry, which is the whole heading.
         Each line needs one of its own. */
      '.thb-slidetype.apa-st .apa-st-line {' +
        'display: block; overflow: hidden;' +
      '}' +

      '.thb-slidetype.apa-st .apa-st-in {' +
        'display: block;' +
        'transform: translateY(100%);' +
        'transition: transform ' + DURATION + 's cubic-bezier(.22, 1, .36, 1);' +
      '}' +

      '.thb-slidetype.apa-st.apa-st-seen .apa-st-in {' +
        'transform: none;' +
        'transition-delay: calc(var(--apa-i, 0) * ' + STAGGER + 's);' +
      '}' +

      /* Regrouping on resize rewrites the spans; without this the rebuilt
         lines would slide up from scratch under a heading already read. */
      '.thb-slidetype.apa-st-still .apa-st-in { transition: none; }' +

      '@media (prefers-reduced-motion: reduce) {' +
        '.thb-slidetype.apa-st .apa-st-in {' +
          'transform: none; transition: none;' +
        '}' +
      '}';

    var el = document.createElement('style');
    el.id = 'apa-slidetype-lines';
    el.textContent = css;
    (document.head || document.documentElement).appendChild(el);
  }

  function escapeHtml(s) {
    return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  }

  /* The element that actually holds the words. The theme wraps the text in a
     `.lines` span inside the entry — that is the hook style1 animates — and
     falls back to the entry itself where the wrapper is absent. */
  function target(entry) {
    return entry.querySelector('.lines') || entry;
  }

  /* Rebuild into flat word spans, ask the browser where the breaks fell, then
     rebuild again into one masked box per line.

     offsetTop is the grouping key: every word on a visual line shares it. The
     tolerance is half a word's height, so anything riding slightly off the
     baseline does not start a line of its own.

     The source text is kept on the node because a regroup has to start from
     flat words again — regrouping grouped markup just re-measures the previous
     answer, and the breaks move whenever the heading's width does. */
  function groupLines(el) {
    var text = el.getAttribute('data-apa-text');

    if (text === null) {
      text = el.textContent.trim().replace(/\s+/g, ' ');
      el.setAttribute('data-apa-text', text);
    }
    if (!text) return 0;

    var words = text.split(/\s+/);

    el.innerHTML = words.map(function (word) {
      return '<span class="apa-st-word">' + escapeHtml(word) + '</span>';
    }).join(' ');

    var spans = el.querySelectorAll('.apa-st-word');
    if (!spans.length) return 0;

    var tol   = Math.max(4, spans[0].offsetHeight / 2);
    var lines = [];
    var top   = null;

    for (var i = 0; i < spans.length; i++) {
      var t = spans[i].offsetTop;

      if (top === null || Math.abs(t - top) > tol) {
        lines.push([]);
        top = t;
      }
      lines[lines.length - 1].push(spans[i].textContent);
    }

    /* joined with a space, not butted together: the line spans are block boxes
       so it renders nothing, but without it textContent runs the last word of
       each line into the first of the next — what a screen reader announces
       and what a reader copies */
    el.innerHTML = lines.map(function (line, i) {
      return '<span class="apa-st-line" style="--apa-i:' + i + '">' +
             '<span class="apa-st-in">' + escapeHtml(line.join(' ')) +
             '</span></span>';
    }).join(' ');

    return lines.length;
  }

  /* Line indices run across the whole block, not per entry: a heading split
     into two entries should still count 0,1,2,3 down the page rather than
     restarting, or the second entry would arrive alongside the first. */
  function build(block) {
    var entries = block.querySelectorAll('.thb-slidetype-entry');
    var n = 0;

    for (var i = 0; i < entries.length; i++) {
      var el = target(entries[i]);
      var made = groupLines(el);

      if (!made) continue;

      var lines = el.querySelectorAll('.apa-st-line');
      for (var k = 0; k < lines.length; k++) {
        lines[k].style.setProperty('--apa-i', n++);
      }
    }

    block.classList.add('apa-st');

    /* Commit the resting state before anybody can change it.

       A transition fires on a change between two computed values, so both have
       to be computed. The lines are created and the reveal class can land in
       the same rendering step — an IntersectionObserver callback is delivered
       during that step, not after it — and then the browser only ever computes
       the finished position. No change, no transition: the heading simply
       appears, which is exactly what "it isn't doing anything" looks like.

       Reading a layout value forces the style recalculation now, so
       translateY(100%) is on record and the reveal has something to animate
       away from. Cheap, and it has to happen here rather than in the caller —
       a regroup re-creates the spans too. */
    if (n > 0) {
      var first = block.querySelector('.apa-st-in');
      if (first) void first.offsetHeight;
    }

    return n > 0;
  }

  function onScreen(el) {
    var r = el.getBoundingClientRect();
    var h = window.innerHeight || document.documentElement.clientHeight;

    return r.bottom > 0 && r.top < h * 0.9;
  }

  /* Grouped first, revealed on approach — the same moment the theme would
     have used, so headings below the fold still animate when they are reached
     rather than having already happened.

     A heading that never reveals is not a missing animation, it is a blank
     space: the lines sit inside their masks, so "not yet" and "gone" look the
     same. So the observer gets a backstop. If no callback has arrived at all
     after a second — which is what happens in a tab that is never painted, and
     would happen anywhere else the observer is stubbed out — the same test is
     run by hand, on scroll, for the rest of the page's life. Cheap, and it
     keeps the reveal tied to arrival rather than just dumping every heading on
     screen at once. */
  function watch(block) {
    var done = false;

    function reveal() {
      if (done) return;
      done = true;

      /* Commit the resting position one more time, in this task, immediately
         before the class that moves it.

         build() already did this, but reveal can arrive from three different
         places — the observer, the scroll backstop, a regroup — and only this
         placement is unconditional. The cost is one style recalculation on a
         span; the failure it prevents is the heading appearing with no
         animation at all, because the browser never computed a "from". */
      var first = block.querySelector('.apa-st-in');
      if (first) void getComputedStyle(first).transform;

      block.classList.add('apa-st-seen');
      window.removeEventListener('scroll', onScroll);
    }

    function onScroll() {
      if (onScreen(block)) reveal();
    }

    if (!('IntersectionObserver' in window)) {
      window.addEventListener('scroll', onScroll, { passive: true });
      onScroll();
      return;
    }

    var heard = false;

    var io = new IntersectionObserver(function (entries) {
      heard = true;
      for (var i = 0; i < entries.length; i++) {
        if (!entries[i].isIntersecting) continue;
        io.disconnect();
        reveal();
      }
    }, { rootMargin: '0px 0px -10% 0px' });

    io.observe(block);

    setTimeout(function () {
      if (heard || done) return;
      io.disconnect();
      window.addEventListener('scroll', onScroll, { passive: true });
      onScroll();
    }, 1000);
  }

  var built = [];

  function boot() {
    injectStyle();

    for (var i = 0; i < blocks.length; i++) {
      if (!build(blocks[i])) continue;
      built.push(blocks[i]);
      watch(blocks[i]);
    }

    if (!built.length) return;

    /* The webfont changes the metrics, so the breaks move under a heading that
       may already be grouped — and possibly already read. */
    if (document.fonts && document.fonts.ready) {
      document.fonts.ready.then(regroup);
    }

    var pending;
    window.addEventListener('resize', function () {
      clearTimeout(pending);
      pending = setTimeout(regroup, 150);
    });
  }

  function regroup() {
    for (var i = 0; i < built.length; i++) {
      var block = built[i];

      /* A heading that has already arrived must not slide in again behind the
         reader's back, so the rebuilt lines are pinned still for a frame and
         only then handed back their transition. */
      var seen = block.classList.contains('apa-st-seen');
      if (seen) block.classList.add('apa-st-still');

      build(block);

      if (!seen) continue;

      block.classList.add('apa-st-seen');
      /* two frames: one for the new spans to take the still rule, one to lift
         it — a single frame lands before the style has been applied */
      requestAnimationFrame(function (b) {
        return function () {
          requestAnimationFrame(function () { b.classList.remove('apa-st-still'); });
        };
      }(block));
    }
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', boot);
  } else {
    boot();
  }
})();
