@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.
- package/package.json +3 -5
- package/src/components/FlipCard/FlipCard.ts +15 -9
- package/src/components/ISI/{RightISI.astro → Right/RightISI.astro} +16 -9
- package/src/components/ISI/Right/RightISI.css +61 -0
- package/src/components/ISI/{RightISI.md → Right/RightISI.md} +0 -1
- package/src/components/ISI/Right/RightISI.ts +78 -0
- package/src/components/ISI/Right/index.ts +1 -0
- package/src/components/ISI/index.ts +5 -0
- package/src/components/Modal/Modal.astro +11 -5
- package/src/components/Modal/Modal.css +49 -39
- package/src/components/Modal/Modal.ts +186 -169
- package/src/components/PdfViewer/PdfViewer.astro +26 -12
- package/src/components/PdfViewer/PdfViewer.css +62 -53
- package/src/components/PdfViewer/PdfViewer.ts +446 -210
- package/src/components/ScrollContainer/ScrollContainer.astro +28 -24
- package/src/components/ScrollContainer/ScrollContainer.css +66 -21
- package/src/components/ScrollContainer/ScrollContainer.ts +170 -67
- package/src/components/index.ts +1 -0
- package/src/lib/index.ts +2 -0
- package/src/components/ISI/RightISI.css +0 -31
- package/src/components/ISI/RightISI.ts +0 -27
- package/src/components/Modal/v2/Modal.astro +0 -42
- package/src/components/Modal/v2/Modal.css +0 -155
- package/src/components/Modal/v2/Modal.ts +0 -219
- package/src/components/Modal/v2/ModalOverlay.astro +0 -14
- package/src/components/Modal/v2/ModalTrigger.astro +0 -29
- package/src/components/Modal/v2/index.ts +0 -1
- package/src/components/ScrollContainer/archive-20260417.ts +0 -150
- package/src/components/v2.ts +0 -3
- 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
|
|
3
|
-
import
|
|
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
|
-
|
|
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
|
-
'
|
|
21
|
+
'evopdfviewer-click': EvoPdfViewerEvent
|
|
16
22
|
}
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
export interface IEvoPdfViewerElement {
|
|
28
|
+
scrollToPage: (page: number | string) => void;
|
|
29
|
+
zoom: (z: -1 | 0 | 1) => void;
|
|
29
30
|
}
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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',
|
|
120
|
-
svg.setAttribute('height',
|
|
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('
|
|
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
|
+
}
|