@meistrari/tela-build 1.22.0 → 1.24.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,22 @@ 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
|
|
25
|
+
/** Override the initial zoom scale. Defaults to 1 for minimal, 0.75 for default. */
|
|
26
|
+
initialScale?: number
|
|
19
27
|
}>(),
|
|
20
28
|
{
|
|
21
29
|
variant: 'default',
|
|
22
30
|
segmentTab: undefined,
|
|
31
|
+
highlightText: undefined,
|
|
32
|
+
highlightPage: undefined,
|
|
33
|
+
highlightExact: false,
|
|
34
|
+
initialScale: undefined,
|
|
23
35
|
},
|
|
24
36
|
)
|
|
25
37
|
|
|
@@ -70,7 +82,7 @@ const currentDocumentPage = ref(1)
|
|
|
70
82
|
const isPageInputFocused = ref(false)
|
|
71
83
|
const isProgrammaticScroll = ref(false)
|
|
72
84
|
|
|
73
|
-
const scale = ref(props.variant === 'minimal' ? 1 : 0.75)
|
|
85
|
+
const scale = ref(props.initialScale ?? (props.variant === 'minimal' ? 1 : 0.75))
|
|
74
86
|
const scrollContainerRef = ref<HTMLElement | null>(null)
|
|
75
87
|
const isDragging = ref(false)
|
|
76
88
|
const dragEnabled = ref(false)
|
|
@@ -80,7 +92,11 @@ const pdfDocHandle = ref<PdfDocumentHandle | null>(null)
|
|
|
80
92
|
const pdfLoadError = ref<string | null>(null)
|
|
81
93
|
|
|
82
94
|
const pageRefs = ref<Map<number, HTMLElement>>(new Map())
|
|
95
|
+
const textLayerRefs = ref<Map<number, HTMLDivElement>>(new Map())
|
|
83
96
|
const renderedPages = ref<Set<number>>(new Set())
|
|
97
|
+
let isRendering = false
|
|
98
|
+
let pendingReRender = false
|
|
99
|
+
let pendingScrollPage: number | null = null
|
|
84
100
|
|
|
85
101
|
function zoomIn() {
|
|
86
102
|
if (scale.value < 3) {
|
|
@@ -185,6 +201,15 @@ function setPageRef(pageNum: number, el: HTMLElement | null) {
|
|
|
185
201
|
}
|
|
186
202
|
}
|
|
187
203
|
|
|
204
|
+
function setTextLayerRef(pageNum: number, el: HTMLDivElement | null) {
|
|
205
|
+
if (el) {
|
|
206
|
+
textLayerRefs.value.set(pageNum, el)
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
textLayerRefs.value.delete(pageNum)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
188
213
|
async function renderPdfPage(pageNum: number) {
|
|
189
214
|
const handle = pdfDocHandle.value
|
|
190
215
|
|
|
@@ -201,10 +226,16 @@ async function renderPdfPage(pageNum: number) {
|
|
|
201
226
|
if (!canvas)
|
|
202
227
|
return
|
|
203
228
|
|
|
229
|
+
const textLayer = textLayerRefs.value.get(pageNum) ?? null
|
|
230
|
+
|
|
204
231
|
await handle.renderPage({
|
|
205
232
|
pageNum,
|
|
206
233
|
canvas: canvas as HTMLCanvasElement,
|
|
207
234
|
scale: scale.value,
|
|
235
|
+
textLayer,
|
|
236
|
+
highlight: props.highlightText,
|
|
237
|
+
highlightPage: props.highlightPage,
|
|
238
|
+
highlightExact: props.highlightExact,
|
|
208
239
|
})
|
|
209
240
|
|
|
210
241
|
renderedPages.value.add(pageNum)
|
|
@@ -222,9 +253,32 @@ async function renderAllPdfPages() {
|
|
|
222
253
|
}
|
|
223
254
|
|
|
224
255
|
async function reRenderAllPdfPages() {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
256
|
+
if (isRendering) {
|
|
257
|
+
pendingReRender = true
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
isRendering = true
|
|
261
|
+
try {
|
|
262
|
+
renderedPages.value.clear()
|
|
263
|
+
await nextTick()
|
|
264
|
+
await renderAllPdfPages()
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
isRendering = false
|
|
268
|
+
if (pendingReRender) {
|
|
269
|
+
pendingReRender = false
|
|
270
|
+
void reRenderAllPdfPages().then(() => {
|
|
271
|
+
if (pendingScrollPage !== null) {
|
|
272
|
+
scrollToPage(pendingScrollPage)
|
|
273
|
+
pendingScrollPage = null
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
else if (pendingScrollPage !== null) {
|
|
278
|
+
scrollToPage(pendingScrollPage)
|
|
279
|
+
pendingScrollPage = null
|
|
280
|
+
}
|
|
281
|
+
}
|
|
228
282
|
}
|
|
229
283
|
|
|
230
284
|
watch(() => props.file, async (newFile) => {
|
|
@@ -275,6 +329,15 @@ watch(() => props.file, async (newFile) => {
|
|
|
275
329
|
|
|
276
330
|
await nextTick()
|
|
277
331
|
await renderAllPdfPages()
|
|
332
|
+
|
|
333
|
+
const scrollTarget = (props.highlightPage && props.highlightPage > 0)
|
|
334
|
+
? props.highlightPage
|
|
335
|
+
: pendingScrollPage
|
|
336
|
+
if (scrollTarget) {
|
|
337
|
+
pendingScrollPage = null
|
|
338
|
+
await nextTick()
|
|
339
|
+
scrollToPage(scrollTarget)
|
|
340
|
+
}
|
|
278
341
|
}
|
|
279
342
|
else {
|
|
280
343
|
pdfLoadError.value = labels.value.failedToLoadFile
|
|
@@ -392,6 +455,20 @@ function handleHotkey(e: KeyboardEvent) {
|
|
|
392
455
|
}
|
|
393
456
|
|
|
394
457
|
useEventListener('keydown', handleHotkey)
|
|
458
|
+
|
|
459
|
+
watch([() => props.highlightText, () => props.highlightPage, () => props.highlightExact], async () => {
|
|
460
|
+
if (props.highlightPage && props.highlightPage > 0) {
|
|
461
|
+
pendingScrollPage = props.highlightPage
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
pendingScrollPage = null
|
|
465
|
+
}
|
|
466
|
+
if (pdfDocHandle.value) {
|
|
467
|
+
await reRenderAllPdfPages()
|
|
468
|
+
}
|
|
469
|
+
// When doc isn't loaded yet, keep pendingScrollPage — it will be
|
|
470
|
+
// consumed by reRenderAllPdfPages().finally once the PDF renders.
|
|
471
|
+
})
|
|
395
472
|
</script>
|
|
396
473
|
|
|
397
474
|
<template>
|
|
@@ -615,6 +692,12 @@ useEventListener('keydown', handleHotkey)
|
|
|
615
692
|
:class="cn(variant === 'minimal' && 'w-256px pdf-page-card--minimal')"
|
|
616
693
|
>
|
|
617
694
|
<canvas />
|
|
695
|
+
<div
|
|
696
|
+
:ref="(el) => setTextLayerRef(pageNum, el as HTMLDivElement)"
|
|
697
|
+
data-text-layer
|
|
698
|
+
class="absolute top-0 left-0 w-full h-full z-1"
|
|
699
|
+
style="pointer-events: none; mix-blend-mode: multiply; line-height: 1; opacity: 0.4;"
|
|
700
|
+
/>
|
|
618
701
|
<div
|
|
619
702
|
data-pdf-page-badge
|
|
620
703
|
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
|
+
}
|