import { SLIDES_SELECTOR } from "../utils/constants.js"; import { queryAll, createStyleSheet } from "../utils/util.js"; /** * Setups up our presentation for printing/exporting to PDF. */ export default class Print { constructor(Reveal) { this.Reveal = Reveal; } /** * Configures the presentation for printing to a static * PDF. */ async setupPDF() { const config = this.Reveal.getConfig(); const slides = queryAll(this.Reveal.getRevealElement(), SLIDES_SELECTOR); // Compute slide numbers now, before we start duplicating slides const injectPageNumbers = config.slideNumber && /all|print/i.test(config.showSlideNumber); const slideSize = this.Reveal.getComputedSlideSize( window.innerWidth, window.innerHeight ); // Dimensions of the PDF pages const pageWidth = Math.floor(slideSize.width * (1 + config.margin)), pageHeight = Math.floor(slideSize.height * (1 + config.margin)); // Dimensions of slides within the pages const slideWidth = slideSize.width, slideHeight = slideSize.height; await new Promise(requestAnimationFrame); // Let the browser know what page size we want to print createStyleSheet( "@page{size:" + pageWidth + "px " + pageHeight + "px; margin: 0px;}" ); // Limit the size of certain elements to the dimensions of the slide createStyleSheet( ".reveal section>img, .reveal section>video, .reveal section>iframe{max-width: " + slideWidth + "px; max-height:" + slideHeight + "px}" ); document.documentElement.classList.add("print-pdf"); document.body.style.width = pageWidth + "px"; document.body.style.height = pageHeight + "px"; const viewportElement = document.querySelector(".reveal-viewport"); let presentationBackground; if (viewportElement) { const viewportStyles = window.getComputedStyle(viewportElement); if (viewportStyles && viewportStyles.background) { presentationBackground = viewportStyles.background; } } // Make sure stretch elements fit on slide await new Promise(requestAnimationFrame); this.Reveal.layoutSlideContents(slideWidth, slideHeight); // Batch scrollHeight access to prevent layout thrashing await new Promise(requestAnimationFrame); const slideScrollHeights = slides.map((slide) => slide.scrollHeight); const pages = []; const pageContainer = slides[0].parentNode; let slideNumber = 1; // Slide and slide background layout slides.forEach(function (slide, index) { // Vertical stacks are not centred since their section // children will be if (slide.classList.contains("stack") === false) { // Center the slide inside of the page, giving the slide some margin let left = (pageWidth - slideWidth) / 2; let top = (pageHeight - slideHeight) / 2; const contentHeight = slideScrollHeights[index]; let numberOfPages = Math.max(Math.ceil(contentHeight / pageHeight), 1); // Adhere to configured pages per slide limit numberOfPages = Math.min(numberOfPages, config.pdfMaxPagesPerSlide); // Center slides vertically if ( (numberOfPages === 1 && config.center) || slide.classList.contains("center") ) { top = Math.max((pageHeight - contentHeight) / 2, 0); } // Wrap the slide in a page element and hide its overflow // so that no page ever flows onto another const page = document.createElement("div"); pages.push(page); page.className = "pdf-page"; page.style.height = (pageHeight + config.pdfPageHeightOffset) * numberOfPages + "px"; // Copy the presentation-wide background to each individual // page when printing if (presentationBackground) { page.style.background = presentationBackground; } page.appendChild(slide); // Position the slide inside of the page slide.style.left = left + "px"; slide.style.top = top + "px"; slide.style.width = slideWidth + "px"; this.Reveal.slideContent.layout(slide); if (slide.slideBackgroundElement) { page.insertBefore(slide.slideBackgroundElement, slide); } // Inject notes if `showNotes` is enabled if (config.showNotes) { // Are there notes for this slide? const notes = this.Reveal.getSlideNotes(slide); if (notes) { const notesSpacing = 8; const notesLayout = typeof config.showNotes === "string" ? config.showNotes : "inline"; const notesElement = document.createElement("div"); notesElement.classList.add("speaker-notes"); notesElement.classList.add("speaker-notes-pdf"); notesElement.setAttribute("data-layout", notesLayout); notesElement.innerHTML = notes; if (notesLayout === "separate-page") { pages.push(notesElement); } else { notesElement.style.left = notesSpacing + "px"; notesElement.style.bottom = notesSpacing + "px"; notesElement.style.width = pageWidth - notesSpacing * 2 + "px"; page.appendChild(notesElement); } } } // Inject page numbers if `slideNumbers` are enabled if (injectPageNumbers) { const numberElement = document.createElement("div"); numberElement.classList.add("slide-number"); numberElement.classList.add("slide-number-pdf"); numberElement.innerHTML = slideNumber++; page.appendChild(numberElement); } // Copy page and show fragments one after another if (config.pdfSeparateFragments) { // Each fragment 'group' is an array containing one or more // fragments. Multiple fragments that appear at the same time // are part of the same group. const fragmentGroups = this.Reveal.fragments.sort( page.querySelectorAll(".fragment"), true ); let previousFragmentStep; fragmentGroups.forEach(function (fragments, index) { // Remove 'current-fragment' from the previous group if (previousFragmentStep) { previousFragmentStep.forEach(function (fragment) { fragment.classList.remove("current-fragment"); }); } // Show the fragments for the current index fragments.forEach(function (fragment) { fragment.classList.add("visible", "current-fragment"); }, this); // Create a separate page for the current fragment state const clonedPage = page.cloneNode(true); // Inject unique page numbers for fragments if (injectPageNumbers) { const numberElement = clonedPage.querySelector(".slide-number-pdf"); const fragmentNumber = index + 1; numberElement.innerHTML += "." + fragmentNumber; } pages.push(clonedPage); previousFragmentStep = fragments; }, this); // Reset the first/original page so that all fragments are hidden fragmentGroups.forEach(function (fragments) { fragments.forEach(function (fragment) { fragment.classList.remove("visible", "current-fragment"); }); }); } // Show all fragments else { queryAll(page, ".fragment:not(.fade-out)").forEach(function ( fragment ) { fragment.classList.add("visible"); }); } } }, this); await new Promise(requestAnimationFrame); pages.forEach((page) => pageContainer.appendChild(page)); // Re-run JS-based content layout after the slide is added to page DOM this.Reveal.slideContent.layout(this.Reveal.getSlidesElement()); // Notify subscribers that the PDF layout is good to go this.Reveal.dispatchEvent({ type: "pdf-ready" }); } /** * Checks if this instance is being used to print a PDF. */ isPrintingPDF() { return /print-pdf/gi.test(window.location.search); } }