|
33 | 33 |
|
34 | 34 | <div ref="pdfContainer" id="pdf-container" @scroll="checkPages"> |
35 | 35 | <progress-bar v-if="documentLoading" class="progress-bar" :show-percentage="true" :progress="progress"/> |
36 | | - <section |
| 36 | + <page-component |
37 | 37 | class="pdf-page-container" |
38 | | - v-for="index in totalPages" |
| 38 | + v-for="(page, index) in pdfPages" |
39 | 39 | :key="index" |
40 | | - :ref="pageRef(index)" |
41 | | - :style="{ height: pageHeight + 'px', width: pageWidth + 'px' }"> |
42 | | - <span class="pdf-page-loading"> {{ $tr('pageNumber', {pageNumber: index}) }} </span> |
43 | | - </section> |
| 40 | + :ref="pageRef(index + 1)" |
| 41 | + :pdfPage="page" |
| 42 | + :defaultHeight="pageHeight" |
| 43 | + :defaultWidth="pageWidth" |
| 44 | + :scale="scale" |
| 45 | + :pageNum="index + 1"> |
| 46 | + </page-component> |
44 | 47 | </div> |
45 | 48 | </div> |
46 | 49 |
|
|
58 | 61 | import responsiveWindow from 'kolibri.coreVue.mixins.responsiveWindow'; |
59 | 62 | import { sessionTimeSpent } from 'kolibri.coreVue.vuex.getters'; |
60 | 63 | import { debounce } from 'lodash'; |
| 64 | + import pageComponent from './pdfPage'; |
61 | 65 |
|
62 | 66 | // Source from which PDFJS loads its service worker, this is based on the __publicPath |
63 | 67 | // global that is defined in the Kolibri webpack pipeline, and the additional entry in the PDF renderer's |
|
78 | 82 | iconButton, |
79 | 83 | progressBar, |
80 | 84 | uiIconButton, |
| 85 | + pageComponent, |
81 | 86 | }, |
82 | 87 | props: ['defaultFile'], |
83 | 88 | data: () => ({ |
|
88 | 93 | totalPages: null, |
89 | 94 | pageHeight: null, |
90 | 95 | pageWidth: null, |
| 96 | + pdfPages: [], |
91 | 97 | }), |
92 | 98 | computed: { |
93 | 99 | fullscreenAllowed() { |
|
126 | 132 | getPage(pageNum) { |
127 | 133 | return this.pdfDocument.getPage(pageNum); |
128 | 134 | }, |
129 | | - startRender(pdfPage) { |
130 | | - // use a promise because this also calls render, allowing us to cancel |
131 | | - return new Promise((resolve, reject) => { |
132 | | - const pageNum = pdfPage.pageNumber; |
133 | | -
|
134 | | - // start the loading message |
135 | | - if (this.currentPageNum === pageNum) { |
136 | | - this.currentPageRendering = true; |
137 | | - } |
138 | | -
|
139 | | - if (this.pdfPages[pageNum]) { |
140 | | - this.pdfPages[pageNum].pdfPage = pdfPage; |
141 | | -
|
142 | | - // Get viewport, which contains directions to be passed into render function |
143 | | - const viewport = pdfPage.getViewport(this.scale); |
144 | | -
|
145 | | - // create the canvas element where page will be rendered |
146 | | - // we do this dynamically to avoid having many canvas elements simultaneously in the page |
147 | | - const canvas = this.pdfPages[pageNum].canvas || document.createElement('canvas'); |
148 | | -
|
149 | | - // define canvas and dummy blank page dimensions |
150 | | - canvas.width = this.pageWidth = viewport.width; |
151 | | - canvas.height = this.pageHeight = viewport.height; |
152 | | - canvas.style.position = 'absolute'; |
153 | | - canvas.style.top = 0; |
154 | | - canvas.style.left = 0; |
155 | | -
|
156 | | - const renderTask = pdfPage.render({ |
157 | | - canvasContext: canvas.getContext('2d'), |
158 | | - viewport, |
159 | | - }); |
160 | | -
|
161 | | - // Keep track of the canvas in case we need to manipulate it later |
162 | | - this.pdfPages[pageNum].canvas = canvas; |
163 | | - this.pdfPages[pageNum].renderTask = renderTask; |
164 | | - this.pdfPages[pageNum].rendering = true; |
165 | | - this.pdfPages[pageNum].loading = false; |
166 | | -
|
167 | | - // resolves here to indicate that the page has been set up for render. |
168 | | - // check flags for the stages of the render |
169 | | - resolve(); |
170 | | -
|
171 | | - renderTask.then( |
172 | | - () => { |
173 | | - // If this has been removed since the rendering started, then we should not proceed |
174 | | - if (this.pdfPages[pageNum]) { |
175 | | - if (this.pdfPages[pageNum].canvas) { |
176 | | - // Canvas has not been deleted in the interim |
177 | | - this.pdfPages[pageNum].rendered = true; |
178 | | - this.$refs[this.pageRef(pageNum)][0].appendChild(this.pdfPages[pageNum].canvas); |
179 | | -
|
180 | | - // end the loading message |
181 | | - if (this.currentPageNum === pageNum) { |
182 | | - this.currentPageRendering = false; |
183 | | - } |
184 | | - } |
185 | | - // Rendering has completed |
186 | | - this.pdfPages[pageNum].rendering = false; |
187 | | - } |
188 | | - }, |
189 | | - // If the render task is cancelled, then it will reject the promise and end up here. |
190 | | - () => { |
191 | | - if (this.pdfPages[pageNum]) { |
192 | | - this.pdfPages[pageNum].rendering = false; |
193 | | - } |
194 | | - } |
195 | | - ); |
196 | | - } |
197 | | - }); |
198 | | - }, |
199 | 135 | showPage(pageNum) { |
200 | 136 | if (pageNum <= this.totalPages && pageNum > 0) { |
201 | | - // Only try to show pages that exist |
202 | | - if (!this.pdfPages[pageNum]) { |
203 | | - this.pdfPages[pageNum] = {}; |
204 | | - } |
205 | | - if ( |
206 | | - // Do not try to show the page if it is already rendered, |
207 | | - // already loading, or already rendering. |
208 | | - !this.pdfPages[pageNum].rendered && |
209 | | - !this.pdfPages[pageNum].loading && |
210 | | - !this.pdfPages[pageNum].rendering |
211 | | - ) { |
212 | | - this.pdfPages[pageNum].loading = true; |
213 | | - // If we already have a reference to the PDFJS page object, then use it, |
214 | | - // rather than refetching it. |
215 | | - if (!this.pdfPages[pageNum].pdfPage) { |
216 | | - this.pdfPages[pageNum].renderPromise = this.getPage(pageNum).then(this.startRender); |
217 | | - } else { |
218 | | - this.pdfPages[pageNum].renderPromise = this.startRender(this.pdfPages[pageNum].pdfPage); |
219 | | - } |
| 137 | + const pageIndex = pageNum - 1; |
| 138 | + if (!this.pdfPages[pageIndex]) { |
| 139 | + // Only bother getting it if the pdfPage object is not already cached in the array |
| 140 | + // Cache the getPage promise in the array to prevent multiple gets, then replace it with |
| 141 | + // the page once it has been fetched |
| 142 | + this.pdfPages.splice( |
| 143 | + pageIndex, |
| 144 | + 1, |
| 145 | + this.getPage(pageNum).then(pdfPage => { |
| 146 | + this.pdfPages.splice(pageIndex, 1, pdfPage); |
| 147 | + this.$refs[this.pageRef(pageNum)][0].active = true; |
| 148 | + }) |
| 149 | + ); |
| 150 | + } else { |
| 151 | + this.$refs[this.pageRef(pageNum)][0].active = true; |
220 | 152 | } |
221 | 153 | } |
222 | 154 | }, |
223 | 155 | hidePage(pageNum) { |
224 | 156 | if (pageNum <= this.totalPages && pageNum > 0) { |
225 | 157 | // Only try to hide possibly existing pages. |
226 | | - if (!this.pdfPages[pageNum]) { |
227 | | - // No page to render, so do nothing |
228 | | - return; |
229 | | - } |
230 | | - const pdfPage = this.pdfPages[pageNum]; |
231 | | - if (pdfPage.rendered) { |
232 | | - pdfPage.rendered = false; |
233 | | - // Already rendered, just remove canvas from DOM. |
234 | | - if (pdfPage.canvas) { |
235 | | - pdfPage.canvas.remove(); |
236 | | - } |
237 | | - } else if (pdfPage.loading) { |
238 | | - // Otherwise, currently loading - let it finish the render promise, |
239 | | - // where the page is still being fetched |
240 | | - // then cancel the resulting renderTask. |
241 | | - const renderTask = pdfPage.renderTask; |
242 | | - pdfPage.renderPromise.then(() => { |
243 | | - renderTask && renderTask.cancel(); |
244 | | - }); |
245 | | - } else if (pdfPage.rendering) { |
246 | | - // Currently rendering, cancel the task directly |
247 | | - pdfPage.renderTask && pdfPage.renderTask.cancel(); |
248 | | - } |
249 | | - // Clean everything up (destroys the pdf page object). |
250 | | - pdfPage.pdfPage && pdfPage.pdfPage.cleanup(); |
251 | | - // Delete the reference so that this page is now a blank slate. |
252 | | - delete this.pdfPages[pageNum]; |
| 158 | + this.$refs[this.pageRef(pageNum)][0].active = false; |
253 | 159 | } |
254 | 160 | }, |
255 | 161 | pageRef(index) { |
|
261 | 167 | const top = this.$refs.pdfContainer.scrollTop; |
262 | 168 | const bottom = top + this.$refs.pdfContainer.clientHeight; |
263 | 169 | // Then work out which pages are visible to the user as a consequence |
264 | | - const topPageNum = Math.ceil(top / this.pageHeight); |
265 | | - const bottomPageNum = Math.ceil(bottom / this.pageHeight); |
266 | | -
|
267 | 170 | // Loop through all pages, show ones that are in the display window, hide ones that aren't |
268 | | - for (let i = 1; i <= this.totalPages; i++) { |
269 | | - // Hide pages that are less than 'pageDisplayWindow' lower than the top page number |
270 | | - // or the same amount higher than the bottom page number |
271 | | - if (i < topPageNum - pageDisplayWindow || i > bottomPageNum + pageDisplayWindow) { |
272 | | - this.hidePage(i); |
273 | | - } else { |
| 171 | + let cumulativeHeight = 0; |
| 172 | + const pagesToDisplay = []; |
| 173 | + let i, display; |
| 174 | + for (i = 1; i <= this.totalPages; i++) { |
| 175 | + // If the current cumulativeHeight (which marks the beginning of this page) |
| 176 | + // is higher than top and less than bottom, then this page |
| 177 | + // should be displayed |
| 178 | + display = false; |
| 179 | + const pageHeight = this.$refs[this.pageRef(i)][0].pageHeight; |
| 180 | + // Top of page is in the middle of the viewport |
| 181 | + if (cumulativeHeight >= top && cumulativeHeight <= bottom) { |
| 182 | + display = true; |
| 183 | + } |
| 184 | + // Page top and bottom wrap the viewport |
| 185 | + if (cumulativeHeight <= top && cumulativeHeight + pageHeight >= bottom) { |
| 186 | + display = true; |
| 187 | + } |
| 188 | + cumulativeHeight += pageHeight; |
| 189 | + // Bottom of page is in the middle of the viewport |
| 190 | + if (cumulativeHeight >= top && cumulativeHeight <= bottom) { |
| 191 | + display = true; |
| 192 | + } |
| 193 | + pagesToDisplay.push(display); |
| 194 | + } |
| 195 | + for (i = 1; i <= this.totalPages; i++) { |
| 196 | + // Render pages conditionally on pagesToDisplay, taking into account the display window |
| 197 | + if ( |
| 198 | + pagesToDisplay |
| 199 | + .slice( |
| 200 | + Math.max(0, i - 1 - pageDisplayWindow), |
| 201 | + Math.min(pagesToDisplay.length, i - 1 + pageDisplayWindow) |
| 202 | + ) |
| 203 | + .some(trueOrFalse => trueOrFalse) |
| 204 | + ) { |
274 | 205 | this.showPage(i); |
| 206 | + } else { |
| 207 | + this.hidePage(i); |
275 | 208 | } |
276 | 209 | } |
277 | 210 | }, renderDebounceTime), |
|
311 | 244 | // Get initial info from the loaded pdf document |
312 | 245 | this.pdfDocument = pdfDocument; |
313 | 246 | this.totalPages = pdfDocument.numPages; |
314 | | - this.pdfPages = {}; |
| 247 | + // Set pdfPages to an array of length total pages |
| 248 | + this.pdfPages = Array(this.totalPages); |
315 | 249 |
|
316 | 250 | return this.getPage(1).then(firstPage => { |
317 | 251 | const pageMargin = 5; |
|
332 | 266 | this.pageWidth = initialViewport.width; |
333 | 267 | // Set the firstPage into the pdfPages object so that we do not refetch the page |
334 | 268 | // from PDFJS when we do our initial render |
335 | | - this.pdfPages[1] = firstPage; |
| 269 | + this.pdfPages.splice(0, 1, firstPage); |
336 | 270 | }); |
337 | 271 | }); |
338 | 272 | }, |
|
361 | 295 | $trs: { |
362 | 296 | exitFullscreen: 'Exit fullscreen', |
363 | 297 | enterFullscreen: 'Enter fullscreen', |
364 | | - pageNumber: '{pageNumber, number}', |
365 | 298 | }, |
366 | 299 | vuex: { |
367 | 300 | getters: { |
|
379 | 312 |
|
380 | 313 | $keen-button-height = 48px |
381 | 314 | $fullscreen-button-height = 36px |
| 315 | + // Defined here and in pdfPage.vue |
382 | 316 | $page-padding = 5px |
383 | 317 |
|
384 | 318 | .doc-viewer |
|
419 | 353 | // prevents a never-visible spot underneath the fullscreen button |
420 | 354 | padding-top: $fullscreen-button-height + $page-padding |
421 | 355 |
|
422 | | - .pdf-page |
423 | | - &-container |
424 | | - background: #FFFFFF |
425 | | - margin: $page-padding auto |
426 | | - position: relative |
427 | | - z-index: 2 // material spec - card (resting) |
428 | | - &-loading |
429 | | - position: absolute |
430 | | - top: 50% |
431 | | - left: 50% |
432 | | - transform: translate(-50%, -50%) |
433 | | - font-size: 2em |
434 | | - line-height: 100% |
435 | | -
|
436 | 356 | .doc-viewer-controls |
437 | 357 | z-index: 6 // material spec - snackbar and FAB |
438 | 358 |
|
|
0 commit comments