highlite 0.1.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.
- checksums.yaml +7 -0
- data/.claude/commands/publish.md +45 -0
- data/README.md +176 -0
- data/Rakefile +12 -0
- data/app/assets/stylesheets/highlite/viewer.css +607 -0
- data/app/helpers/highlite/application_helper.rb +20 -0
- data/app/javascript/highlite/controllers/.keep +0 -0
- data/app/javascript/highlite/controllers/highlight_controller.js +829 -0
- data/app/javascript/highlite/controllers/highlights_panel_controller.js +313 -0
- data/app/javascript/highlite/controllers/sidebar_controller.js +465 -0
- data/app/javascript/highlite/controllers/viewer_controller.js +373 -0
- data/app/javascript/highlite/index.js +30 -0
- data/app/javascript/highlite/lib/.keep +0 -0
- data/app/javascript/highlite/lib/highlight_store.js +235 -0
- data/app/javascript/highlite/lib/pdf_renderer.js +212 -0
- data/app/views/highlite/_floating_toolbar.html.erb +79 -0
- data/app/views/highlite/_left_sidebar.html.erb +43 -0
- data/app/views/highlite/_right_sidebar.html.erb +56 -0
- data/app/views/highlite/_toolbar.html.erb +63 -0
- data/app/views/highlite/_viewer.html.erb +63 -0
- data/config/importmap.rb +17 -0
- data/lib/generators/highlite/install/install_generator.rb +44 -0
- data/lib/generators/highlite/install/templates/_right_sidebar.html.erb.tt +16 -0
- data/lib/generators/highlite/install/templates/initializer.rb.tt +20 -0
- data/lib/highlite/configuration.rb +27 -0
- data/lib/highlite/engine.rb +26 -0
- data/lib/highlite/version.rb +5 -0
- data/lib/highlite.rb +17 -0
- data/sig/highlite.rbs +4 -0
- data/tasks/todo.md +129 -0
- data/test/dummy/Rakefile +3 -0
- data/test/dummy/app/controllers/application_controller.rb +2 -0
- data/test/dummy/app/controllers/documents_controller.rb +6 -0
- data/test/dummy/app/javascript/application.js +1 -0
- data/test/dummy/app/views/documents/show.html.erb +1 -0
- data/test/dummy/app/views/layouts/application.html.erb +13 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/config/application.rb +15 -0
- data/test/dummy/config/boot.rb +2 -0
- data/test/dummy/config/environment.rb +2 -0
- data/test/dummy/config/importmap.rb +3 -0
- data/test/dummy/config/routes.rb +3 -0
- data/test/dummy/config.ru +2 -0
- data/test/dummy/public/convention-over-configuration.pdf +0 -0
- metadata +117 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { PdfRenderer } from "highlite/lib/pdf_renderer"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SidebarController — Stimulus controller for the left sidebar.
|
|
6
|
+
*
|
|
7
|
+
* Provides two tabbed views:
|
|
8
|
+
* - **Outline** — Hierarchical TOC extracted from PDF metadata
|
|
9
|
+
* - **Pages** — Thumbnail previews for each page
|
|
10
|
+
*
|
|
11
|
+
* Targets:
|
|
12
|
+
* outlineTab - Outline tab button
|
|
13
|
+
* pagesTab - Pages tab button
|
|
14
|
+
* outlinePanel - Outline content container
|
|
15
|
+
* pagesPanel - Pages/thumbnails content container
|
|
16
|
+
* thumbnailsContainer - Container for thumbnail canvases
|
|
17
|
+
* pageCounter - "Page X of Y" display
|
|
18
|
+
*
|
|
19
|
+
* Listens for:
|
|
20
|
+
* highlite:document-loaded — receive outline + page count
|
|
21
|
+
* highlite:page-changed — update active thumbnail
|
|
22
|
+
* highlite:highlight-created/removed/cleared — update outline dots
|
|
23
|
+
*/
|
|
24
|
+
export default class extends Controller {
|
|
25
|
+
static targets = [
|
|
26
|
+
"outlineTab",
|
|
27
|
+
"pagesTab",
|
|
28
|
+
"outlinePanel",
|
|
29
|
+
"pagesPanel",
|
|
30
|
+
"thumbnailsContainer",
|
|
31
|
+
"pageCounter",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
connect() {
|
|
35
|
+
this._pageCount = 0
|
|
36
|
+
this._outline = []
|
|
37
|
+
this._activePage = 1
|
|
38
|
+
this._renderer = null
|
|
39
|
+
this._thumbnailObserver = null
|
|
40
|
+
this._renderedThumbnails = new Set()
|
|
41
|
+
|
|
42
|
+
// Bind event handlers
|
|
43
|
+
this._onDocumentLoaded = this._handleDocumentLoaded.bind(this)
|
|
44
|
+
this._onPageChanged = this._handlePageChanged.bind(this)
|
|
45
|
+
this._onHighlightChanged = this._handleHighlightChanged.bind(this)
|
|
46
|
+
|
|
47
|
+
// Listen on the parent viewer element
|
|
48
|
+
const viewer = this._getViewerElement()
|
|
49
|
+
if (viewer) {
|
|
50
|
+
viewer.addEventListener(
|
|
51
|
+
"highlite:document-loaded",
|
|
52
|
+
this._onDocumentLoaded
|
|
53
|
+
)
|
|
54
|
+
viewer.addEventListener(
|
|
55
|
+
"highlite:page-changed",
|
|
56
|
+
this._onPageChanged
|
|
57
|
+
)
|
|
58
|
+
viewer.addEventListener(
|
|
59
|
+
"highlite:highlight-created",
|
|
60
|
+
this._onHighlightChanged
|
|
61
|
+
)
|
|
62
|
+
viewer.addEventListener(
|
|
63
|
+
"highlite:highlight-removed",
|
|
64
|
+
this._onHighlightChanged
|
|
65
|
+
)
|
|
66
|
+
viewer.addEventListener(
|
|
67
|
+
"highlite:highlights-cleared",
|
|
68
|
+
this._onHighlightChanged
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
disconnect() {
|
|
74
|
+
const viewer = this._getViewerElement()
|
|
75
|
+
if (viewer) {
|
|
76
|
+
viewer.removeEventListener(
|
|
77
|
+
"highlite:document-loaded",
|
|
78
|
+
this._onDocumentLoaded
|
|
79
|
+
)
|
|
80
|
+
viewer.removeEventListener(
|
|
81
|
+
"highlite:page-changed",
|
|
82
|
+
this._onPageChanged
|
|
83
|
+
)
|
|
84
|
+
viewer.removeEventListener(
|
|
85
|
+
"highlite:highlight-created",
|
|
86
|
+
this._onHighlightChanged
|
|
87
|
+
)
|
|
88
|
+
viewer.removeEventListener(
|
|
89
|
+
"highlite:highlight-removed",
|
|
90
|
+
this._onHighlightChanged
|
|
91
|
+
)
|
|
92
|
+
viewer.removeEventListener(
|
|
93
|
+
"highlite:highlights-cleared",
|
|
94
|
+
this._onHighlightChanged
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (this._thumbnailObserver) {
|
|
99
|
+
this._thumbnailObserver.disconnect()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Actions — Tab switching
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Show the Outline tab.
|
|
109
|
+
*/
|
|
110
|
+
showOutline() {
|
|
111
|
+
this._setActiveTab("outline")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Show the Pages tab.
|
|
116
|
+
*/
|
|
117
|
+
showPages() {
|
|
118
|
+
this._setActiveTab("pages")
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_setActiveTab(tab) {
|
|
122
|
+
const isOutline = tab === "outline"
|
|
123
|
+
|
|
124
|
+
if (this.hasOutlineTabTarget) {
|
|
125
|
+
this.outlineTabTarget.classList.toggle(
|
|
126
|
+
"highlite-tab-active",
|
|
127
|
+
isOutline
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
if (this.hasPagesTabTarget) {
|
|
131
|
+
this.pagesTabTarget.classList.toggle(
|
|
132
|
+
"highlite-tab-active",
|
|
133
|
+
!isOutline
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
if (this.hasOutlinePanelTarget) {
|
|
137
|
+
this.outlinePanelTarget.classList.toggle("hidden", !isOutline)
|
|
138
|
+
}
|
|
139
|
+
if (this.hasPagesPanelTarget) {
|
|
140
|
+
this.pagesPanelTarget.classList.toggle("hidden", isOutline)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Event handlers
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Handle document loaded event — build outline and thumbnails.
|
|
150
|
+
* @param {CustomEvent} event
|
|
151
|
+
*/
|
|
152
|
+
_handleDocumentLoaded(event) {
|
|
153
|
+
const { pageCount, outline } = event.detail
|
|
154
|
+
this._pageCount = pageCount
|
|
155
|
+
this._outline = outline || []
|
|
156
|
+
|
|
157
|
+
this._renderOutline()
|
|
158
|
+
this._createThumbnails()
|
|
159
|
+
this._updatePageCounter(1)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Handle page changed event — update active thumbnail.
|
|
164
|
+
* @param {CustomEvent} event
|
|
165
|
+
*/
|
|
166
|
+
_handlePageChanged(event) {
|
|
167
|
+
const { page } = event.detail
|
|
168
|
+
this._activePage = page
|
|
169
|
+
this._updateActiveThumbnail(page)
|
|
170
|
+
this._updatePageCounter(page)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Handle highlight changes — update outline dots.
|
|
175
|
+
*/
|
|
176
|
+
_handleHighlightChanged() {
|
|
177
|
+
this._updateOutlineDots()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Outline rendering
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
_renderOutline() {
|
|
185
|
+
if (!this.hasOutlinePanelTarget) return
|
|
186
|
+
|
|
187
|
+
if (this._outline.length === 0) {
|
|
188
|
+
this.outlinePanelTarget.innerHTML =
|
|
189
|
+
'<p class="highlite-sidebar-empty">No outline available</p>'
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const list = this._buildOutlineList(this._outline)
|
|
194
|
+
this.outlinePanelTarget.innerHTML = ""
|
|
195
|
+
this.outlinePanelTarget.appendChild(list)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Recursively build the outline tree as nested lists.
|
|
200
|
+
* @param {Array} items
|
|
201
|
+
* @param {number} [depth=0]
|
|
202
|
+
* @returns {HTMLElement}
|
|
203
|
+
*/
|
|
204
|
+
_buildOutlineList(items, depth = 0) {
|
|
205
|
+
const ul = document.createElement("ul")
|
|
206
|
+
ul.className = "highlite-outline-list"
|
|
207
|
+
if (depth > 0) ul.style.paddingLeft = "1rem"
|
|
208
|
+
|
|
209
|
+
for (const item of items) {
|
|
210
|
+
const li = document.createElement("li")
|
|
211
|
+
li.className = "highlite-outline-item"
|
|
212
|
+
|
|
213
|
+
const link = document.createElement("button")
|
|
214
|
+
link.className = "highlite-outline-link"
|
|
215
|
+
link.type = "button"
|
|
216
|
+
|
|
217
|
+
// Title
|
|
218
|
+
const titleSpan = document.createElement("span")
|
|
219
|
+
titleSpan.className = "highlite-outline-title"
|
|
220
|
+
titleSpan.textContent = item.title
|
|
221
|
+
|
|
222
|
+
// Highlight dot (hidden by default)
|
|
223
|
+
const dot = document.createElement("span")
|
|
224
|
+
dot.className = "highlite-outline-dot"
|
|
225
|
+
dot.style.display = "none"
|
|
226
|
+
if (item.pageNum) dot.dataset.outlinePage = item.pageNum
|
|
227
|
+
|
|
228
|
+
// Page number
|
|
229
|
+
const pageSpan = document.createElement("span")
|
|
230
|
+
pageSpan.className = "highlite-outline-page"
|
|
231
|
+
pageSpan.textContent = item.pageNum || ""
|
|
232
|
+
|
|
233
|
+
link.appendChild(dot)
|
|
234
|
+
link.appendChild(titleSpan)
|
|
235
|
+
link.appendChild(pageSpan)
|
|
236
|
+
|
|
237
|
+
if (item.pageNum) {
|
|
238
|
+
link.addEventListener("click", () => {
|
|
239
|
+
this._navigateToPage(item.pageNum)
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
li.appendChild(link)
|
|
244
|
+
|
|
245
|
+
// Render children
|
|
246
|
+
if (item.items && item.items.length > 0) {
|
|
247
|
+
const details = document.createElement("details")
|
|
248
|
+
details.open = depth < 1 // Auto-expand first level
|
|
249
|
+
const summary = document.createElement("summary")
|
|
250
|
+
summary.appendChild(link)
|
|
251
|
+
details.appendChild(summary)
|
|
252
|
+
details.appendChild(this._buildOutlineList(item.items, depth + 1))
|
|
253
|
+
li.innerHTML = ""
|
|
254
|
+
li.appendChild(details)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
ul.appendChild(li)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return ul
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Update blue dots on outline items that have highlights on their page.
|
|
265
|
+
*/
|
|
266
|
+
_updateOutlineDots() {
|
|
267
|
+
const dots = this.element.querySelectorAll(
|
|
268
|
+
".highlite-outline-dot[data-outline-page]"
|
|
269
|
+
)
|
|
270
|
+
// Get highlight store from viewer element
|
|
271
|
+
const viewer = this._getViewerElement()
|
|
272
|
+
if (!viewer) return
|
|
273
|
+
|
|
274
|
+
for (const dot of dots) {
|
|
275
|
+
const page = parseInt(dot.dataset.outlinePage, 10)
|
|
276
|
+
const highlights = viewer.querySelectorAll(
|
|
277
|
+
`.highlite-page[data-page-number="${page}"] .highlite-highlight`
|
|
278
|
+
)
|
|
279
|
+
dot.style.display = highlights.length > 0 ? "inline-block" : "none"
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// Thumbnail rendering
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
_createThumbnails() {
|
|
288
|
+
if (!this.hasThumbnailsContainerTarget) return
|
|
289
|
+
|
|
290
|
+
const container = this.thumbnailsContainerTarget
|
|
291
|
+
container.innerHTML = ""
|
|
292
|
+
this._renderedThumbnails.clear()
|
|
293
|
+
|
|
294
|
+
for (let i = 1; i <= this._pageCount; i++) {
|
|
295
|
+
const wrapper = document.createElement("button")
|
|
296
|
+
wrapper.type = "button"
|
|
297
|
+
wrapper.className = "highlite-thumbnail"
|
|
298
|
+
wrapper.dataset.pageNumber = i
|
|
299
|
+
if (i === 1) wrapper.classList.add("highlite-thumbnail-active")
|
|
300
|
+
|
|
301
|
+
const canvas = document.createElement("canvas")
|
|
302
|
+
canvas.className = "highlite-thumbnail-canvas"
|
|
303
|
+
canvas.width = 1
|
|
304
|
+
canvas.height = 1
|
|
305
|
+
|
|
306
|
+
const label = document.createElement("span")
|
|
307
|
+
label.className = "highlite-thumbnail-label"
|
|
308
|
+
label.textContent = i
|
|
309
|
+
|
|
310
|
+
wrapper.appendChild(canvas)
|
|
311
|
+
wrapper.appendChild(label)
|
|
312
|
+
|
|
313
|
+
wrapper.addEventListener("click", () => {
|
|
314
|
+
this._navigateToPage(i)
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
container.appendChild(wrapper)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Lazy-load thumbnails with IntersectionObserver
|
|
321
|
+
this._setupThumbnailObserver()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
_setupThumbnailObserver() {
|
|
325
|
+
if (this._thumbnailObserver) {
|
|
326
|
+
this._thumbnailObserver.disconnect()
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
this._thumbnailObserver = new IntersectionObserver(
|
|
330
|
+
(entries) => {
|
|
331
|
+
for (const entry of entries) {
|
|
332
|
+
if (!entry.isIntersecting) continue
|
|
333
|
+
|
|
334
|
+
const pageNum = parseInt(entry.target.dataset.pageNumber, 10)
|
|
335
|
+
if (this._renderedThumbnails.has(pageNum)) continue
|
|
336
|
+
|
|
337
|
+
this._renderedThumbnails.add(pageNum)
|
|
338
|
+
this._renderThumbnail(pageNum, entry.target)
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
{ root: this.hasThumbnailsContainerTarget ? this.thumbnailsContainerTarget : null }
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
const thumbnails = this.thumbnailsContainerTarget.querySelectorAll(
|
|
345
|
+
".highlite-thumbnail"
|
|
346
|
+
)
|
|
347
|
+
for (const thumb of thumbnails) {
|
|
348
|
+
this._thumbnailObserver.observe(thumb)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Render a single thumbnail by finding the viewer's PdfRenderer.
|
|
354
|
+
* @param {number} pageNum
|
|
355
|
+
* @param {HTMLElement} wrapper
|
|
356
|
+
*/
|
|
357
|
+
async _renderThumbnail(pageNum, wrapper) {
|
|
358
|
+
const canvas = wrapper.querySelector("canvas")
|
|
359
|
+
if (!canvas) return
|
|
360
|
+
|
|
361
|
+
// Get the renderer from the viewer controller
|
|
362
|
+
const viewer = this._getViewerElement()
|
|
363
|
+
if (!viewer) return
|
|
364
|
+
|
|
365
|
+
// Access the Stimulus controller instance to use its renderer
|
|
366
|
+
const app = window.Stimulus || this.application
|
|
367
|
+
const viewerController = app?.getControllerForElementAndIdentifier?.(
|
|
368
|
+
viewer,
|
|
369
|
+
"highlite--viewer"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
if (viewerController?.renderer) {
|
|
373
|
+
try {
|
|
374
|
+
// Calculate scale to fill sidebar width
|
|
375
|
+
const containerWidth = this.thumbnailsContainerTarget.clientWidth - 8 // account for border/padding
|
|
376
|
+
const viewport = await viewerController.renderer.getViewport(pageNum, 1.0)
|
|
377
|
+
const scale = containerWidth / viewport.width
|
|
378
|
+
await viewerController.renderer.renderThumbnail(pageNum, canvas, scale)
|
|
379
|
+
// Clear inline dimensions set by renderPage so CSS width:100% takes effect
|
|
380
|
+
canvas.style.width = ""
|
|
381
|
+
canvas.style.height = ""
|
|
382
|
+
} catch {
|
|
383
|
+
// Thumbnail rendering may fail for some pages; degrade gracefully
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Update which thumbnail has the active border.
|
|
390
|
+
* @param {number} page
|
|
391
|
+
*/
|
|
392
|
+
_updateActiveThumbnail(page) {
|
|
393
|
+
if (!this.hasThumbnailsContainerTarget) return
|
|
394
|
+
|
|
395
|
+
const thumbnails = this.thumbnailsContainerTarget.querySelectorAll(
|
|
396
|
+
".highlite-thumbnail"
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
for (const thumb of thumbnails) {
|
|
400
|
+
const num = parseInt(thumb.dataset.pageNumber, 10)
|
|
401
|
+
thumb.classList.toggle("highlite-thumbnail-active", num === page)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Scroll active thumbnail into view
|
|
405
|
+
const active = this.thumbnailsContainerTarget.querySelector(
|
|
406
|
+
".highlite-thumbnail-active"
|
|
407
|
+
)
|
|
408
|
+
if (active) {
|
|
409
|
+
active.scrollIntoView({ block: "nearest", behavior: "smooth" })
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
// Navigation
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Navigate to a page by dispatching to the viewer controller.
|
|
419
|
+
* @param {number} pageNum
|
|
420
|
+
*/
|
|
421
|
+
_navigateToPage(pageNum) {
|
|
422
|
+
const viewer = this._getViewerElement()
|
|
423
|
+
if (!viewer) return
|
|
424
|
+
|
|
425
|
+
// Access the viewer controller to call scrollToPage
|
|
426
|
+
const app = window.Stimulus || this.application
|
|
427
|
+
const viewerController = app?.getControllerForElementAndIdentifier?.(
|
|
428
|
+
viewer,
|
|
429
|
+
"highlite--viewer"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
if (viewerController) {
|
|
433
|
+
viewerController.scrollToPage(pageNum)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
// UI updates
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
_updatePageCounter(page) {
|
|
442
|
+
if (this.hasPageCounterTarget) {
|
|
443
|
+
this.pageCounterTarget.textContent = `Page ${page} of ${this._pageCount}`
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
// Helpers
|
|
449
|
+
// ---------------------------------------------------------------------------
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Find the viewer element in the DOM.
|
|
453
|
+
* @returns {HTMLElement|null}
|
|
454
|
+
*/
|
|
455
|
+
_getViewerElement() {
|
|
456
|
+
return (
|
|
457
|
+
this.element.closest(
|
|
458
|
+
"[data-controller*='highlite--viewer']"
|
|
459
|
+
) ||
|
|
460
|
+
document.querySelector(
|
|
461
|
+
"[data-controller*='highlite--viewer']"
|
|
462
|
+
)
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
}
|