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 ); } }