@meistrari/tela-build 1.22.0 → 1.23.0
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.
|
@@ -16,10 +16,19 @@ const props = withDefaults(
|
|
|
16
16
|
/** Load PDF document from URL. Required for PDF preview. */
|
|
17
17
|
pdfLoader?: (url: string) => Promise<PdfDocumentHandle | null>
|
|
18
18
|
labels?: PreviewContentLabels
|
|
19
|
+
/** Text to highlight on the PDF pages. When set, a text layer is rendered with yellow highlights. */
|
|
20
|
+
highlightText?: string | null
|
|
21
|
+
/** The page to scroll to and highlight on. When set, scrolls to this page and restricts highlights to it. */
|
|
22
|
+
highlightPage?: number | null
|
|
23
|
+
/** When true, uses exact matching instead of fuzzy word-based matching for highlights. */
|
|
24
|
+
highlightExact?: boolean
|
|
19
25
|
}>(),
|
|
20
26
|
{
|
|
21
27
|
variant: 'default',
|
|
22
28
|
segmentTab: undefined,
|
|
29
|
+
highlightText: undefined,
|
|
30
|
+
highlightPage: undefined,
|
|
31
|
+
highlightExact: false,
|
|
23
32
|
},
|
|
24
33
|
)
|
|
25
34
|
|
|
@@ -80,7 +89,11 @@ const pdfDocHandle = ref<PdfDocumentHandle | null>(null)
|
|
|
80
89
|
const pdfLoadError = ref<string | null>(null)
|
|
81
90
|
|
|
82
91
|
const pageRefs = ref<Map<number, HTMLElement>>(new Map())
|
|
92
|
+
const textLayerRefs = ref<Map<number, HTMLDivElement>>(new Map())
|
|
83
93
|
const renderedPages = ref<Set<number>>(new Set())
|
|
94
|
+
let isRendering = false
|
|
95
|
+
let pendingReRender = false
|
|
96
|
+
let pendingScrollPage: number | null = null
|
|
84
97
|
|
|
85
98
|
function zoomIn() {
|
|
86
99
|
if (scale.value < 3) {
|
|
@@ -185,6 +198,15 @@ function setPageRef(pageNum: number, el: HTMLElement | null) {
|
|
|
185
198
|
}
|
|
186
199
|
}
|
|
187
200
|
|
|
201
|
+
function setTextLayerRef(pageNum: number, el: HTMLDivElement | null) {
|
|
202
|
+
if (el) {
|
|
203
|
+
textLayerRefs.value.set(pageNum, el)
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
textLayerRefs.value.delete(pageNum)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
188
210
|
async function renderPdfPage(pageNum: number) {
|
|
189
211
|
const handle = pdfDocHandle.value
|
|
190
212
|
|
|
@@ -201,10 +223,16 @@ async function renderPdfPage(pageNum: number) {
|
|
|
201
223
|
if (!canvas)
|
|
202
224
|
return
|
|
203
225
|
|
|
226
|
+
const textLayer = textLayerRefs.value.get(pageNum) ?? null
|
|
227
|
+
|
|
204
228
|
await handle.renderPage({
|
|
205
229
|
pageNum,
|
|
206
230
|
canvas: canvas as HTMLCanvasElement,
|
|
207
231
|
scale: scale.value,
|
|
232
|
+
textLayer,
|
|
233
|
+
highlight: props.highlightText,
|
|
234
|
+
highlightPage: props.highlightPage,
|
|
235
|
+
highlightExact: props.highlightExact,
|
|
208
236
|
})
|
|
209
237
|
|
|
210
238
|
renderedPages.value.add(pageNum)
|
|
@@ -222,9 +250,32 @@ async function renderAllPdfPages() {
|
|
|
222
250
|
}
|
|
223
251
|
|
|
224
252
|
async function reRenderAllPdfPages() {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
253
|
+
if (isRendering) {
|
|
254
|
+
pendingReRender = true
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
isRendering = true
|
|
258
|
+
try {
|
|
259
|
+
renderedPages.value.clear()
|
|
260
|
+
await nextTick()
|
|
261
|
+
await renderAllPdfPages()
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
isRendering = false
|
|
265
|
+
if (pendingReRender) {
|
|
266
|
+
pendingReRender = false
|
|
267
|
+
void reRenderAllPdfPages().then(() => {
|
|
268
|
+
if (pendingScrollPage !== null) {
|
|
269
|
+
scrollToPage(pendingScrollPage)
|
|
270
|
+
pendingScrollPage = null
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
else if (pendingScrollPage !== null) {
|
|
275
|
+
scrollToPage(pendingScrollPage)
|
|
276
|
+
pendingScrollPage = null
|
|
277
|
+
}
|
|
278
|
+
}
|
|
228
279
|
}
|
|
229
280
|
|
|
230
281
|
watch(() => props.file, async (newFile) => {
|
|
@@ -275,6 +326,15 @@ watch(() => props.file, async (newFile) => {
|
|
|
275
326
|
|
|
276
327
|
await nextTick()
|
|
277
328
|
await renderAllPdfPages()
|
|
329
|
+
|
|
330
|
+
const scrollTarget = (props.highlightPage && props.highlightPage > 0)
|
|
331
|
+
? props.highlightPage
|
|
332
|
+
: pendingScrollPage
|
|
333
|
+
if (scrollTarget) {
|
|
334
|
+
pendingScrollPage = null
|
|
335
|
+
await nextTick()
|
|
336
|
+
scrollToPage(scrollTarget)
|
|
337
|
+
}
|
|
278
338
|
}
|
|
279
339
|
else {
|
|
280
340
|
pdfLoadError.value = labels.value.failedToLoadFile
|
|
@@ -392,6 +452,20 @@ function handleHotkey(e: KeyboardEvent) {
|
|
|
392
452
|
}
|
|
393
453
|
|
|
394
454
|
useEventListener('keydown', handleHotkey)
|
|
455
|
+
|
|
456
|
+
watch([() => props.highlightText, () => props.highlightPage, () => props.highlightExact], async () => {
|
|
457
|
+
if (props.highlightPage && props.highlightPage > 0) {
|
|
458
|
+
pendingScrollPage = props.highlightPage
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
pendingScrollPage = null
|
|
462
|
+
}
|
|
463
|
+
if (pdfDocHandle.value) {
|
|
464
|
+
await reRenderAllPdfPages()
|
|
465
|
+
}
|
|
466
|
+
// When doc isn't loaded yet, keep pendingScrollPage — it will be
|
|
467
|
+
// consumed by reRenderAllPdfPages().finally once the PDF renders.
|
|
468
|
+
})
|
|
395
469
|
</script>
|
|
396
470
|
|
|
397
471
|
<template>
|
|
@@ -615,6 +689,12 @@ useEventListener('keydown', handleHotkey)
|
|
|
615
689
|
:class="cn(variant === 'minimal' && 'w-256px pdf-page-card--minimal')"
|
|
616
690
|
>
|
|
617
691
|
<canvas />
|
|
692
|
+
<div
|
|
693
|
+
:ref="(el) => setTextLayerRef(pageNum, el as HTMLDivElement)"
|
|
694
|
+
data-text-layer
|
|
695
|
+
class="absolute top-0 left-0 w-full h-full z-1"
|
|
696
|
+
style="pointer-events: none; mix-blend-mode: multiply; line-height: 1; opacity: 0.4;"
|
|
697
|
+
/>
|
|
618
698
|
<div
|
|
619
699
|
data-pdf-page-badge
|
|
620
700
|
absolute z-2 top-8px left-8px
|
|
@@ -29,6 +29,12 @@ const props = withDefaults(
|
|
|
29
29
|
pdfLoader?: (url: string) => Promise<import('./types').PdfDocumentHandle | null>
|
|
30
30
|
/** Labels for PreviewContent (failedToLoadFile, page, download, etc.) */
|
|
31
31
|
contentLabels?: import('./types').PreviewContentLabels
|
|
32
|
+
/** Text to highlight on the PDF pages. Passed to PreviewContent. */
|
|
33
|
+
highlightText?: string | null
|
|
34
|
+
/** Page to scroll to and highlight on. Passed to PreviewContent. */
|
|
35
|
+
highlightPage?: number | null
|
|
36
|
+
/** When true, uses exact matching instead of fuzzy word-based matching. */
|
|
37
|
+
highlightExact?: boolean
|
|
32
38
|
}>(),
|
|
33
39
|
{
|
|
34
40
|
variant: 'default',
|
|
@@ -120,6 +126,9 @@ const fileReaderKey = computed(() => {
|
|
|
120
126
|
:pdf-loader="pdfLoader"
|
|
121
127
|
:labels="contentLabels"
|
|
122
128
|
:variant="variant"
|
|
129
|
+
:highlight-text="highlightText"
|
|
130
|
+
:highlight-page="highlightPage"
|
|
131
|
+
:highlight-exact="highlightExact"
|
|
123
132
|
class="h-full"
|
|
124
133
|
@fullscreen="emit('fullscreen')"
|
|
125
134
|
/>
|
|
@@ -60,6 +60,14 @@ export interface PreviewSelectVariableLabels {
|
|
|
60
60
|
|
|
61
61
|
export interface PdfDocumentHandle {
|
|
62
62
|
numPages: number
|
|
63
|
-
renderPage: (opts: {
|
|
63
|
+
renderPage: (opts: {
|
|
64
|
+
pageNum: number
|
|
65
|
+
canvas: HTMLCanvasElement
|
|
66
|
+
scale: number
|
|
67
|
+
textLayer?: HTMLDivElement | null
|
|
68
|
+
highlight?: string | null
|
|
69
|
+
highlightPage?: number | null
|
|
70
|
+
highlightExact?: boolean
|
|
71
|
+
}) => Promise<void>
|
|
64
72
|
destroy: () => void
|
|
65
73
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter, Ref } from 'vue'
|
|
2
|
+
import { nextTick, ref, toValue } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface CitationTarget {
|
|
5
|
+
file: string
|
|
6
|
+
page: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function useCitationNavigation(options: {
|
|
10
|
+
citations: MaybeRefOrGetter<Record<string, any> | null | undefined>
|
|
11
|
+
}) {
|
|
12
|
+
const highlightText = ref<string | null>(null)
|
|
13
|
+
const highlightPage = ref<number | null>(null)
|
|
14
|
+
const highlightExact = ref(false)
|
|
15
|
+
const activeFile = ref<string | null>(null)
|
|
16
|
+
|
|
17
|
+
function lookupCitation(path: string): CitationTarget | null {
|
|
18
|
+
const citations = toValue(options.citations)
|
|
19
|
+
if (!citations)
|
|
20
|
+
return null
|
|
21
|
+
|
|
22
|
+
const parts = path.split('.')
|
|
23
|
+
let current: any = citations
|
|
24
|
+
|
|
25
|
+
for (const part of parts) {
|
|
26
|
+
if (current == null)
|
|
27
|
+
return null
|
|
28
|
+
if (Array.isArray(current)) {
|
|
29
|
+
const idx = Number.parseInt(part, 10)
|
|
30
|
+
if (Number.isNaN(idx))
|
|
31
|
+
return null
|
|
32
|
+
current = current[idx]
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
current = current[part]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!current || typeof current !== 'object' || !('page' in current))
|
|
40
|
+
return null
|
|
41
|
+
|
|
42
|
+
if ('file' in current && current.file)
|
|
43
|
+
return { file: current.file, page: current.page }
|
|
44
|
+
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function navigateToCitation(path: string, value?: any): void {
|
|
49
|
+
const citation = lookupCitation(path)
|
|
50
|
+
if (!citation) {
|
|
51
|
+
clearCitation()
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Reset then set via nextTick to force watchers to re-trigger
|
|
56
|
+
// even when clicking the same citation field twice
|
|
57
|
+
highlightText.value = null
|
|
58
|
+
highlightPage.value = null
|
|
59
|
+
highlightExact.value = false
|
|
60
|
+
activeFile.value = null
|
|
61
|
+
|
|
62
|
+
nextTick(() => {
|
|
63
|
+
activeFile.value = citation.file
|
|
64
|
+
highlightPage.value = citation.page
|
|
65
|
+
highlightText.value = value != null
|
|
66
|
+
? (typeof value === 'string' ? value : String(value))
|
|
67
|
+
: null
|
|
68
|
+
highlightExact.value = true
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function clearCitation(): void {
|
|
73
|
+
highlightText.value = null
|
|
74
|
+
highlightPage.value = null
|
|
75
|
+
highlightExact.value = false
|
|
76
|
+
activeFile.value = null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
highlightText: highlightText as Ref<string | null>,
|
|
81
|
+
highlightPage: highlightPage as Ref<number | null>,
|
|
82
|
+
highlightExact: highlightExact as Ref<boolean>,
|
|
83
|
+
activeFile: activeFile as Ref<string | null>,
|
|
84
|
+
lookupCitation,
|
|
85
|
+
navigateToCitation,
|
|
86
|
+
clearCitation,
|
|
87
|
+
}
|
|
88
|
+
}
|