@meistrari/tela-build 1.25.1 → 1.25.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { useEventListener, useScroll } from '@vueuse/core'
|
|
2
|
+
import { useThrottleFn, useEventListener, useScroll } from '@vueuse/core'
|
|
3
3
|
import { cn } from '@/lib/utils'
|
|
4
4
|
import type { PreviewFile, PreviewContentLabels, PdfDocumentHandle } from './types'
|
|
5
5
|
|
|
@@ -24,6 +24,8 @@ const props = withDefaults(
|
|
|
24
24
|
highlightExact?: boolean
|
|
25
25
|
/** Override the initial zoom scale. Defaults to 1 for minimal, 0.75 for default. */
|
|
26
26
|
initialScale?: number
|
|
27
|
+
/** When true, shows the loading skeleton regardless of internal loading state. Use when the file content is being fetched externally (e.g. lazy download). */
|
|
28
|
+
loading?: boolean
|
|
27
29
|
}>(),
|
|
28
30
|
{
|
|
29
31
|
variant: 'default',
|
|
@@ -32,6 +34,7 @@ const props = withDefaults(
|
|
|
32
34
|
highlightPage: undefined,
|
|
33
35
|
highlightExact: false,
|
|
34
36
|
initialScale: undefined,
|
|
37
|
+
loading: false,
|
|
35
38
|
},
|
|
36
39
|
)
|
|
37
40
|
|
|
@@ -94,16 +97,33 @@ const pdfLoadError = ref<string | null>(null)
|
|
|
94
97
|
const pageRefs = ref<Map<number, HTMLElement>>(new Map())
|
|
95
98
|
const textLayerRefs = ref<Map<number, HTMLDivElement>>(new Map())
|
|
96
99
|
const renderedPages = ref<Set<number>>(new Set())
|
|
100
|
+
const pageDimensions = ref<Map<number, { width: number, height: number }>>(new Map())
|
|
101
|
+
const defaultPageDimensions = ref<{ width: number, height: number } | null>(null)
|
|
102
|
+
const visiblePages = ref<Set<number>>(new Set())
|
|
103
|
+
const pdfObserver = ref<IntersectionObserver | null>(null)
|
|
104
|
+
const BUFFER_PAGES = 1
|
|
105
|
+
const INITIAL_DIMENSION_PAGES = 3
|
|
106
|
+
const DIMENSION_BATCH_SIZE = 8
|
|
107
|
+
const canvasRenderTokens = new WeakMap<HTMLCanvasElement, number>()
|
|
108
|
+
let canvasRenderTokenCounter = 0
|
|
109
|
+
let pageRenderGeneration = 0
|
|
110
|
+
const inFlightPageRenders = new Map<number, {
|
|
111
|
+
generation: number
|
|
112
|
+
scale: number
|
|
113
|
+
promise: Promise<void>
|
|
114
|
+
}>()
|
|
97
115
|
let isRendering = false
|
|
98
116
|
let pendingReRender = false
|
|
99
117
|
let pendingScrollPage: number | null = null
|
|
100
118
|
|
|
119
|
+
const throttledReRender = useThrottleFn(reRenderAllPdfPages, 150, true)
|
|
120
|
+
|
|
101
121
|
function zoomIn() {
|
|
102
122
|
if (scale.value < 3) {
|
|
103
123
|
scale.value = Math.min(scale.value + 0.25, 3)
|
|
104
124
|
|
|
105
125
|
if (props.file.fileType === 'application/pdf') {
|
|
106
|
-
|
|
126
|
+
throttledReRender()
|
|
107
127
|
}
|
|
108
128
|
}
|
|
109
129
|
}
|
|
@@ -113,10 +133,33 @@ function zoomOut() {
|
|
|
113
133
|
scale.value = Math.max(scale.value - 0.25, 0.5)
|
|
114
134
|
|
|
115
135
|
if (props.file.fileType === 'application/pdf') {
|
|
116
|
-
|
|
136
|
+
throttledReRender()
|
|
117
137
|
}
|
|
118
138
|
}
|
|
119
139
|
}
|
|
140
|
+
function clearCanvasElement(canvas: HTMLCanvasElement | null | undefined) {
|
|
141
|
+
if (!canvas)
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
canvasRenderTokens.delete(canvas)
|
|
145
|
+
|
|
146
|
+
const ctx = canvas.getContext('2d')
|
|
147
|
+
if (ctx) {
|
|
148
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
canvas.width = 0
|
|
152
|
+
canvas.height = 0
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function clearTextLayerElement(textLayer: HTMLDivElement | null | undefined) {
|
|
156
|
+
if (!textLayer)
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
textLayer.innerHTML = ''
|
|
160
|
+
textLayer.style.width = ''
|
|
161
|
+
textLayer.style.height = ''
|
|
162
|
+
}
|
|
120
163
|
|
|
121
164
|
function handleMouseDown(e: MouseEvent) {
|
|
122
165
|
if (!scrollContainerRef.value || !dragEnabled.value)
|
|
@@ -195,8 +238,13 @@ watch(scrollY, () => {
|
|
|
195
238
|
function setPageRef(pageNum: number, el: HTMLElement | null) {
|
|
196
239
|
if (el) {
|
|
197
240
|
pageRefs.value.set(pageNum, el)
|
|
241
|
+
el.setAttribute('data-page-num', String(pageNum))
|
|
242
|
+
pdfObserver.value?.observe(el)
|
|
198
243
|
}
|
|
199
244
|
else {
|
|
245
|
+
const existing = pageRefs.value.get(pageNum)
|
|
246
|
+
if (existing)
|
|
247
|
+
pdfObserver.value?.unobserve(existing)
|
|
200
248
|
pageRefs.value.delete(pageNum)
|
|
201
249
|
}
|
|
202
250
|
}
|
|
@@ -211,6 +259,34 @@ function setTextLayerRef(pageNum: number, el: HTMLDivElement | null) {
|
|
|
211
259
|
}
|
|
212
260
|
|
|
213
261
|
async function renderPdfPage(pageNum: number) {
|
|
262
|
+
const generation = pageRenderGeneration
|
|
263
|
+
const renderScale = scale.value
|
|
264
|
+
const existingRender = inFlightPageRenders.get(pageNum)
|
|
265
|
+
|
|
266
|
+
if (existingRender && existingRender.generation === generation && existingRender.scale === renderScale) {
|
|
267
|
+
await existingRender.promise
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const renderPromise = renderPdfPageOnce(pageNum, generation, renderScale)
|
|
272
|
+
inFlightPageRenders.set(pageNum, {
|
|
273
|
+
generation,
|
|
274
|
+
scale: renderScale,
|
|
275
|
+
promise: renderPromise,
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
await renderPromise
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
const activeRender = inFlightPageRenders.get(pageNum)
|
|
283
|
+
if (activeRender?.promise === renderPromise) {
|
|
284
|
+
inFlightPageRenders.delete(pageNum)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function renderPdfPageOnce(pageNum: number, generation: number, renderScale: number) {
|
|
214
290
|
const handle = pdfDocHandle.value
|
|
215
291
|
|
|
216
292
|
if (!handle)
|
|
@@ -221,50 +297,347 @@ async function renderPdfPage(pageNum: number) {
|
|
|
221
297
|
if (!pageContainer)
|
|
222
298
|
return
|
|
223
299
|
|
|
224
|
-
const canvas = pageContainer.querySelector('canvas')
|
|
300
|
+
const canvas = pageContainer.querySelector('canvas') as HTMLCanvasElement | null
|
|
225
301
|
|
|
226
302
|
if (!canvas)
|
|
227
303
|
return
|
|
304
|
+
const renderToken = ++canvasRenderTokenCounter
|
|
305
|
+
canvasRenderTokens.set(canvas, renderToken)
|
|
228
306
|
|
|
229
307
|
const textLayer = textLayerRefs.value.get(pageNum) ?? null
|
|
230
308
|
|
|
231
309
|
await handle.renderPage({
|
|
232
310
|
pageNum,
|
|
233
|
-
canvas
|
|
234
|
-
scale:
|
|
311
|
+
canvas,
|
|
312
|
+
scale: renderScale,
|
|
235
313
|
textLayer,
|
|
236
314
|
highlight: props.highlightText,
|
|
237
315
|
highlightPage: props.highlightPage,
|
|
238
316
|
highlightExact: props.highlightExact,
|
|
239
317
|
})
|
|
240
318
|
|
|
319
|
+
const currentRenderToken = canvasRenderTokens.get(canvas)
|
|
320
|
+
if (
|
|
321
|
+
currentRenderToken !== renderToken
|
|
322
|
+
|| handle !== pdfDocHandle.value
|
|
323
|
+
|| generation !== pageRenderGeneration
|
|
324
|
+
|| renderScale !== scale.value
|
|
325
|
+
) {
|
|
326
|
+
if (currentRenderToken === undefined) {
|
|
327
|
+
clearCanvasElement(canvas)
|
|
328
|
+
}
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
|
|
241
332
|
renderedPages.value.add(pageNum)
|
|
242
333
|
}
|
|
243
334
|
|
|
244
|
-
|
|
245
|
-
const
|
|
335
|
+
function clearPageCanvas(pageNum: number) {
|
|
336
|
+
const container = pageRefs.value.get(pageNum)
|
|
337
|
+
if (!container)
|
|
338
|
+
return
|
|
339
|
+
const canvas = container.querySelector('canvas') as HTMLCanvasElement | null
|
|
340
|
+
const textLayer = textLayerRefs.value.get(pageNum)
|
|
341
|
+
clearCanvasElement(canvas)
|
|
342
|
+
clearTextLayerElement(textLayer)
|
|
343
|
+
renderedPages.value.delete(pageNum)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function clearAllPageCanvases() {
|
|
347
|
+
for (const [pageNum, container] of pageRefs.value.entries()) {
|
|
348
|
+
const canvas = container.querySelector('canvas') as HTMLCanvasElement | null
|
|
349
|
+
const textLayer = textLayerRefs.value.get(pageNum)
|
|
350
|
+
clearCanvasElement(canvas)
|
|
351
|
+
clearTextLayerElement(textLayer)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function getBufferedPageNumbers(pageNums: Iterable<number>, total: number): number[] {
|
|
356
|
+
const bufferedPages = new Set<number>()
|
|
357
|
+
|
|
358
|
+
for (const pageNum of pageNums) {
|
|
359
|
+
for (let i = pageNum - BUFFER_PAGES; i <= pageNum + BUFFER_PAGES; i++) {
|
|
360
|
+
if (i >= 1 && i <= total) {
|
|
361
|
+
bufferedPages.add(i)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return [...bufferedPages].sort((a, b) => a - b)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function getPagesToRender(total: number): number[] {
|
|
370
|
+
if (visiblePages.value.size === 0) {
|
|
371
|
+
return getBufferedPageNumbers([1], total)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return getBufferedPageNumbers(visiblePages.value, total)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function getInitialDimensionPages(total: number): number[] {
|
|
378
|
+
return Array.from({ length: Math.min(total, INITIAL_DIMENSION_PAGES) }, (_, index) => index + 1)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function yieldToBrowser(): Promise<void> {
|
|
382
|
+
if (typeof requestAnimationFrame === 'undefined') {
|
|
383
|
+
return new Promise(resolve => setTimeout(resolve, 0))
|
|
384
|
+
}
|
|
246
385
|
|
|
386
|
+
return new Promise(resolve => requestAnimationFrame(() => resolve()))
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let dimensionGeneration = 0
|
|
390
|
+
|
|
391
|
+
async function measurePageDimension(
|
|
392
|
+
handle: PdfDocumentHandle,
|
|
393
|
+
pageNum: number,
|
|
394
|
+
generation: number,
|
|
395
|
+
): Promise<{ width: number, height: number } | null> {
|
|
396
|
+
if (!handle.getPageDimensions)
|
|
397
|
+
return null
|
|
398
|
+
|
|
399
|
+
const existing = pageDimensions.value.get(pageNum)
|
|
400
|
+
if (existing)
|
|
401
|
+
return existing
|
|
402
|
+
|
|
403
|
+
const dims = await handle.getPageDimensions({ pageNum, scale: scale.value })
|
|
404
|
+
if (!dims)
|
|
405
|
+
return null
|
|
406
|
+
|
|
407
|
+
if (generation !== dimensionGeneration || handle !== pdfDocHandle.value)
|
|
408
|
+
return null
|
|
409
|
+
|
|
410
|
+
pageDimensions.value.set(pageNum, dims)
|
|
411
|
+
|
|
412
|
+
if (pageNum === 1 || !defaultPageDimensions.value) {
|
|
413
|
+
defaultPageDimensions.value = dims
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return dims
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function primePageDimensions(
|
|
420
|
+
handle: PdfDocumentHandle,
|
|
421
|
+
priorityPages: number[],
|
|
422
|
+
generation: number,
|
|
423
|
+
): Promise<void> {
|
|
424
|
+
if (!handle.getPageDimensions)
|
|
425
|
+
return
|
|
426
|
+
|
|
427
|
+
for (const pageNum of priorityPages) {
|
|
428
|
+
await measurePageDimension(handle, pageNum, generation)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function warmRemainingPageDimensions(
|
|
433
|
+
handle: PdfDocumentHandle,
|
|
434
|
+
priorityPages: number[],
|
|
435
|
+
generation: number,
|
|
436
|
+
): Promise<void> {
|
|
437
|
+
if (!handle.getPageDimensions)
|
|
438
|
+
return
|
|
439
|
+
|
|
440
|
+
const priorityPageSet = new Set(priorityPages)
|
|
441
|
+
const deferredPages: number[] = []
|
|
442
|
+
|
|
443
|
+
for (let pageNum = 1; pageNum <= handle.numPages; pageNum++) {
|
|
444
|
+
if (!priorityPageSet.has(pageNum)) {
|
|
445
|
+
deferredPages.push(pageNum)
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
for (let i = 0; i < deferredPages.length; i += DIMENSION_BATCH_SIZE) {
|
|
450
|
+
const batch = deferredPages.slice(i, i + DIMENSION_BATCH_SIZE)
|
|
451
|
+
await Promise.all(batch.map(pageNum => measurePageDimension(handle, pageNum, generation)))
|
|
452
|
+
|
|
453
|
+
if (generation !== dimensionGeneration || handle !== pdfDocHandle.value)
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
await yieldToBrowser()
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
let isRenderingVisiblePages = false
|
|
461
|
+
let shouldRenderVisiblePagesAgain = false
|
|
462
|
+
|
|
463
|
+
async function renderVisiblePagesOnce() {
|
|
464
|
+
const handle = pdfDocHandle.value
|
|
247
465
|
if (!handle)
|
|
248
466
|
return
|
|
249
467
|
|
|
468
|
+
const pagesToRender = new Set<number>(getPagesToRender(handle.numPages))
|
|
469
|
+
|
|
470
|
+
for (const pageNum of pagesToRender) {
|
|
471
|
+
if (!renderedPages.value.has(pageNum)) {
|
|
472
|
+
await renderPdfPage(pageNum)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
for (const pageNum of [...renderedPages.value]) {
|
|
477
|
+
if (!pagesToRender.has(pageNum)) {
|
|
478
|
+
clearPageCanvas(pageNum)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function renderVisiblePages() {
|
|
484
|
+
if (isRenderingVisiblePages) {
|
|
485
|
+
shouldRenderVisiblePagesAgain = true
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
isRenderingVisiblePages = true
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
do {
|
|
493
|
+
shouldRenderVisiblePagesAgain = false
|
|
494
|
+
await renderVisiblePagesOnce()
|
|
495
|
+
}
|
|
496
|
+
while (shouldRenderVisiblePagesAgain)
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
isRenderingVisiblePages = false
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
let renderRafId: number | null = null
|
|
504
|
+
|
|
505
|
+
function scheduleRenderVisiblePages() {
|
|
506
|
+
if (renderRafId !== null)
|
|
507
|
+
return
|
|
508
|
+
renderRafId = requestAnimationFrame(() => {
|
|
509
|
+
renderRafId = null
|
|
510
|
+
void renderVisiblePages()
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function setupPdfObserver() {
|
|
515
|
+
pdfObserver.value?.disconnect()
|
|
516
|
+
|
|
517
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
518
|
+
renderFallbackAllPages()
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
pdfObserver.value = new IntersectionObserver(
|
|
523
|
+
(entries) => {
|
|
524
|
+
for (const entry of entries) {
|
|
525
|
+
const pageNum = Number(entry.target.getAttribute('data-page-num'))
|
|
526
|
+
if (Number.isNaN(pageNum))
|
|
527
|
+
continue
|
|
528
|
+
|
|
529
|
+
if (entry.isIntersecting) {
|
|
530
|
+
visiblePages.value.add(pageNum)
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
visiblePages.value.delete(pageNum)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
scheduleRenderVisiblePages()
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
root: scrollContainerRef.value,
|
|
540
|
+
rootMargin: '300px 0px',
|
|
541
|
+
},
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
for (const [pageNum, el] of pageRefs.value) {
|
|
545
|
+
el.setAttribute('data-page-num', String(pageNum))
|
|
546
|
+
pdfObserver.value.observe(el)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Render first pages immediately — don't wait for async observer callback
|
|
550
|
+
visiblePages.value.add(1)
|
|
551
|
+
void renderVisiblePages()
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function renderFallbackAllPages() {
|
|
555
|
+
const handle = pdfDocHandle.value
|
|
556
|
+
if (!handle)
|
|
557
|
+
return
|
|
250
558
|
for (let pageNum = 1; pageNum <= handle.numPages; pageNum++) {
|
|
251
559
|
await renderPdfPage(pageNum)
|
|
252
560
|
}
|
|
253
561
|
}
|
|
254
562
|
|
|
563
|
+
async function precomputePageDimensions() {
|
|
564
|
+
const handle = pdfDocHandle.value
|
|
565
|
+
if (!handle)
|
|
566
|
+
return
|
|
567
|
+
|
|
568
|
+
const priorityPages = getInitialDimensionPages(handle.numPages)
|
|
569
|
+
const generation = ++dimensionGeneration
|
|
570
|
+
|
|
571
|
+
await primePageDimensions(handle, priorityPages, generation)
|
|
572
|
+
void warmRemainingPageDimensions(handle, priorityPages, generation)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function getPagePlaceholderStyle(pageNum: number) {
|
|
576
|
+
const dims = pageDimensions.value.get(pageNum) ?? defaultPageDimensions.value
|
|
577
|
+
if (!dims || renderedPages.value.has(pageNum))
|
|
578
|
+
return undefined
|
|
579
|
+
return {
|
|
580
|
+
minWidth: `${dims.width}px`,
|
|
581
|
+
minHeight: `${dims.height}px`,
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function resetPdfRenderState() {
|
|
586
|
+
dimensionGeneration++
|
|
587
|
+
pageRenderGeneration++
|
|
588
|
+
inFlightPageRenders.clear()
|
|
589
|
+
|
|
590
|
+
if (renderRafId !== null) {
|
|
591
|
+
cancelAnimationFrame(renderRafId)
|
|
592
|
+
renderRafId = null
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
pdfObserver.value?.disconnect()
|
|
596
|
+
pdfObserver.value = null
|
|
597
|
+
visiblePages.value.clear()
|
|
598
|
+
pageDimensions.value.clear()
|
|
599
|
+
defaultPageDimensions.value = null
|
|
600
|
+
|
|
601
|
+
clearAllPageCanvases()
|
|
602
|
+
renderedPages.value.clear()
|
|
603
|
+
currentDocumentPage.value = 1
|
|
604
|
+
totalPages.value = 0
|
|
605
|
+
}
|
|
606
|
+
|
|
255
607
|
async function reRenderAllPdfPages() {
|
|
608
|
+
const handle = pdfDocHandle.value
|
|
609
|
+
if (!handle)
|
|
610
|
+
return
|
|
611
|
+
|
|
256
612
|
if (isRendering) {
|
|
257
613
|
pendingReRender = true
|
|
258
614
|
return
|
|
259
615
|
}
|
|
616
|
+
|
|
260
617
|
isRendering = true
|
|
618
|
+
|
|
261
619
|
try {
|
|
620
|
+
pageRenderGeneration++
|
|
621
|
+
inFlightPageRenders.clear()
|
|
622
|
+
clearAllPageCanvases()
|
|
262
623
|
renderedPages.value.clear()
|
|
624
|
+
pageDimensions.value.clear()
|
|
625
|
+
defaultPageDimensions.value = null
|
|
626
|
+
|
|
627
|
+
const priorityPages = getPagesToRender(handle.numPages)
|
|
628
|
+
const generation = ++dimensionGeneration
|
|
629
|
+
|
|
630
|
+
await primePageDimensions(handle, priorityPages, generation)
|
|
631
|
+
void warmRemainingPageDimensions(handle, priorityPages, generation)
|
|
263
632
|
await nextTick()
|
|
264
|
-
|
|
633
|
+
if (visiblePages.value.size === 0) {
|
|
634
|
+
visiblePages.value.add(1)
|
|
635
|
+
}
|
|
636
|
+
await renderVisiblePages()
|
|
265
637
|
}
|
|
266
638
|
finally {
|
|
267
639
|
isRendering = false
|
|
640
|
+
|
|
268
641
|
if (pendingReRender) {
|
|
269
642
|
pendingReRender = false
|
|
270
643
|
void reRenderAllPdfPages().then(() => {
|
|
@@ -274,6 +647,7 @@ async function reRenderAllPdfPages() {
|
|
|
274
647
|
}
|
|
275
648
|
})
|
|
276
649
|
}
|
|
650
|
+
|
|
277
651
|
else if (pendingScrollPage !== null) {
|
|
278
652
|
scrollToPage(pendingScrollPage)
|
|
279
653
|
pendingScrollPage = null
|
|
@@ -281,7 +655,17 @@ async function reRenderAllPdfPages() {
|
|
|
281
655
|
}
|
|
282
656
|
}
|
|
283
657
|
|
|
284
|
-
watch(() => props.file, async (newFile) => {
|
|
658
|
+
watch(() => props.file, async (newFile, oldFile) => {
|
|
659
|
+
const isSamePdfRecompute = newFile?.fileType === 'application/pdf'
|
|
660
|
+
&& pdfDocHandle.value
|
|
661
|
+
&& newFile?.fileName === oldFile?.fileName
|
|
662
|
+
&& newFile?.fileUrl === oldFile?.fileUrl
|
|
663
|
+
|
|
664
|
+
if (isSamePdfRecompute) {
|
|
665
|
+
return
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
resetPdfRenderState()
|
|
285
669
|
pdfDocHandle.value?.destroy()
|
|
286
670
|
pdfDocHandle.value = null
|
|
287
671
|
pdfLoadError.value = null
|
|
@@ -327,8 +711,11 @@ watch(() => props.file, async (newFile) => {
|
|
|
327
711
|
pdfDocHandle.value = doc
|
|
328
712
|
totalPages.value = doc.numPages
|
|
329
713
|
|
|
714
|
+
await precomputePageDimensions()
|
|
330
715
|
await nextTick()
|
|
331
|
-
|
|
716
|
+
if (scrollContainerRef.value) {
|
|
717
|
+
setupPdfObserver()
|
|
718
|
+
}
|
|
332
719
|
|
|
333
720
|
const scrollTarget = (props.highlightPage && props.highlightPage > 0)
|
|
334
721
|
? props.highlightPage
|
|
@@ -346,6 +733,7 @@ watch(() => props.file, async (newFile) => {
|
|
|
346
733
|
}, { immediate: true, deep: true })
|
|
347
734
|
|
|
348
735
|
onBeforeUnmount(() => {
|
|
736
|
+
resetPdfRenderState()
|
|
349
737
|
pdfDocHandle.value?.destroy()
|
|
350
738
|
})
|
|
351
739
|
|
|
@@ -484,61 +872,111 @@ watch([() => props.highlightText, () => props.highlightPage, () => props.highlig
|
|
|
484
872
|
@mouseenter="handleContainerMouseEnter"
|
|
485
873
|
@mouseleave="handleContainerMouseLeave"
|
|
486
874
|
>
|
|
487
|
-
<
|
|
488
|
-
|
|
489
|
-
:class="cn({
|
|
490
|
-
'pt-56px': !props.segmentTab && props.file.fileType === 'application/pdf',
|
|
491
|
-
'pt-88px': props.segmentTab && props.file.fileType === 'application/pdf',
|
|
492
|
-
'px-46px': variant === 'default',
|
|
493
|
-
'px-32px': variant === 'minimal',
|
|
494
|
-
})"
|
|
495
|
-
>
|
|
875
|
+
<template v-if="isLoading || props.loading">
|
|
876
|
+
<!-- PDF skeleton: mimics the real PDF page stack -->
|
|
496
877
|
<div
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
'
|
|
501
|
-
'
|
|
878
|
+
v-if="props.file.fileType === 'application/pdf'"
|
|
879
|
+
:class="cn('flex flex-col items-center gap-8px pb-80px', {
|
|
880
|
+
'pt-56px': !props.segmentTab,
|
|
881
|
+
'pt-88px': props.segmentTab,
|
|
882
|
+
'px-28px': true,
|
|
502
883
|
})"
|
|
503
884
|
>
|
|
504
|
-
<div
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
885
|
+
<div
|
|
886
|
+
v-for="pageNum in 2" :key="pageNum"
|
|
887
|
+
relative bg-white rounded-12px overflow-hidden mx-auto
|
|
888
|
+
:class="cn({
|
|
889
|
+
'w-446px min-h-631px': variant === 'default',
|
|
890
|
+
'w-256px min-h-363px': variant === 'minimal',
|
|
891
|
+
})"
|
|
892
|
+
>
|
|
893
|
+
<!-- Page badge -->
|
|
894
|
+
<div absolute top-8px left-8px flex items-center w-fit h-16px>
|
|
895
|
+
<div
|
|
896
|
+
overflow-hidden rounded-4px
|
|
897
|
+
:class="cn({
|
|
898
|
+
'w-48px h-16px': variant === 'default',
|
|
899
|
+
'w-36px h-14px': variant === 'minimal',
|
|
900
|
+
})"
|
|
901
|
+
>
|
|
902
|
+
<TelaSkeleton w-full h-full bg-gray-100 />
|
|
903
|
+
</div>
|
|
511
904
|
</div>
|
|
512
|
-
|
|
513
|
-
<div flex="~ col" gap-16px>
|
|
905
|
+
<!-- Content lines -->
|
|
514
906
|
<div
|
|
515
|
-
|
|
516
|
-
:
|
|
517
|
-
|
|
907
|
+
flex="~ col"
|
|
908
|
+
:class="cn({
|
|
909
|
+
'gap-14px px-32px pt-44px pb-28px': variant === 'default',
|
|
910
|
+
'gap-10px px-16px pt-32px pb-14px': variant === 'minimal',
|
|
911
|
+
})"
|
|
518
912
|
>
|
|
519
|
-
<
|
|
913
|
+
<div
|
|
914
|
+
v-for="(width, index) in [100, 88, 72, 64, 78, 92, 60, 44, 85, 70, 56, 36]" :key="index"
|
|
915
|
+
overflow-hidden rounded-8px
|
|
916
|
+
:h="variant === 'default' ? '14px' : '10px'"
|
|
917
|
+
:style="{ width: `${width}%` }"
|
|
918
|
+
>
|
|
919
|
+
<TelaSkeleton w-full h-full rounded-8px bg-gray-100 />
|
|
920
|
+
</div>
|
|
520
921
|
</div>
|
|
521
922
|
</div>
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
923
|
+
</div>
|
|
924
|
+
|
|
925
|
+
<!-- Document skeleton for non-PDF files -->
|
|
926
|
+
<div
|
|
927
|
+
v-else
|
|
928
|
+
:class="cn({
|
|
929
|
+
'px-46px': variant === 'default',
|
|
930
|
+
'px-32px': variant === 'minimal',
|
|
931
|
+
})"
|
|
932
|
+
>
|
|
933
|
+
<div
|
|
934
|
+
bg-white rounded-12px
|
|
935
|
+
flex="~ col" justify-between mx-auto relative
|
|
936
|
+
:class="cn({
|
|
937
|
+
'w-446px min-h-631px px-32px py-28px': variant === 'default',
|
|
938
|
+
'w-256px min-h-363px px-16px py-14px': variant === 'minimal',
|
|
939
|
+
})"
|
|
940
|
+
>
|
|
941
|
+
<div flex="~ col" gap-16px>
|
|
942
|
+
<div
|
|
943
|
+
v-for="(width, index) in [100, 88, 72, 64]" :key="index"
|
|
944
|
+
:h="variant === 'default' ? '16px' : '12px'" overflow-hidden rounded-8px
|
|
945
|
+
:style="{ width: `${width}%` }"
|
|
946
|
+
>
|
|
947
|
+
<TelaSkeleton w-full h-full rounded-8px bg-gray-100 />
|
|
948
|
+
</div>
|
|
529
949
|
</div>
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
950
|
+
<div flex="~ col" gap-16px>
|
|
951
|
+
<div
|
|
952
|
+
v-for="(width, index) in [72, 92, 64, 48]" :key="index"
|
|
953
|
+
:h="variant === 'default' ? '16px' : '12px'" overflow-hidden rounded-8px
|
|
954
|
+
:style="{ width: `${width}%` }"
|
|
955
|
+
>
|
|
956
|
+
<TelaSkeleton w-full h-full rounded-8px bg-gray-100 />
|
|
957
|
+
</div>
|
|
958
|
+
</div>
|
|
959
|
+
<div flex="~ col" gap-16px>
|
|
960
|
+
<div
|
|
961
|
+
v-for="(width, index) in [70, 90, 56, 36]" :key="index"
|
|
962
|
+
:h="variant === 'default' ? '16px' : '12px'" overflow-hidden rounded-8px
|
|
963
|
+
:style="{ width: `${width}%` }"
|
|
964
|
+
>
|
|
965
|
+
<TelaSkeleton w-full h-full rounded-8px bg-gray-100 />
|
|
966
|
+
</div>
|
|
967
|
+
</div>
|
|
968
|
+
<div v-if="variant === 'default'" flex="~ col" gap-16px>
|
|
969
|
+
<div
|
|
970
|
+
v-for="(width, index) in [82, 56, 32, 20]" :key="index"
|
|
971
|
+
h-16px overflow-hidden rounded-8px
|
|
972
|
+
:style="{ width: `${width}%` }"
|
|
973
|
+
>
|
|
974
|
+
<TelaSkeleton w-full h-full rounded-8px bg-gray-100 />
|
|
975
|
+
</div>
|
|
538
976
|
</div>
|
|
539
977
|
</div>
|
|
540
978
|
</div>
|
|
541
|
-
</
|
|
979
|
+
</template>
|
|
542
980
|
|
|
543
981
|
<template v-else-if="error">
|
|
544
982
|
<div flex="~ col" items-center justify-center h-400px>
|
|
@@ -688,8 +1126,10 @@ watch([() => props.highlightText, () => props.highlightPage, () => props.highlig
|
|
|
688
1126
|
:key="pageNum"
|
|
689
1127
|
:ref="(el) => setPageRef(pageNum, el as HTMLElement)"
|
|
690
1128
|
data-pdf-page-card
|
|
1129
|
+
:data-page-num="pageNum"
|
|
691
1130
|
relative bg-white rounded-12px overflow-hidden mx-auto
|
|
692
1131
|
:class="cn(variant === 'minimal' && 'w-256px pdf-page-card--minimal')"
|
|
1132
|
+
:style="getPagePlaceholderStyle(pageNum)"
|
|
693
1133
|
>
|
|
694
1134
|
<canvas />
|
|
695
1135
|
<div
|
|
@@ -35,6 +35,8 @@ const props = withDefaults(
|
|
|
35
35
|
highlightPage?: number | null
|
|
36
36
|
/** When true, uses exact matching instead of fuzzy word-based matching. */
|
|
37
37
|
highlightExact?: boolean
|
|
38
|
+
/** When true, shows the loading skeleton. Pass when file content is being fetched externally. */
|
|
39
|
+
loading?: boolean
|
|
38
40
|
}>(),
|
|
39
41
|
{
|
|
40
42
|
variant: 'default',
|
|
@@ -126,6 +128,7 @@ const fileReaderKey = computed(() => {
|
|
|
126
128
|
:pdf-loader="pdfLoader"
|
|
127
129
|
:labels="contentLabels"
|
|
128
130
|
:variant="variant"
|
|
131
|
+
:loading="loading"
|
|
129
132
|
:highlight-text="highlightText"
|
|
130
133
|
:highlight-page="highlightPage"
|
|
131
134
|
:highlight-exact="highlightExact"
|
|
@@ -69,5 +69,6 @@ export interface PdfDocumentHandle {
|
|
|
69
69
|
highlightPage?: number | null
|
|
70
70
|
highlightExact?: boolean
|
|
71
71
|
}) => Promise<void>
|
|
72
|
+
getPageDimensions?: (opts: { pageNum: number, scale: number }) => Promise<{ width: number, height: number } | null>
|
|
72
73
|
destroy: () => void
|
|
73
74
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { TooltipContent, TooltipPortal, useForwardPropsEmits } from 'reka-ui'
|
|
3
3
|
import type { TooltipContentEmits, TooltipContentProps } from 'reka-ui'
|
|
4
4
|
import { reactiveOmit } from '@vueuse/core'
|
|
5
|
+
import { inject } from 'vue'
|
|
5
6
|
import type { HTMLAttributes } from 'vue'
|
|
6
7
|
|
|
7
8
|
defineOptions({
|
|
@@ -19,10 +20,12 @@ const emits = defineEmits<TooltipContentEmits & {
|
|
|
19
20
|
const delegatedProps = reactiveOmit(props, 'class')
|
|
20
21
|
|
|
21
22
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|
23
|
+
|
|
24
|
+
const disablePortal = inject<boolean>('tela-disable-tooltip-portal', false)
|
|
22
25
|
</script>
|
|
23
26
|
|
|
24
27
|
<template>
|
|
25
|
-
<TooltipPortal>
|
|
28
|
+
<TooltipPortal :disabled="disablePortal">
|
|
26
29
|
<TooltipContent
|
|
27
30
|
v-bind="{ ...forwarded, ...$attrs }"
|
|
28
31
|
:style="{ pointerEvents: 'auto' }"
|