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