@inizioevoke/astro-core 1.7.0 → 2.0.1

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.
Files changed (30) hide show
  1. package/package.json +3 -5
  2. package/src/components/FlipCard/FlipCard.ts +15 -9
  3. package/src/components/ISI/{RightISI.astro → Right/RightISI.astro} +16 -9
  4. package/src/components/ISI/Right/RightISI.css +61 -0
  5. package/src/components/ISI/{RightISI.md → Right/RightISI.md} +0 -1
  6. package/src/components/ISI/Right/RightISI.ts +78 -0
  7. package/src/components/ISI/Right/index.ts +1 -0
  8. package/src/components/ISI/index.ts +5 -0
  9. package/src/components/Modal/Modal.astro +11 -5
  10. package/src/components/Modal/Modal.css +49 -39
  11. package/src/components/Modal/Modal.ts +186 -169
  12. package/src/components/PdfViewer/PdfViewer.astro +26 -12
  13. package/src/components/PdfViewer/PdfViewer.css +62 -53
  14. package/src/components/PdfViewer/PdfViewer.ts +446 -210
  15. package/src/components/ScrollContainer/ScrollContainer.astro +28 -24
  16. package/src/components/ScrollContainer/ScrollContainer.css +66 -21
  17. package/src/components/ScrollContainer/ScrollContainer.ts +170 -67
  18. package/src/components/index.ts +1 -0
  19. package/src/lib/index.ts +2 -0
  20. package/src/components/ISI/RightISI.css +0 -31
  21. package/src/components/ISI/RightISI.ts +0 -27
  22. package/src/components/Modal/v2/Modal.astro +0 -42
  23. package/src/components/Modal/v2/Modal.css +0 -155
  24. package/src/components/Modal/v2/Modal.ts +0 -219
  25. package/src/components/Modal/v2/ModalOverlay.astro +0 -14
  26. package/src/components/Modal/v2/ModalTrigger.astro +0 -29
  27. package/src/components/Modal/v2/index.ts +0 -1
  28. package/src/components/ScrollContainer/archive-20260417.ts +0 -150
  29. package/src/components/v2.ts +0 -3
  30. package/src/lib/v2.ts +0 -1
@@ -1,42 +1,461 @@
1
+ /// <reference types="vite/client" />
1
2
  import * as pdfjs from './pdf.min.mjs';
2
- import type { PDFDocumentProxy } from 'pdfjs-dist';
3
- import * as ScrollContainer from '../ScrollContainer/archive-20260417';
3
+ import workerSrc from './pdf.worker.min.mjs?url';
4
+ import type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist';
5
+ import { EvoScrollContainerElement, type Scrolling } from '../ScrollContainer/ScrollContainer';
4
6
 
5
- pdfjs.GlobalWorkerOptions.workerSrc = './pdf.worker.min.mjs';
7
+ const TAG_NAME = 'evo-pdf-viewer';
8
+ const MIN_ZOOM = 1; // 10% — stored as tenths
9
+ const MAX_ZOOM = 80; // 800%
10
+ const RERENDER_DEBOUNCE_MS = 300;
11
+ const MAX_CANVAS_DIMENSION = 16384;
6
12
 
7
-
8
- export interface PdfViewerCustomEventDetail {
13
+ export interface EvoPdfViewerEventDetail {
9
14
  type: string;
10
15
  cmd?: string;
11
16
  args?: Record<string, string>
12
17
  }
18
+ export type EvoPdfViewerEvent = CustomEvent<EvoPdfViewerEventDetail>;
13
19
  declare global {
14
20
  interface ElementEventMap {
15
- 'pdfviewer-click': CustomEvent<PdfViewerCustomEventDetail>
21
+ 'evopdfviewer-click': EvoPdfViewerEvent
16
22
  }
17
23
  }
18
24
 
19
- interface LinkAnnotation {
20
- rect: [number, number, number, number];
21
- url?: string;
22
- dest?: any;
23
- newWindow?: boolean;
24
- }
25
+ export type Layout = Omit<Scrolling, 'both'>
25
26
 
26
- interface PdfViewerSettings {
27
- workerDir?: string;
28
- workerSrc?: string;
27
+ export interface IEvoPdfViewerElement {
28
+ scrollToPage: (page: number | string) => void;
29
+ zoom: (z: -1 | 0 | 1) => void;
29
30
  }
30
- export function init(settings: PdfViewerSettings = {}) {
31
- if (!settings.workerDir) {
32
- settings.workerDir = './';
31
+ export class EvoPdfViewerElement extends HTMLElement implements IEvoPdfViewerElement {
32
+ #toolbar!: HTMLElement;
33
+ #pageSelect!: HTMLSelectElement;
34
+ #scrollContainer!: EvoScrollContainerElement;
35
+
36
+ #toolbarHeight = 0;
37
+ #defaultZoom = 100;
38
+ #pageCount = 0;
39
+ #layout: Layout = 'vertical';
40
+ #fit: 'width' | 'height' | `${string}%` | number = 'width';
41
+
42
+ #pdf!: PDFDocumentProxy;
43
+ #outputScale = 1;
44
+ #renderQuality = 2;
45
+ #targetQuality = 2;
46
+ #pageLayouts = new Map<number, { viewportScale: number; cssWidth: string; cssHeight: string }>();
47
+ #renderedPages = new Map<number, { effectiveQuality: number; task?: RenderTask }>();
48
+ #visiblePages = new Set<number>();
49
+ #lazyObserver!: IntersectionObserver;
50
+ #boundUpdateCurrentPage = () => this.#updateCurrentPage();
51
+ #rerenderTimer: number | null = null;
52
+
53
+ constructor() {
54
+ super();
55
+ pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
56
+ }
57
+
58
+ connectedCallback() {
59
+ this.#toolbar = this.querySelector('.evo-pdf-viewer-toolbar') as HTMLElement;
60
+ this.#pageSelect = this.#toolbar.querySelector('.evo-pdf-viewer-nav-page select') as HTMLSelectElement;
61
+ this.#scrollContainer = this.querySelector('evo-scroll-container') as EvoScrollContainerElement;
62
+
63
+ this.#toolbarHeight = parseFloat(getComputedStyle(this.#toolbar).height);
64
+ this.#defaultZoom = parseInt(this.getAttribute('data-zoom') ?? '100', 10);
65
+ this.#layout = this.dataset.layout as Layout;
66
+
67
+ const fit = this.dataset.fit;
68
+ if (fit) {
69
+ if (/^\d+%$/.test(fit)){
70
+ this.#fit = fit as `${string}%`;
71
+ } else if (/^\d+$/.test(fit)) {
72
+ this.#fit = parseInt(fit, 10);
73
+ } else if (fit === 'height' || fit === 'width') {
74
+ this.#fit = fit;
75
+ }
76
+ }
77
+
78
+ // *** nav
79
+ this.querySelector<HTMLButtonElement>('button.evo-pdf-viewer-toolbar-prev')?.addEventListener('click', () => {
80
+ const page = parseInt(this.#pageSelect.options[this.#pageSelect.selectedIndex].value, 10) - 1;
81
+ if (page > 0) {
82
+ this.scrollToPage(page);
83
+ }
84
+ }, { passive: true });
85
+
86
+ this.querySelector<HTMLButtonElement>('button.evo-pdf-viewer-toolbar-next')?.addEventListener('click', () => {
87
+ const page = parseInt(this.#pageSelect.options[this.#pageSelect.selectedIndex].value, 10) + 1;
88
+ if (page <= this.#pageCount) {
89
+ this.scrollToPage(page);
90
+ }
91
+ }, { passive: true });
92
+
93
+ this.#pageSelect.addEventListener('change', () => {
94
+ const page = this.#pageSelect.options[this.#pageSelect.selectedIndex].value;
95
+ this.scrollToPage(page);
96
+ }, { passive: true });
97
+
98
+ // *** zoom
99
+ this.querySelector<HTMLButtonElement>('button.evo-pdf-viewer-toolbar-zoom-out')?.addEventListener('click', () => {
100
+ this.zoom(-1);
101
+ }, { passive: true });
102
+
103
+ this.querySelector<HTMLButtonElement>('button.evo-pdf-viewer-toolbar-zoom-in')?.addEventListener('click', () => {
104
+ this.zoom(1);
105
+ }, { passive: true });
106
+
107
+ this.querySelector<HTMLButtonElement>('button.evo-pdf-viewer-toolbar-zoom-reset')?.addEventListener('click', () => {
108
+ this.zoom(0);
109
+ }, { passive: true });
110
+
111
+
112
+ // *** bookmarks
113
+ const bookmarksSelect = this.#toolbar.querySelector<HTMLSelectElement>('.evo-pdf-viewer-toolbar-bookmarks select');
114
+ if (bookmarksSelect) {
115
+ bookmarksSelect.addEventListener('change', () => {
116
+ this.scrollToPage(bookmarksSelect.options[bookmarksSelect.selectedIndex].value);
117
+ bookmarksSelect.selectedIndex = 0;
118
+ });
119
+ }
120
+
121
+ this.#scrollContainer.addEventListener('scroll', this.#boundUpdateCurrentPage, { passive: true });
122
+
123
+ this.#renderPDF()
124
+ .then(() => {
125
+ this.zoom(0);
126
+ this.#scrollContainer.refresh();
127
+ this.#updateCurrentPage();
128
+ })
129
+ .catch((err) => {
130
+ console.error('A PDF rendering error occurred', err);
131
+ });
132
+
133
+ }
134
+
135
+ scrollToPage(page: number | string) {
136
+ if (typeof page == 'string') {
137
+ page = parseInt(page, 10);
138
+ }
139
+ const wrapper = this.#scrollContainer.querySelector<HTMLElement>(`.evo-pdf-viewer-page-wrapper[data-page="${page}"]`);
140
+ if (wrapper) {
141
+ if (this.#layout === 'horizontal') {
142
+ this.#scrollContainer.scroll({
143
+ left: wrapper.getBoundingClientRect().left + this.#scrollContainer.scrollLeft - this.#scrollContainer.getBoundingClientRect().left,
144
+ behavior: 'smooth'
145
+ });
146
+ } else {
147
+ this.#scrollContainer.scroll({
148
+ top: wrapper.getBoundingClientRect().top + this.#scrollContainer.scrollTop - this.#toolbarHeight,
149
+ behavior: 'smooth'
150
+ });
151
+ }
152
+ this.#pageSelect.selectedIndex = page - 1;
153
+ }
154
+ }
155
+
156
+ zoom(z: -1 | 0 | 1) {
157
+ const style = getComputedStyle(this);
158
+ const current = Math.floor(parseFloat(style.getPropertyValue('--evo-pdf-viewer-scale')) * 10);
159
+ const zoomIn = z === 1;
160
+ if (z !== 0) {
161
+
162
+ if (current >= 12) {
163
+ zoomIn ? z += 1 : z -= 1;
164
+ if (current >= 20) {
165
+ zoomIn ? z += 1 : z -= 1;
166
+ if (current >= 40) {
167
+ zoomIn ? z += 1 : z -= 1;
168
+ }
169
+ }
170
+ }
171
+ }
172
+ const target = z === 0 ? this.#defaultZoom / 10 : current + z;
173
+ const scale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, target));
174
+ if (scale === current) return;
175
+
176
+ const scroll = {
177
+ left: this.#scrollContainer.scrollLeft,
178
+ top: this.#scrollContainer.scrollTop
179
+ };
180
+ this.style.setProperty('--evo-pdf-viewer-scale', (scale / 10).toString());
181
+ this.#scrollContainer.scroll({
182
+ left: scroll.left * scale / current,
183
+ top: scroll.top * scale / current,
184
+ behavior: 'instant'
185
+ });
186
+ this.#scrollContainer.refresh();
187
+
188
+ // Update the target quality immediately so any page entering view via the
189
+ // lazy observer renders at the new scale; only the visible pages are
190
+ // re-rendered eagerly (debounced) — others get sharpened lazily on scroll.
191
+ this.#targetQuality = Math.max(this.#renderQuality, this.#renderQuality * (scale / 10));
192
+ this.#scheduleVisibleRerender();
193
+ }
194
+
195
+ #scheduleVisibleRerender() {
196
+ if (this.#rerenderTimer !== null) {
197
+ clearTimeout(this.#rerenderTimer);
198
+ }
199
+ this.#rerenderTimer = window.setTimeout(() => {
200
+ this.#rerenderTimer = null;
201
+ for (const pageNum of this.#visiblePages) {
202
+ const wrapper = this.#scrollContainer.content.querySelector<HTMLElement>(
203
+ `.evo-pdf-viewer-page-wrapper[data-page="${pageNum}"]`
204
+ );
205
+ if (wrapper) this.#considerRender(pageNum, wrapper);
206
+ }
207
+ }, RERENDER_DEBOUNCE_MS);
33
208
  }
34
- if (!settings.workerSrc) {
35
- settings.workerSrc = `${settings.workerDir}${settings.workerDir.endsWith('/') ? '' : '/'}pdf.worker.min.mjs`
209
+
210
+ // Decide whether to (re-)render a page at the current target quality.
211
+ // Used by both the lazy observer and the post-zoom sweep so the gating
212
+ // logic lives in one place.
213
+ #considerRender(pageNum: number, wrapper: HTMLElement) {
214
+ const info = this.#renderedPages.get(pageNum);
215
+ if (!info) {
216
+ this.#renderPage(pageNum, wrapper, this.#targetQuality);
217
+ return;
218
+ }
219
+ // Skip when an in-flight render at the same target is already running.
220
+ if (info.task && info.effectiveQuality >= this.#targetQuality * 0.95) return;
221
+
222
+ const ratio = info.effectiveQuality / this.#targetQuality;
223
+ // Zoom-in: re-render when canvas is noticeably below target sharpness.
224
+ // Zoom-out: re-render only on big drops (>25%) to reclaim memory without
225
+ // constant churn on tiny steps.
226
+ if (ratio >= 0.95 && ratio <= 1.25) return;
227
+
228
+ info.task?.cancel();
229
+ this.#renderPage(pageNum, wrapper, this.#targetQuality);
230
+ }
231
+
232
+ async #renderPDF() {
233
+ this.#outputScale = window.devicePixelRatio || 1;
234
+ this.#renderQuality = parseFloat(this.getAttribute('data-quality') ?? '2');
235
+ this.#targetQuality = this.#renderQuality;
236
+ this.#pdf = await pdfjs.getDocument(this.getAttribute('data-pdf') as string).promise;
237
+
238
+ const pages = new Array(this.#pdf.numPages).fill(1).map((_, i) => i + 1);
239
+ this.#pageCount = pages.length;
240
+
241
+ this.querySelector<HTMLElement>('.evo-pdf-viewer-nav-page-count')!.innerText = pages.length.toString();
242
+
243
+ this.#pageSelect.options.remove(0);
244
+ for (const pageNum of pages) {
245
+ const option = document.createElement('option');
246
+ option.value = pageNum.toString();
247
+ option.innerText = `${pageNum}`;
248
+ this.#pageSelect.options.add(option);
249
+ }
250
+
251
+ const containerStyle = getComputedStyle(this.#scrollContainer.content);
252
+ const trackWidth = parseInt(containerStyle.getPropertyValue('--evo-scroll-container-track-width'));
253
+ const containerWidth = parseFloat(containerStyle.width);
254
+ const containerHeight = parseFloat(containerStyle.height);
255
+ const gap = 16;
256
+
257
+ // Pass 1: create sized placeholder wrappers for all pages so the scroll
258
+ // container knows its total dimensions before any canvas is rendered.
259
+ for (const pageNum of pages) {
260
+ const pdfPage = await this.#pdf.getPage(pageNum);
261
+ const baseVp = pdfPage.getViewport({ scale: 1 });
262
+
263
+ let viewportScale: number;
264
+ let cssProp: 'width' | 'height';
265
+ let cssRenderedSize: number;
266
+
267
+ if (this.#layout === 'horizontal') {
268
+ if (this.#fit === 'height') {
269
+ viewportScale = (containerHeight - trackWidth * 2) / baseVp.height;
270
+ cssRenderedSize = Math.floor(baseVp.height * viewportScale) - (trackWidth * 2);
271
+ cssProp = 'height';
272
+ } else if (this.#fit === 'width') {
273
+ viewportScale = containerWidth / baseVp.width;
274
+ cssRenderedSize = Math.floor(baseVp.width * viewportScale);
275
+ cssProp = 'width';
276
+ } else if (/^\d+%$/.test(this.#fit.toString())) { // replacing 'actual' with a percentage
277
+ viewportScale = parseInt(this.#fit.toString(), 10) / 100;
278
+ cssRenderedSize = Math.floor(baseVp.height * viewportScale) - (trackWidth * 2);
279
+ cssProp = 'height';
280
+ } else {
281
+ const n = this.#fit as number;
282
+ viewportScale = (containerWidth - (n - 1) * gap) / (n * baseVp.width);
283
+ cssRenderedSize = Math.floor(baseVp.width * viewportScale);
284
+ cssProp = 'width';
285
+ }
286
+ } else {
287
+ if (this.#fit === 'width') {
288
+ viewportScale = containerWidth / baseVp.width;
289
+ cssRenderedSize = Math.floor(baseVp.width * viewportScale) - (trackWidth * 2);
290
+ cssProp = 'width';
291
+ } else if (this.#fit === 'height') {
292
+ viewportScale = containerHeight / baseVp.height;
293
+ cssRenderedSize = Math.floor(baseVp.height * viewportScale);
294
+ cssProp = 'height';
295
+ } else if (/^\d+%$/.test(this.#fit.toString())) { // replacing 'actual' with a percentage
296
+ viewportScale = parseInt(this.#fit.toString(), 10) / 100;
297
+ cssRenderedSize = Math.floor(baseVp.width * viewportScale) - (trackWidth * 2);
298
+ cssProp = 'width';
299
+ } else {
300
+ const n = this.#fit as number;
301
+ viewportScale = (containerHeight - (n - 1) * gap) / (n * baseVp.height);
302
+ cssRenderedSize = Math.floor(baseVp.height * viewportScale);
303
+ cssProp = 'height';
304
+ }
305
+ }
306
+
307
+ const layoutVp = pdfPage.getViewport({ scale: viewportScale });
308
+ const cssOtherSize = Math.round(cssProp === 'width'
309
+ ? cssRenderedSize * layoutVp.height / layoutVp.width
310
+ : cssRenderedSize * layoutVp.width / layoutVp.height);
311
+ const cssWidth = `calc(${cssProp === 'width' ? cssRenderedSize : cssOtherSize}px * var(--evo-pdf-viewer-scale))`;
312
+ const cssHeight = `calc(${cssProp === 'height' ? cssRenderedSize : cssOtherSize}px * var(--evo-pdf-viewer-scale))`;
313
+
314
+ this.#pageLayouts.set(pageNum, { viewportScale, cssWidth, cssHeight });
315
+
316
+ const pageWrapper = document.createElement('div');
317
+ pageWrapper.className = 'evo-pdf-viewer-page-wrapper';
318
+ pageWrapper.setAttribute('data-page', pageNum.toString());
319
+ pageWrapper.style.position = 'relative';
320
+ pageWrapper.style.width = cssWidth;
321
+ pageWrapper.style.height = cssHeight;
322
+ this.#scrollContainer.content.append(pageWrapper);
323
+ this.#scrollContainer.refresh();
324
+ }
325
+
326
+ // Pass 2: observe wrappers. The observer is the single gate for both
327
+ // initial render and post-zoom re-render — pages outside its rootMargin
328
+ // keep their stale-quality canvas until they scroll back into view.
329
+ this.#lazyObserver = new IntersectionObserver(
330
+ (entries) => {
331
+ for (const entry of entries) {
332
+ const wrapper = entry.target as HTMLElement;
333
+ const pageNum = parseInt(wrapper.getAttribute('data-page')!, 10);
334
+ if (entry.isIntersecting) {
335
+ this.#visiblePages.add(pageNum);
336
+ this.#considerRender(pageNum, wrapper);
337
+ } else {
338
+ this.#visiblePages.delete(pageNum);
339
+ }
340
+ }
341
+ },
342
+ {
343
+ root: this.#scrollContainer.content,
344
+ // Render one full container-length ahead in the scroll direction.
345
+ rootMargin: this.#layout === 'horizontal'
346
+ ? `0px ${containerWidth}px 0px ${containerWidth}px`
347
+ : `${containerHeight}px 0px ${containerHeight}px 0px`,
348
+ threshold: 0
349
+ }
350
+ );
351
+
352
+ for (const wrapper of this.#scrollContainer.content.querySelectorAll<HTMLElement>('.evo-pdf-viewer-page-wrapper')) {
353
+ this.#lazyObserver.observe(wrapper);
354
+ }
355
+ }
356
+
357
+ async #renderPage(pageNum: number, wrapper: HTMLElement, quality = this.#renderQuality) {
358
+ const layout = this.#pageLayouts.get(pageNum)!;
359
+ const pdfPage = await this.#pdf.getPage(pageNum);
360
+
361
+ // Cap effective quality so the canvas never exceeds the browser's max texture
362
+ // dimension on any axis.
363
+ const baseVp = pdfPage.getViewport({ scale: layout.viewportScale });
364
+ const maxByWidth = MAX_CANVAS_DIMENSION / (baseVp.width * this.#outputScale);
365
+ const maxByHeight = MAX_CANVAS_DIMENSION / (baseVp.height * this.#outputScale);
366
+ const effectiveQuality = Math.min(quality, maxByWidth, maxByHeight);
367
+
368
+ const viewport = pdfPage.getViewport({ scale: layout.viewportScale * effectiveQuality });
369
+ const layoutVp = baseVp;
370
+
371
+ const canvas = document.createElement('canvas');
372
+ canvas.setAttribute('data-page', pageNum.toString());
373
+ canvas.width = Math.floor(viewport.width * this.#outputScale);
374
+ canvas.height = Math.floor(viewport.height * this.#outputScale);
375
+ canvas.style.width = layout.cssWidth;
376
+ canvas.style.height = layout.cssHeight;
377
+
378
+ const task = pdfPage.render({
379
+ canvas,
380
+ transform: this.#outputScale !== 1 ? [this.#outputScale, 0, 0, this.#outputScale, 0, 0] : undefined,
381
+ viewport
382
+ });
383
+ this.#renderedPages.set(pageNum, { effectiveQuality, task });
384
+
385
+ try {
386
+ await task.promise;
387
+ } catch (err: any) {
388
+ if (err?.name === 'RenderingCancelledException') return;
389
+ throw err;
390
+ }
391
+
392
+ // Swap the new canvas in only after a successful render so we never flash
393
+ // an empty canvas during a re-render.
394
+ const oldCanvas = wrapper.querySelector('canvas');
395
+ const oldOverlay = wrapper.querySelector('.evo-pdf-viewer-link-overlay');
396
+ if (oldCanvas) oldCanvas.remove();
397
+ if (oldOverlay) oldOverlay.remove();
398
+ wrapper.appendChild(canvas);
399
+ this.#renderedPages.set(pageNum, { effectiveQuality });
400
+
401
+ const links = await getPageLinks(pdfPage);
402
+ if (links.length > 0) {
403
+ const linkOverlay = createLinkOverlay(canvas, links, layoutVp, pageNum, this, this.scrollToPage.bind(this), this.#pdf);
404
+ wrapper.appendChild(linkOverlay);
405
+ }
406
+ }
407
+
408
+ #updateCurrentPage() {
409
+ const wrappers = this.#scrollContainer.content.querySelectorAll<HTMLElement>('.evo-pdf-viewer-page-wrapper');
410
+ let visibleWrapper: HTMLElement | undefined;
411
+ let maxVisibility = 0;
412
+
413
+ wrappers.forEach((wrapper) => {
414
+ const rect = wrapper.getBoundingClientRect();
415
+ const containerRect = this.#scrollContainer.getBoundingClientRect();
416
+
417
+ let intersection: number;
418
+ let dimension: number;
419
+ if (this.#layout === 'horizontal') {
420
+ const intersectLeft = Math.max(rect.left, containerRect.left);
421
+ const intersectRight = Math.min(rect.right, containerRect.right);
422
+ intersection = Math.max(0, intersectRight - intersectLeft);
423
+ dimension = rect.width || 1;
424
+ } else {
425
+ const intersectTop = Math.max(rect.top, containerRect.top);
426
+ const intersectBottom = Math.min(rect.bottom, containerRect.bottom);
427
+ intersection = Math.max(0, intersectBottom - intersectTop);
428
+ dimension = rect.height || 1;
429
+ }
430
+
431
+ const visibility = intersection / dimension;
432
+ if (visibility > maxVisibility) {
433
+ maxVisibility = visibility;
434
+ visibleWrapper = wrapper;
435
+ }
436
+ });
437
+
438
+ if (visibleWrapper && maxVisibility >= 0.1) {
439
+ const pageNum = visibleWrapper.getAttribute('data-page');
440
+ if (pageNum) {
441
+ this.#pageSelect.selectedIndex = parseInt(pageNum, 10) - 1;
442
+ }
443
+ }
36
444
  }
37
- pdfjs.GlobalWorkerOptions.workerSrc = settings.workerSrc;
38
445
  }
39
446
 
447
+ if (!customElements.get(TAG_NAME)) {
448
+ customElements.define(TAG_NAME, EvoPdfViewerElement);
449
+ }
450
+
451
+
452
+
453
+ interface LinkAnnotation {
454
+ rect: [number, number, number, number];
455
+ url?: string;
456
+ dest?: any;
457
+ newWindow?: boolean;
458
+ }
40
459
  /**
41
460
  * Extract link annotations from a PDF page
42
461
  */
@@ -104,24 +523,16 @@ function createLinkOverlay(
104
523
  gotoPage: (page: number | string) => void,
105
524
  pdf: PDFDocumentProxy
106
525
  ): SVGSVGElement {
107
- const trackWidth = parseInt(
108
- getComputedStyle(canvas.parentElement!).getPropertyValue('--evo-scroll-container-track-width')
109
- );
110
-
111
- const canvasWidth = Math.floor(viewport.width) - trackWidth * 2;
112
- const canvasHeight = Math.floor(viewport.height);
113
-
114
- // Create SVG overlay
115
526
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
116
527
  svg.setAttribute('class', 'evo-pdf-viewer-link-overlay');
117
528
  svg.setAttribute('data-page', pageNum.toString());
118
529
  svg.setAttribute('viewBox', `0 0 ${viewport.width} ${viewport.height}`);
119
- svg.setAttribute('width', `calc(${canvasWidth}px * var(--evo-pdf-viewer-scale))`);
120
- svg.setAttribute('height', `calc(${canvasHeight}px * var(--evo-pdf-viewer-scale))`);
530
+ svg.setAttribute('width', canvas.style.width);
531
+ svg.setAttribute('height', canvas.style.height);
121
532
  svg.style.position = 'absolute';
122
533
  svg.style.top = '0';
123
534
  svg.style.left = '0';
124
- svg.style.cursor = 'pointer';
535
+ // svg.style.cursor = 'pointer';
125
536
 
126
537
  // Add link rectangles
127
538
  for (const link of links) {
@@ -143,7 +554,7 @@ function createLinkOverlay(
143
554
  e.stopPropagation();
144
555
 
145
556
  if (link.url) {
146
- if (link.url.startsWith('http://pdfviewer')) {
557
+ if (link.url.startsWith('http://pdfviewer.evo')) {
147
558
  const url = new URL(link.url);
148
559
  const [ type, cmd ] = url.pathname.split('/').filter(p => p.trim() !== '');
149
560
  const args = url.search === '' ? undefined :
@@ -153,7 +564,7 @@ function createLinkOverlay(
153
564
  })
154
565
  );
155
566
 
156
- pdfViewer.dispatchEvent(new CustomEvent('pdfviewer-click', {
567
+ pdfViewer.dispatchEvent(new CustomEvent('evopdfviewer-click', {
157
568
  detail: { type: type ?? '', cmd, args }
158
569
  }));
159
570
  return;
@@ -215,179 +626,4 @@ function createLinkOverlay(
215
626
  }
216
627
 
217
628
  return svg;
218
- }
219
-
220
- export async function createViewer(selector: string) {
221
- const pdfViewer = document.querySelector<HTMLElement>(selector);
222
- if (pdfViewer) {
223
- // const ddlPage = pdfViewer.querySelector('select') as HTMLSelectElement;
224
- const toolbar = pdfViewer.querySelector('.evo-pdf-viewer-toolbar') as HTMLElement;
225
- const toolbarHeight = parseFloat(getComputedStyle(toolbar).height);
226
- const pageSelect = toolbar.querySelector('.evo-pdf-viewer-nav-page select') as HTMLSelectElement;
227
- const scrollContainer = ScrollContainer.getScrollContainer(pdfViewer.querySelector('.evo-scroll-container') as HTMLElement) as ScrollContainer.IScrollContainer;
228
- const defaultZoom = parseInt(pdfViewer.getAttribute('data-zoom') ?? '100', 10);
229
- const zoom = (z: number) => {
230
- const style = getComputedStyle(pdfViewer);
231
- const current = Math.floor(parseFloat(style.getPropertyValue('--evo-pdf-viewer-scale')) * 10);
232
- const scale = z === 0 ? defaultZoom / 10 : current + z;
233
- if (scale >= 1) {
234
- const scroll = ScrollContainer.getScroll(scrollContainer);
235
- pdfViewer.style.setProperty('--evo-pdf-viewer-scale', (scale/10).toString());
236
- ScrollContainer.scroll(scrollContainer, { left: scroll.left * scale / current, top: scroll.top * scale / current });
237
- ScrollContainer.refresh(scrollContainer);
238
- }
239
- };
240
- zoom(0);
241
-
242
- const gotoPage = (page: number | string) => {
243
- if (typeof page == 'string') {
244
- page = parseInt(page, 10);
245
- }
246
- requestAnimationFrame(() => {
247
- const container = pdfViewer.querySelector('.evo-scroll-content') as HTMLElement;
248
- const canvas = container.querySelector<HTMLElement>(`canvas[data-page="${page}"]`);
249
- if (canvas) {
250
- container.scroll({ top: canvas.getBoundingClientRect().top + container.scrollTop - toolbarHeight }); // , behavior: 'smooth' });
251
- pageSelect.selectedIndex = page - 1;
252
- }
253
- })
254
- };
255
-
256
- pageSelect.addEventListener('change', (e) => {
257
- const page = pageSelect.options[pageSelect.selectedIndex].value;
258
- gotoPage(page);
259
- }, { passive: true });
260
-
261
- pdfViewer.querySelector<HTMLButtonElement>('button.evo-pdf-viewer-toolbar-zoom-out')?.addEventListener('click', () => {
262
- zoom(-1);
263
- }, { passive: true });
264
-
265
- pdfViewer.querySelector<HTMLButtonElement>('button.evo-pdf-viewer-toolbar-zoom-in')?.addEventListener('click', () => {
266
- zoom(1);
267
- }, { passive: true });
268
-
269
- pdfViewer.querySelector<HTMLButtonElement>('button.evo-pdf-viewer-toolbar-zoom-reset')?.addEventListener('click', () => {
270
- zoom(0);
271
- }, { passive: true });
272
-
273
- const pdf: PDFDocumentProxy = await pdfjs.getDocument(pdfViewer.getAttribute('data-pdf') as string).promise;
274
-
275
- const bookmarksSelect = toolbar.querySelector<HTMLSelectElement>('.evo-pdf-viewer-toolbar-bookmarks select');
276
- if (bookmarksSelect) {
277
- bookmarksSelect.addEventListener('change', () => {
278
- gotoPage(bookmarksSelect.options[bookmarksSelect.selectedIndex].value);
279
- bookmarksSelect.selectedIndex = 0;
280
- });
281
- }
282
-
283
- const outputScale = window.devicePixelRatio || 1;
284
- const pages = new Array(pdf.numPages).fill(1).map((v, i) => { return i + 1; });
285
-
286
-
287
- pdfViewer.querySelector<HTMLButtonElement>('button.evo-pdf-viewer-toolbar-prev')?.addEventListener('click', () => {
288
- const page = parseInt(pageSelect.options[pageSelect.selectedIndex].value, 10) - 1;
289
- if (page > 0) {
290
- gotoPage(page);
291
- }
292
- }, { passive: true });
293
-
294
- pdfViewer.querySelector<HTMLButtonElement>('button.evo-pdf-viewer-toolbar-next')?.addEventListener('click', () => {
295
- const page = parseInt(pageSelect.options[pageSelect.selectedIndex].value, 10) + 1;
296
- if (page <= pages.length) {
297
- gotoPage(page);
298
- }
299
- }, { passive: true });
300
-
301
- const pdfContainer = pdfViewer.querySelector('.evo-scroll-content') as HTMLElement;
302
- pdfViewer.querySelector<HTMLElement>('.evo-pdf-viewer-nav-page-count')!.innerText = pages.length.toString();
303
- pageSelect.options.remove(0);
304
- for (const pageNum of pages) {
305
- const option = document.createElement('option');
306
- option.value = pageNum.toString();
307
- option.innerText = `${pageNum}`;
308
- pageSelect.options.add(option);
309
- }
310
- for (const pageNum of pages) {
311
-
312
- const pdfPage = await pdf.getPage(pageNum);
313
- const pdfPageWidth = pdfPage.getViewport({ scale: 1}).width;
314
- const containerStyle = getComputedStyle(pdfContainer);
315
- const trackWidth = parseInt(containerStyle.getPropertyValue('--evo-scroll-container-track-width'));
316
- const viewport = pdfPage.getViewport({ scale: parseFloat(containerStyle.width) / pdfPageWidth });
317
-
318
- const canvas = document.createElement('canvas');
319
- canvas.setAttribute('data-page', pageNum.toString());
320
- const width = Math.floor(viewport.width * outputScale);
321
- const height = Math.floor(viewport.height * outputScale);
322
- canvas.width = width;
323
- canvas.height = height;
324
- canvas.style.aspectRatio = `${width}/${height}`;
325
- canvas.style.width = `calc(${Math.floor(viewport.width) - (trackWidth * 2)}px * var(--evo-pdf-viewer-scale)`;
326
- // canvas.style.height = `${Math.floor(viewport.height)}px`;
327
-
328
- // Create wrapper for canvas and overlay
329
- const pageWrapper = document.createElement('div');
330
- pageWrapper.className = 'evo-pdf-viewer-page-wrapper';
331
- pageWrapper.style.position = 'relative';
332
- pageWrapper.style.width = canvas.style.width;
333
- pageWrapper.style.aspectRatio = canvas.style.aspectRatio;
334
- pageWrapper.appendChild(canvas);
335
-
336
- pdfContainer.append(pageWrapper);
337
-
338
- pdfPage.render({
339
- canvas,
340
- // canvasContext: canvas.getContext('2d') as CanvasRenderingContext2D,
341
- transform: outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : undefined,
342
- viewport
343
- });
344
-
345
- // Extract and render links
346
- const links = await getPageLinks(pdfPage);
347
- if (links.length > 0) {
348
- const linkOverlay = createLinkOverlay(canvas, links, viewport, pageNum, pdfViewer, gotoPage, pdf);
349
- pageWrapper.appendChild(linkOverlay);
350
- }
351
-
352
- ScrollContainer.refresh(scrollContainer);
353
- }
354
-
355
- // Function to check and update current page
356
- const updateCurrentPage = () => {
357
- const canvases = pdfContainer.querySelectorAll<HTMLCanvasElement>('canvas');
358
- let visiblePage: HTMLCanvasElement | undefined;
359
- let maxVisibility = 0;
360
-
361
- canvases.forEach((canvas) => {
362
- const rect = canvas.getBoundingClientRect();
363
- const containerRect = scrollContainer.container.getBoundingClientRect();
364
-
365
- // Calculate intersection height
366
- const intersectTop = Math.max(rect.top, containerRect.top);
367
- const intersectBottom = Math.min(rect.bottom, containerRect.bottom);
368
- const intersectionHeight = Math.max(0, intersectBottom - intersectTop);
369
-
370
- // Calculate visibility ratio
371
- const visibility = intersectionHeight / (rect.height || 1);
372
-
373
- if (visibility > maxVisibility) {
374
- maxVisibility = visibility;
375
- visiblePage = canvas;
376
- }
377
- });
378
-
379
- if (visiblePage && maxVisibility >= 0.1) {
380
- const pageNum = visiblePage.getAttribute('data-page');
381
- if (pageNum) {
382
- pageSelect.selectedIndex = parseInt(pageNum, 10) - 1;
383
- }
384
- }
385
- };
386
-
387
- // Update on scroll
388
- scrollContainer.content.addEventListener('scroll', updateCurrentPage, { passive: true });
389
-
390
- // Initial update
391
- updateCurrentPage();
392
- }
393
- }
629
+ }