@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
- renderedPages.value.clear()
226
- await nextTick()
227
- await renderAllPdfPages()
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: { 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.24.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",