ids theme: adaptive vertical centering of slide content
Slides with little content look lost when everything is glued below the
title bar, while packed slides must start at the top. reveal.js' own
center option cannot express this -- it also vertically centers the
headings.
Instead the template groups each slide's body content (everything after
the heading block except speaker notes) into a div.ids-slide-content,
and the theme turns such slides into flex columns of full slide height:
the heading block stays pinned to the top and the wrapper's auto margins
center the content in the remaining space. Auto margins collapse to zero
when there is no free space, so packed slides still start directly below
the heading and overflow downwards exactly as before -- no measuring and
no resize handling involved. The wrapper also reserves the footer band
(--ids-footer-reserve, published from the actual footer height in slide
coordinates), so centered content never covers the footer; slides too
packed to fit above it may, as they always could.
Heading-only section-divider slides keep their dedicated centered-title
layout, the transcript view treats the wrapper transparently, and the
new rules are screen-scoped so the pdf export keeps laying out slides
itself.
Change-Id: Iccfbebebbf18ec860c1ccd950043a84f638c33da
diff --git a/inst/reveal.js-5.2.1/css/theme/source/ids.scss b/inst/reveal.js-5.2.1/css/theme/source/ids.scss
index e833b8f..f431662 100644
--- a/inst/reveal.js-5.2.1/css/theme/source/ids.scss
+++ b/inst/reveal.js-5.2.1/css/theme/source/ids.scss
@@ -366,6 +366,44 @@
background: #{$idsOrange} !important;
}
+// Adaptive vertical centering of slide content: the template groups each
+// slide's body content (everything after the heading block except speaker
+// notes) into a div.ids-slide-content. Slides carrying such a wrapper become
+// flex columns of full slide height: the heading block stays pinned to the
+// top and the wrapper's auto margins center the content in the remaining
+// space. Auto margins collapse to zero when there is no free space, so
+// packed slides still start directly below the heading and overflow
+// downwards as before -- the drawback that makes reveal.js' own `center`
+// option unusable here (it would also center the headings). The wrapper
+// keeps the footer band free (--ids-footer-reserve, published by the
+// template from the actual footer height), so centered content never
+// covers the footer; slides too packed to fit above it may, as they always
+// could. Display only ever changes for slides reveal.js is actually
+// showing, mirroring the section-divider rule above, which works the same
+// way; screen-only, so the pdf export keeps laying out slides itself.
+@media screen {
+ html .reveal .slides section:not(.stack):not(.title-frame) {
+ height: 100% !important;
+ display: flex !important;
+ flex-direction: column !important;
+ }
+
+ .reveal .slides section:not(.stack):not(.title-frame) > .ids-slide-content {
+ flex: none;
+ margin-top: auto !important;
+ margin-bottom: auto !important;
+ padding-bottom: var(--ids-footer-reserve, 48px);
+
+ // under block layout the first content element's margin-top collapsed
+ // with the heading's margin-bottom; as flex items the two would add up
+ // instead, so drop it and let the heading spacing rule alone define
+ // the gap, as before
+ > :first-child {
+ margin-top: 0 !important;
+ }
+ }
+}
+
.reveal .slides section.level1:not(.title-frame) > h1:first-child {
text-align: center !important;
margin: 0 !important;
diff --git a/inst/reveal.js-5.2.1/dist/theme/ids.css b/inst/reveal.js-5.2.1/dist/theme/ids.css
index dd54e86..8d56808 100644
--- a/inst/reveal.js-5.2.1/dist/theme/ids.css
+++ b/inst/reveal.js-5.2.1/dist/theme/ids.css
@@ -523,6 +523,40 @@
content: none !important;
}
+/* Adaptive vertical centering of slide content: the template groups each
+ slide's body content (everything after the heading block except speaker
+ notes) into a div.ids-slide-content. Slides carrying such a wrapper become
+ flex columns of full slide height: the heading block stays pinned to the
+ top and the wrapper's auto margins center the content in the remaining
+ space. Auto margins collapse to zero when there is no free space, so
+ packed slides still start directly below the heading and overflow
+ downwards as before -- the drawback that makes reveal.js' own `center`
+ option unusable here (it would also center the headings). The wrapper
+ keeps the footer band free (--ids-footer-reserve, published by the
+ template from the actual footer height), so centered content never
+ covers the footer; slides too packed to fit above it may, as they always
+ could. Display only ever changes for slides reveal.js is actually
+ showing, mirroring the section-divider rule above, which works the same
+ way; screen-only, so the pdf export keeps laying out slides itself. */
+@media screen {
+ html .reveal .slides section:not(.stack):not(.title-frame) {
+ height: 100% !important;
+ display: flex !important;
+ flex-direction: column !important;
+ }
+
+ .reveal .slides section:not(.stack):not(.title-frame) > .ids-slide-content {
+ flex: none;
+ margin-top: auto !important;
+ margin-bottom: auto !important;
+ padding-bottom: var(--ids-footer-reserve, 48px);
+ }
+
+ .reveal .slides section:not(.stack):not(.title-frame) > .ids-slide-content > :first-child {
+ margin-top: 0 !important;
+ }
+}
+
:root {
--r-bold-color: #f6a800;
--r-list-bullet-color: #f6a800;
diff --git a/inst/rmarkdown/templates/revealjs_presentation/resources/default.html b/inst/rmarkdown/templates/revealjs_presentation/resources/default.html
index 45b55e4..f5eb25b 100644
--- a/inst/rmarkdown/templates/revealjs_presentation/resources/default.html
+++ b/inst/rmarkdown/templates/revealjs_presentation/resources/default.html
@@ -1177,6 +1177,72 @@
})();
</script>
<script>
+ // Adaptive vertical centering of slide content: slides with little content
+ // look lost when everything is glued below the title bar, while packed
+ // slides must start at the top. Reveal.js' own `center` option cannot do
+ // this -- it also vertically centers the headings. Instead, each slide's
+ // body content (everything after the heading block, except speaker notes)
+ // is grouped into a .ids-slide-content wrapper here, and the ids theme
+ // turns these slides into flex columns: the heading stays at the top, and
+ // the wrapper's auto margins center it in the remaining space. Auto
+ // margins collapse to zero when there is no free space, so packed slides
+ // still start directly below the heading and overflow downwards exactly
+ // as before. The wrapper also reserves the footer band (--ids-footer-
+ // reserve, published below from the actual footer height), so centered
+ // content never reaches into the footer; slides too packed to fit above
+ // it may, as they always could.
+ (function () {
+ // The footer is viewport-fixed and display:none on the title page;
+ // express its height (plus the dotted rule, the progress bar and a
+ // little air) in slide coordinates, where the theme consumes it.
+ function publishFooterReserve() {
+ var footer = document.getElementById('ids-footer');
+ if (!footer || typeof Reveal === 'undefined') return;
+ var prevDisplay = footer.style.display;
+ footer.style.display = 'block';
+ var px = (footer.offsetHeight + 10) / (Reveal.getScale() || 1);
+ footer.style.display = prevDisplay;
+ document.documentElement.style.setProperty('--ids-footer-reserve', px + 'px');
+ }
+
+ // number of leading elements forming the heading block: the slide title
+ // (h1/h2) and, on level-2 slides, an h3 framesubtitle following it
+ function headingCount(slide) {
+ var kids = slide.children;
+ if (!kids[0] || (kids[0].tagName !== 'H1' && kids[0].tagName !== 'H2')) return 0;
+ if (kids[0].tagName === 'H2' && kids[1] && kids[1].tagName === 'H3') return 2;
+ return 1;
+ }
+
+ function wrapSlideContent(slide) {
+ if (slide.dataset.contentWrapped) return;
+ slide.dataset.contentWrapped = '1';
+ var content = Array.prototype.filter.call(
+ Array.prototype.slice.call(slide.children, headingCount(slide)),
+ function (el) { return el.tagName !== 'ASIDE'; }
+ );
+ // heading-only (divider) and empty slides keep their dedicated layout
+ if (!content.length) return;
+ var wrapper = document.createElement('div');
+ wrapper.className = 'ids-slide-content';
+ slide.insertBefore(wrapper, slide.children[headingCount(slide)] || null);
+ content.forEach(function (el) { wrapper.appendChild(el); });
+ }
+
+ function build() {
+ if (typeof Reveal === 'undefined' || !Reveal.isReady()) return;
+ publishFooterReserve();
+ Reveal.getSlides().forEach(wrapSlideContent);
+ }
+
+ if (typeof Reveal !== 'undefined') {
+ if (Reveal.isReady()) build();
+ else Reveal.addEventListener('ready', build);
+ Reveal.addEventListener('resize', publishFooterReserve);
+ }
+ })();
+</script>
+<script>
// Practice/read-aloud view: pressing 'r' toggles a single scrollable page
// containing every slide's content and speaker notes. Text-to-speech
// browser extensions cannot walk a reveal.js deck (hidden slides are
@@ -1195,13 +1261,19 @@
section.className = 'ids-transcript-slide';
var heading = slide.querySelector('h1,h2');
if (heading) section.appendChild(heading.cloneNode(true));
- Array.prototype.forEach.call(slide.childNodes, function (node) {
+ function appendContent(node) {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.tagName === 'ASIDE' || node.tagName === 'H1' ||
node.tagName === 'H2' || node.tagName === 'SECTION' ||
(node.classList && node.classList.contains('ids-overview-toc'))) return;
+ if (node.classList && node.classList.contains('ids-slide-content')) {
+ // the adaptive-centering wrapper is transparent for the transcript
+ Array.prototype.forEach.call(node.childNodes, appendContent);
+ return;
+ }
section.appendChild(node.cloneNode(true));
- });
+ }
+ Array.prototype.forEach.call(slide.childNodes, appendContent);
var notes = Array.prototype.map.call(
slide.querySelectorAll('aside.notes'),
function (n) { return n.innerHTML; }