ids theme: shrink overflowing slide headings onto one line (right reserve, level1 exempt)

Change-Id: I2279b0f3b3a1ae11cb39764b41fd8dccc76c0ce0
diff --git a/inst/rmarkdown/templates/revealjs_presentation/resources/default.html b/inst/rmarkdown/templates/revealjs_presentation/resources/default.html
index 4bb6834..e65dd40 100644
--- a/inst/rmarkdown/templates/revealjs_presentation/resources/default.html
+++ b/inst/rmarkdown/templates/revealjs_presentation/resources/default.html
@@ -745,6 +745,78 @@
 
 $if(theme-ids)$
 <script>
+  // IDS theme: long slide headings should shrink onto one line instead of
+  // wrapping (and pushing the orange bar taller or overflowing the slide).
+  // Slide content is laid out in fixed slide coordinates and only ever scaled
+  // by a transform, so overflow is independent of the window size and one fit
+  // pass holds everywhere. The Fira web fonts swap in asynchronously and can
+  // change text widths after reveal's initial layout, so refit when fonts and
+  // the document have finished loading too.
+  (function () {
+    var MIN_FACTOR = 0.55,   // never shrink a heading below 55% of its CD size
+        RIGHT_RESERVE = 0.05; // keep fitted titles this fraction of the slide
+                              // height off the right edge (and out of the logo)
+
+    function fitHeading(h) {
+      if (h.dataset.fitDone) return; // layout px don't change, fit once
+      if (!h.dataset.origFontSize) {
+        h.dataset.origFontSize = parseFloat(getComputedStyle(h).fontSize);
+      }
+      var orig = parseFloat(h.dataset.origFontSize);
+      // Slides beyond viewDistance are laid out lazily and have no overflow
+      // to measure yet (clientWidth 0); retry them once they enter layout.
+      if (!h.clientWidth) return;
+
+      var reveal = document.querySelector('.reveal');
+      var slideH = reveal && parseFloat(getComputedStyle(reveal).getPropertyValue('--slide-height')) || 720;
+      var scale = Reveal.getScale() || 1;
+      h.style.whiteSpace = 'nowrap';
+      // scrollWidth is useless here (clamped to the box width and blind to a
+      // right reserve), so measure the text itself: the range rect is in
+      // viewport coordinates, unscale into slide coordinates.
+      var range = document.createRange();
+      range.selectNodeContents(h);
+      var textW = range.getBoundingClientRect().width / scale;
+      var padLeft = parseFloat(getComputedStyle(h).paddingLeft) || 0;
+      var avail = h.clientWidth - padLeft - slideH * RIGHT_RESERVE;
+      if (textW > avail + 1) {
+        // text width is linear in font-size (letter-spacing included), so a
+        // single step fits exactly -- no iterative forced layouts
+        var size = Math.max(orig * (avail / textW) * 0.995, orig * MIN_FACTOR);
+        if (size < orig) h.style.fontSize = size + 'px';
+      }
+      h.style.whiteSpace = '';
+      h.dataset.fitDone = '1';
+    }
+
+    function fitSlideHeadings() {
+      // level1 section-divider slides deliberately stage their lone title
+      // large and centered; those may keep wrapping onto several lines.
+      document.querySelectorAll(
+        '.reveal .slides section:not(.title-frame):not(.level1) > h1:first-child, ' +
+        '.reveal .slides section:not(.title-frame) > h2:first-child, ' +
+        '.reveal .slides section:not(.title-frame) > h2:first-child + h3'
+      ).forEach(fitHeading);
+    }
+
+    function refitAll() { // fonts swapped in: widths changed, fit again
+      document.querySelectorAll('[data-fit-done]').forEach(function (h) {
+        if (h.dataset.origFontSize) h.style.fontSize = h.dataset.origFontSize + 'px';
+        delete h.dataset.fitDone;
+      });
+      fitSlideHeadings();
+    }
+
+    Reveal.on('ready', fitSlideHeadings);
+    Reveal.on('slidechanged', function () { requestAnimationFrame(fitSlideHeadings); });
+    window.addEventListener('load', fitSlideHeadings);
+    if (document.fonts && document.fonts.ready) document.fonts.ready.then(refitAll);
+  })();
+</script>
+$endif$
+
+$if(theme-ids)$
+<script>
   // IDS theme: in the bibliography, "Authors (Year):" should form a bold first
   // line, followed by a line break. Which part of an entry citeproc wraps in
   // spans/links depends on the entry type, so a CSS "break before first child