@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
- renderedPages.value.clear()
226
- await nextTick()
227
- await renderAllPdfPages()
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: { pageNum: number, canvas: HTMLCanvasElement, scale: number }) => Promise<void>
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-build",
3
- "version": "1.22.0",
3
+ "version": "1.23.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",