Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 1 | import { SLIDES_SELECTOR } from '../utils/constants.js' |
| 2 | import { queryAll, createStyleSheet } from '../utils/util.js' |
| 3 | |
| 4 | /** |
| 5 | * Setups up our presentation for printing/exporting to PDF. |
| 6 | */ |
| 7 | export default class Print { |
| 8 | |
| 9 | constructor( Reveal ) { |
| 10 | |
| 11 | this.Reveal = Reveal; |
| 12 | |
| 13 | } |
| 14 | |
| 15 | /** |
| 16 | * Configures the presentation for printing to a static |
| 17 | * PDF. |
| 18 | */ |
| 19 | async setupPDF() { |
| 20 | |
| 21 | const config = this.Reveal.getConfig(); |
| 22 | const slides = queryAll( this.Reveal.getRevealElement(), SLIDES_SELECTOR ) |
| 23 | |
| 24 | // Compute slide numbers now, before we start duplicating slides |
| 25 | const doingSlideNumbers = config.slideNumber && /all|print/i.test( config.showSlideNumber ); |
| 26 | |
| 27 | const slideSize = this.Reveal.getComputedSlideSize( window.innerWidth, window.innerHeight ); |
| 28 | |
| 29 | // Dimensions of the PDF pages |
| 30 | const pageWidth = Math.floor( slideSize.width * ( 1 + config.margin ) ), |
| 31 | pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) ); |
| 32 | |
| 33 | // Dimensions of slides within the pages |
| 34 | const slideWidth = slideSize.width, |
| 35 | slideHeight = slideSize.height; |
| 36 | |
| 37 | await new Promise( requestAnimationFrame ); |
| 38 | |
| 39 | // Let the browser know what page size we want to print |
| 40 | createStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0px;}' ); |
| 41 | |
| 42 | // Limit the size of certain elements to the dimensions of the slide |
| 43 | createStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' ); |
| 44 | |
| 45 | document.documentElement.classList.add( 'print-pdf' ); |
| 46 | document.body.style.width = pageWidth + 'px'; |
| 47 | document.body.style.height = pageHeight + 'px'; |
| 48 | |
Christophe Dervieux | 8afae13 | 2021-12-06 15:16:42 +0100 | [diff] [blame] | 49 | const viewportElement = document.querySelector( '.reveal-viewport' ); |
| 50 | let presentationBackground; |
| 51 | if( viewportElement ) { |
| 52 | const viewportStyles = window.getComputedStyle( viewportElement ); |
| 53 | if( viewportStyles && viewportStyles.background ) { |
| 54 | presentationBackground = viewportStyles.background; |
| 55 | } |
| 56 | } |
| 57 | |
Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 58 | // Make sure stretch elements fit on slide |
| 59 | await new Promise( requestAnimationFrame ); |
| 60 | this.Reveal.layoutSlideContents( slideWidth, slideHeight ); |
| 61 | |
Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 62 | // Batch scrollHeight access to prevent layout thrashing |
| 63 | await new Promise( requestAnimationFrame ); |
| 64 | |
| 65 | const slideScrollHeights = slides.map( slide => slide.scrollHeight ); |
| 66 | |
| 67 | const pages = []; |
| 68 | const pageContainer = slides[0].parentNode; |
| 69 | |
| 70 | // Slide and slide background layout |
| 71 | slides.forEach( function( slide, index ) { |
| 72 | |
| 73 | // Vertical stacks are not centred since their section |
| 74 | // children will be |
| 75 | if( slide.classList.contains( 'stack' ) === false ) { |
| 76 | // Center the slide inside of the page, giving the slide some margin |
| 77 | let left = ( pageWidth - slideWidth ) / 2; |
| 78 | let top = ( pageHeight - slideHeight ) / 2; |
| 79 | |
| 80 | const contentHeight = slideScrollHeights[ index ]; |
| 81 | let numberOfPages = Math.max( Math.ceil( contentHeight / pageHeight ), 1 ); |
| 82 | |
| 83 | // Adhere to configured pages per slide limit |
| 84 | numberOfPages = Math.min( numberOfPages, config.pdfMaxPagesPerSlide ); |
| 85 | |
| 86 | // Center slides vertically |
| 87 | if( numberOfPages === 1 && config.center || slide.classList.contains( 'center' ) ) { |
| 88 | top = Math.max( ( pageHeight - contentHeight ) / 2, 0 ); |
| 89 | } |
| 90 | |
| 91 | // Wrap the slide in a page element and hide its overflow |
| 92 | // so that no page ever flows onto another |
| 93 | const page = document.createElement( 'div' ); |
| 94 | pages.push( page ); |
| 95 | |
| 96 | page.className = 'pdf-page'; |
| 97 | page.style.height = ( ( pageHeight + config.pdfPageHeightOffset ) * numberOfPages ) + 'px'; |
Christophe Dervieux | 8afae13 | 2021-12-06 15:16:42 +0100 | [diff] [blame] | 98 | |
| 99 | // Copy the presentation-wide background to each individual |
| 100 | // page when printing |
| 101 | if( presentationBackground ) { |
| 102 | page.style.background = presentationBackground; |
| 103 | } |
| 104 | |
Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 105 | page.appendChild( slide ); |
| 106 | |
| 107 | // Position the slide inside of the page |
| 108 | slide.style.left = left + 'px'; |
| 109 | slide.style.top = top + 'px'; |
| 110 | slide.style.width = slideWidth + 'px'; |
| 111 | |
Christophe Dervieux | 8afae13 | 2021-12-06 15:16:42 +0100 | [diff] [blame] | 112 | // Re-run the slide layout so that r-fit-text is applied based on |
| 113 | // the printed slide size |
| 114 | this.Reveal.slideContent.layout( slide ) |
| 115 | |
Christophe Dervieux | e1893ae | 2021-10-07 17:09:02 +0200 | [diff] [blame] | 116 | if( slide.slideBackgroundElement ) { |
| 117 | page.insertBefore( slide.slideBackgroundElement, slide ); |
| 118 | } |
| 119 | |
| 120 | // Inject notes if `showNotes` is enabled |
| 121 | if( config.showNotes ) { |
| 122 | |
| 123 | // Are there notes for this slide? |
| 124 | const notes = this.Reveal.getSlideNotes( slide ); |
| 125 | if( notes ) { |
| 126 | |
| 127 | const notesSpacing = 8; |
| 128 | const notesLayout = typeof config.showNotes === 'string' ? config.showNotes : 'inline'; |
| 129 | const notesElement = document.createElement( 'div' ); |
| 130 | notesElement.classList.add( 'speaker-notes' ); |
| 131 | notesElement.classList.add( 'speaker-notes-pdf' ); |
| 132 | notesElement.setAttribute( 'data-layout', notesLayout ); |
| 133 | notesElement.innerHTML = notes; |
| 134 | |
| 135 | if( notesLayout === 'separate-page' ) { |
| 136 | pages.push( notesElement ); |
| 137 | } |
| 138 | else { |
| 139 | notesElement.style.left = notesSpacing + 'px'; |
| 140 | notesElement.style.bottom = notesSpacing + 'px'; |
| 141 | notesElement.style.width = ( pageWidth - notesSpacing*2 ) + 'px'; |
| 142 | page.appendChild( notesElement ); |
| 143 | } |
| 144 | |
| 145 | } |
| 146 | |
| 147 | } |
| 148 | |
| 149 | // Inject slide numbers if `slideNumbers` are enabled |
| 150 | if( doingSlideNumbers ) { |
| 151 | const slideNumber = index + 1; |
| 152 | const numberElement = document.createElement( 'div' ); |
| 153 | numberElement.classList.add( 'slide-number' ); |
| 154 | numberElement.classList.add( 'slide-number-pdf' ); |
| 155 | numberElement.innerHTML = slideNumber; |
| 156 | page.appendChild( numberElement ); |
| 157 | } |
| 158 | |
| 159 | // Copy page and show fragments one after another |
| 160 | if( config.pdfSeparateFragments ) { |
| 161 | |
| 162 | // Each fragment 'group' is an array containing one or more |
| 163 | // fragments. Multiple fragments that appear at the same time |
| 164 | // are part of the same group. |
| 165 | const fragmentGroups = this.Reveal.fragments.sort( page.querySelectorAll( '.fragment' ), true ); |
| 166 | |
| 167 | let previousFragmentStep; |
| 168 | |
| 169 | fragmentGroups.forEach( function( fragments ) { |
| 170 | |
| 171 | // Remove 'current-fragment' from the previous group |
| 172 | if( previousFragmentStep ) { |
| 173 | previousFragmentStep.forEach( function( fragment ) { |
| 174 | fragment.classList.remove( 'current-fragment' ); |
| 175 | } ); |
| 176 | } |
| 177 | |
| 178 | // Show the fragments for the current index |
| 179 | fragments.forEach( function( fragment ) { |
| 180 | fragment.classList.add( 'visible', 'current-fragment' ); |
| 181 | }, this ); |
| 182 | |
| 183 | // Create a separate page for the current fragment state |
| 184 | const clonedPage = page.cloneNode( true ); |
| 185 | pages.push( clonedPage ); |
| 186 | |
| 187 | previousFragmentStep = fragments; |
| 188 | |
| 189 | }, this ); |
| 190 | |
| 191 | // Reset the first/original page so that all fragments are hidden |
| 192 | fragmentGroups.forEach( function( fragments ) { |
| 193 | fragments.forEach( function( fragment ) { |
| 194 | fragment.classList.remove( 'visible', 'current-fragment' ); |
| 195 | } ); |
| 196 | } ); |
| 197 | |
| 198 | } |
| 199 | // Show all fragments |
| 200 | else { |
| 201 | queryAll( page, '.fragment:not(.fade-out)' ).forEach( function( fragment ) { |
| 202 | fragment.classList.add( 'visible' ); |
| 203 | } ); |
| 204 | } |
| 205 | |
| 206 | } |
| 207 | |
| 208 | }, this ); |
| 209 | |
| 210 | await new Promise( requestAnimationFrame ); |
| 211 | |
| 212 | pages.forEach( page => pageContainer.appendChild( page ) ); |
| 213 | |
| 214 | // Notify subscribers that the PDF layout is good to go |
| 215 | this.Reveal.dispatchEvent({ type: 'pdf-ready' }); |
| 216 | |
| 217 | } |
| 218 | |
| 219 | /** |
| 220 | * Checks if this instance is being used to print a PDF. |
| 221 | */ |
| 222 | isPrintingPDF() { |
| 223 | |
| 224 | return ( /print-pdf/gi ).test( window.location.search ); |
| 225 | |
| 226 | } |
| 227 | |
| 228 | } |