@inizioevoke/astro-core 1.5.1 → 1.6.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.
@@ -133,7 +133,7 @@
133
133
  .evo-scroll-container {
134
134
  --evo-scroll-container-track-color: #cccccc;
135
135
  .evo-scroll-content {
136
- canvas {
136
+ .evo-pdf-viewer-page-wrapper {
137
137
  display: block;
138
138
  margin: 18px auto;
139
139
  background-color: white;
@@ -145,6 +145,21 @@
145
145
  margin-bottom: 0;
146
146
  }
147
147
  }
148
+
149
+ canvas {
150
+ display: block;
151
+ width: 100%;
152
+ height: 100%;
153
+ }
154
+
155
+ .evo-pdf-viewer-link-overlay {
156
+ pointer-events: auto;
157
+ z-index: 1;
158
+
159
+ rect {
160
+ cursor: pointer;
161
+ }
162
+ }
148
163
  }
149
164
  }
150
165
  }
@@ -4,6 +4,25 @@ import * as ScrollContainer from '../ScrollContainer/archive-20260417';
4
4
 
5
5
  pdfjs.GlobalWorkerOptions.workerSrc = './pdf.worker.min.mjs';
6
6
 
7
+
8
+ export interface PdfViewerCustomEventDetail {
9
+ type: string;
10
+ cmd?: string;
11
+ args?: Record<string, string>
12
+ }
13
+ declare global {
14
+ interface ElementEventMap {
15
+ 'pdfviewer-click': CustomEvent<PdfViewerCustomEventDetail>
16
+ }
17
+ }
18
+
19
+ interface LinkAnnotation {
20
+ rect: [number, number, number, number];
21
+ url?: string;
22
+ dest?: any;
23
+ newWindow?: boolean;
24
+ }
25
+
7
26
  interface PdfViewerSettings {
8
27
  workerDir?: string;
9
28
  workerSrc?: string;
@@ -18,6 +37,186 @@ export function init(settings: PdfViewerSettings = {}) {
18
37
  pdfjs.GlobalWorkerOptions.workerSrc = settings.workerSrc;
19
38
  }
20
39
 
40
+ /**
41
+ * Extract link annotations from a PDF page
42
+ */
43
+ async function getPageLinks(pdfPage: any): Promise<LinkAnnotation[]> {
44
+ const annotations = await pdfPage.getAnnotations();
45
+ const links: LinkAnnotation[] = [];
46
+
47
+ for (const annotation of annotations) {
48
+ // Only process link annotations (type 2 is subtype for URI links and internal links)
49
+ if (annotation.subtype === 'Link') {
50
+ const link: LinkAnnotation = {
51
+ rect: annotation.rect
52
+ };
53
+
54
+ if (annotation.url) {
55
+ link.url = annotation.url;
56
+ link.newWindow = annotation.newWindow;
57
+ } else if (annotation.dest) {
58
+ link.dest = annotation.dest;
59
+ }
60
+
61
+ links.push(link);
62
+ }
63
+ }
64
+
65
+ return links;
66
+ }
67
+
68
+ /**
69
+ * Convert PDF coordinates to viewport coordinates
70
+ * PDF coords: [left, bottom, right, top] with origin at bottom-left
71
+ * Return: {x, y, width, height} with origin at top-left
72
+ */
73
+ function convertPdfRectToViewport(
74
+ pdfRect: [number, number, number, number],
75
+ viewport: any
76
+ ): { x: number; y: number; width: number; height: number } {
77
+ const [pdfLeft, pdfBottom, pdfRight, pdfTop] = pdfRect;
78
+ const pageHeight = viewport.height / viewport.scale;
79
+
80
+ // Scale coordinates
81
+ const x = pdfLeft * viewport.scale;
82
+ const y = (pageHeight - pdfTop) * viewport.scale;
83
+ const width = (pdfRight - pdfLeft) * viewport.scale;
84
+ const height = (pdfTop - pdfBottom) * viewport.scale;
85
+
86
+ return { x, y, width, height };
87
+ }
88
+
89
+ /*
90
+ * Custom PDF link events
91
+ * To create a custom event, create a link in the PDF that starts with `http://pdfviewer`
92
+ * Arguments can be passed via the path and query string
93
+ * http://pdfviewer/type/cmd/?args=1
94
+ */
95
+ /**
96
+ * Create an SVG overlay with link areas above a canvas element
97
+ */
98
+ function createLinkOverlay(
99
+ canvas: HTMLCanvasElement,
100
+ links: LinkAnnotation[],
101
+ viewport: any,
102
+ pageNum: number,
103
+ pdfViewer: HTMLElement,
104
+ gotoPage: (page: number | string) => void,
105
+ pdf: PDFDocumentProxy
106
+ ): 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
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
116
+ svg.setAttribute('class', 'evo-pdf-viewer-link-overlay');
117
+ svg.setAttribute('data-page', pageNum.toString());
118
+ 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))`);
121
+ svg.style.position = 'absolute';
122
+ svg.style.top = '0';
123
+ svg.style.left = '0';
124
+ svg.style.cursor = 'pointer';
125
+
126
+ // Add link rectangles
127
+ for (const link of links) {
128
+ const rect = convertPdfRectToViewport(link.rect, viewport);
129
+
130
+ // Create invisible clickable area
131
+ const linkRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
132
+ linkRect.setAttribute('x', rect.x.toString());
133
+ linkRect.setAttribute('y', rect.y.toString());
134
+ linkRect.setAttribute('width', rect.width.toString());
135
+ linkRect.setAttribute('height', rect.height.toString());
136
+ linkRect.setAttribute('fill', 'transparent');
137
+ linkRect.setAttribute('stroke', 'none');
138
+ linkRect.style.cursor = 'pointer';
139
+
140
+ // Handle click on link
141
+ linkRect.addEventListener('click', (e) => {
142
+ e.preventDefault();
143
+ e.stopPropagation();
144
+
145
+ if (link.url) {
146
+ if (link.url.startsWith('http://pdfviewer')) {
147
+ const url = new URL(link.url);
148
+ const [ type, cmd ] = url.pathname.split('/').filter(p => p.trim() !== '');
149
+ const args = url.search === '' ? undefined :
150
+ Object.fromEntries(url.search.slice(1).split('&')
151
+ .map((pair) => {
152
+ return pair.split('=').slice(0, 2);
153
+ })
154
+ );
155
+
156
+ pdfViewer.dispatchEvent(new CustomEvent('pdfviewer-click', {
157
+ detail: { type: type ?? '', cmd, args }
158
+ }));
159
+ return;
160
+ }
161
+ // Check for custom protocol
162
+ // if (link.url.includes('://')) {
163
+ // const protocol = link.url.split('://')[0];
164
+ // if (!['http', 'https', 'ftp', 'ftps', 'mailto'].includes(protocol)) {
165
+ // // Dispatch custom event for non-standard protocols
166
+ // const event = new CustomEvent('pdf-link-click', {
167
+ // detail: {
168
+ // type: 'uri',
169
+ // uri: link.url,
170
+ // pageNum,
171
+ // newWindow: link.newWindow
172
+ // }
173
+ // });
174
+ // pdfViewer.dispatchEvent(event);
175
+ // return;
176
+ // }
177
+ // }
178
+
179
+ // Handle standard URLs
180
+ const target = link.newWindow ? '_blank' : '_self';
181
+ window.open(link.url, target);
182
+ } else if (link.dest) {
183
+ // Handle internal navigation
184
+ // dest is typically an array [pageRef, ...] where pageRef is {num, gen} or a page index
185
+ if (Array.isArray(link.dest)) {
186
+ const destRef = link.dest[0];
187
+
188
+ // Check if it's a reference object with num and gen properties
189
+ if (destRef && typeof destRef === 'object' && 'num' in destRef && 'gen' in destRef) {
190
+ // Resolve the reference to a page index using the PDF document
191
+ pdf.getPageIndex(destRef).then((pageIndex: number) => {
192
+ gotoPage(pageIndex + 1); // PDF pages are 0-indexed, but gotoPage expects 1-indexed
193
+ }).catch((err: Error) => {
194
+ console.error('Failed to resolve PDF link destination:', err);
195
+ });
196
+ } else if (typeof destRef === 'number') {
197
+ // Direct page index
198
+ gotoPage(destRef + 1);
199
+ }
200
+ }
201
+
202
+ // Dispatch event for custom dest handling
203
+ const event = new CustomEvent('pdf-link-click', {
204
+ detail: {
205
+ type: 'goto',
206
+ dest: link.dest,
207
+ pageNum
208
+ }
209
+ });
210
+ pdfViewer.dispatchEvent(event);
211
+ }
212
+ }, { passive: false });
213
+
214
+ svg.appendChild(linkRect);
215
+ }
216
+
217
+ return svg;
218
+ }
219
+
21
220
  export async function createViewer(selector: string) {
22
221
  const pdfViewer = document.querySelector<HTMLElement>(selector);
23
222
  if (pdfViewer) {
@@ -123,7 +322,16 @@ export async function createViewer(selector: string) {
123
322
  canvas.style.aspectRatio = `${width}/${height}`;
124
323
  canvas.style.width = `calc(${Math.floor(viewport.width) - (trackWidth * 2)}px * var(--evo-pdf-viewer-scale)`;
125
324
  // canvas.style.height = `${Math.floor(viewport.height)}px`;
126
- pdfContainer.append(canvas);
325
+
326
+ // Create wrapper for canvas and overlay
327
+ const pageWrapper = document.createElement('div');
328
+ pageWrapper.className = 'evo-pdf-viewer-page-wrapper';
329
+ pageWrapper.style.position = 'relative';
330
+ pageWrapper.style.width = canvas.style.width;
331
+ pageWrapper.style.aspectRatio = canvas.style.aspectRatio;
332
+ pageWrapper.appendChild(canvas);
333
+
334
+ pdfContainer.append(pageWrapper);
127
335
 
128
336
  pdfPage.render({
129
337
  canvas,
@@ -131,6 +339,14 @@ export async function createViewer(selector: string) {
131
339
  transform: outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : undefined,
132
340
  viewport
133
341
  });
342
+
343
+ // Extract and render links
344
+ const links = await getPageLinks(pdfPage);
345
+ if (links.length > 0) {
346
+ const linkOverlay = createLinkOverlay(canvas, links, viewport, pageNum, pdfViewer, gotoPage, pdf);
347
+ pageWrapper.appendChild(linkOverlay);
348
+ }
349
+
134
350
  ScrollContainer.refresh(scrollContainer);
135
351
  }
136
352