@ngstarter-ui/components 21.0.31 → 21.0.33

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.
@@ -0,0 +1,1143 @@
1
+ import { DOCUMENT, isPlatformBrowser } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { Injectable, inject, DestroyRef, ElementRef, PLATFORM_ID, viewChild, input, numberAttribute, booleanAttribute, output, signal, computed, effect, untracked, ChangeDetectionStrategy, Component } from '@angular/core';
4
+ import { BlockLoader } from '@ngstarter-ui/components/block-loader';
5
+ import { Button } from '@ngstarter-ui/components/button';
6
+ import { Icon } from '@ngstarter-ui/components/icon';
7
+ import { ImagePlaceholder } from '@ngstarter-ui/components/image-placeholder';
8
+ import { Menu, MenuDivider, MenuHeading, MenuItem, MenuTrigger } from '@ngstarter-ui/components/menu';
9
+ import { Panel, PanelContent, PanelHeader, PanelSidebar } from '@ngstarter-ui/components/panel';
10
+ import { PluginRegistry } from '@embedpdf/core';
11
+
12
+ class PdfViewerEngineService {
13
+ engines = new Map();
14
+ getEngine(wasmUrl) {
15
+ const cachedEngine = this.engines.get(wasmUrl);
16
+ if (cachedEngine) {
17
+ return cachedEngine;
18
+ }
19
+ const engine = this.createEngine(wasmUrl);
20
+ this.engines.set(wasmUrl, engine);
21
+ return engine;
22
+ }
23
+ createRegistry(engine, defaultScale) {
24
+ const registry = new PluginRegistry(engine, { defaultScale });
25
+ return registry.initialize().then(() => registry);
26
+ }
27
+ async createEngine(wasmUrl) {
28
+ const { createPdfiumEngine } = await import('@embedpdf/engines/pdfium-direct-engine');
29
+ return createPdfiumEngine(wasmUrl, { encoderPoolSize: 0 });
30
+ }
31
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PdfViewerEngineService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
32
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PdfViewerEngineService, providedIn: 'root' });
33
+ }
34
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PdfViewerEngineService, decorators: [{
35
+ type: Injectable,
36
+ args: [{
37
+ providedIn: 'root'
38
+ }]
39
+ }] });
40
+
41
+ class PdfViewer {
42
+ textSelectionHorizontalPadding = 2;
43
+ textSelectionLineHeight = 1.2;
44
+ document = inject(DOCUMENT);
45
+ destroyRef = inject(DestroyRef);
46
+ engineService = inject(PdfViewerEngineService);
47
+ hostElement = inject(ElementRef);
48
+ platformId = inject(PLATFORM_ID);
49
+ isBrowser = isPlatformBrowser(this.platformId);
50
+ viewerBody = viewChild('viewerBody', ...(ngDevMode ? [{ debugName: "viewerBody" }] : /* istanbul ignore next */ []));
51
+ pageList = viewChild('pageList', ...(ngDevMode ? [{ debugName: "pageList" }] : /* istanbul ignore next */ []));
52
+ src = input(null, ...(ngDevMode ? [{ debugName: "src" }] : /* istanbul ignore next */ []));
53
+ wasmUrl = input('/assets/embedpdf/pdfium.wasm', ...(ngDevMode ? [{ debugName: "wasmUrl" }] : /* istanbul ignore next */ []));
54
+ page = input(1, { ...(ngDevMode ? { debugName: "page" } : /* istanbul ignore next */ {}), transform: numberAttribute });
55
+ scale = input(1, { ...(ngDevMode ? { debugName: "scale" } : /* istanbul ignore next */ {}), transform: numberAttribute });
56
+ minScale = input(0.2, { ...(ngDevMode ? { debugName: "minScale" } : /* istanbul ignore next */ {}), transform: numberAttribute });
57
+ maxScale = input(60, { ...(ngDevMode ? { debugName: "maxScale" } : /* istanbul ignore next */ {}), transform: numberAttribute });
58
+ zoomStep = input(0.1, { ...(ngDevMode ? { debugName: "zoomStep" } : /* istanbul ignore next */ {}), transform: numberAttribute });
59
+ renderAll = input(true, { ...(ngDevMode ? { debugName: "renderAll" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
60
+ showToolbar = input(true, { ...(ngDevMode ? { debugName: "showToolbar" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
61
+ showPageList = input(true, { ...(ngDevMode ? { debugName: "showPageList" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
62
+ withAnnotations = input(true, { ...(ngDevMode ? { debugName: "withAnnotations" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
63
+ withForms = input(true, { ...(ngDevMode ? { debugName: "withForms" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
64
+ loaded = output();
65
+ pageChanged = output();
66
+ pageRendered = output();
67
+ error = output();
68
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
69
+ errorState = signal(null, ...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
70
+ renderedPages = signal([], ...(ngDevMode ? [{ debugName: "renderedPages" }] : /* istanbul ignore next */ []));
71
+ thumbnailPages = signal([], ...(ngDevMode ? [{ debugName: "thumbnailPages" }] : /* istanbul ignore next */ []));
72
+ pageCount = signal(0, ...(ngDevMode ? [{ debugName: "pageCount" }] : /* istanbul ignore next */ []));
73
+ activePage = signal(1, ...(ngDevMode ? [{ debugName: "activePage" }] : /* istanbul ignore next */ []));
74
+ zoom = signal(1, ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
75
+ pageListVisible = signal(true, ...(ngDevMode ? [{ debugName: "pageListVisible" }] : /* istanbul ignore next */ []));
76
+ spreadMode = signal('single', ...(ngDevMode ? [{ debugName: "spreadMode" }] : /* istanbul ignore next */ []));
77
+ scrollLayout = signal('horizontal', ...(ngDevMode ? [{ debugName: "scrollLayout" }] : /* istanbul ignore next */ []));
78
+ selectionRects = signal([], ...(ngDevMode ? [{ debugName: "selectionRects" }] : /* istanbul ignore next */ []));
79
+ hasDocument = computed(() => this.pageCount() > 0, ...(ngDevMode ? [{ debugName: "hasDocument" }] : /* istanbul ignore next */ []));
80
+ isPageListVisible = computed(() => this.showPageList() && this.hasDocument() && this.pageListVisible(), ...(ngDevMode ? [{ debugName: "isPageListVisible" }] : /* istanbul ignore next */ []));
81
+ thumbnailPageMap = computed(() => new Map(this.thumbnailPages().map((thumbnail) => [thumbnail.pageNumber, thumbnail])), ...(ngDevMode ? [{ debugName: "thumbnailPageMap" }] : /* istanbul ignore next */ []));
82
+ pageItems = computed(() => Array.from({ length: this.pageCount() }, (_, index) => {
83
+ const pageNumber = index + 1;
84
+ return {
85
+ pageNumber,
86
+ thumbnail: this.thumbnailPageMap().get(pageNumber) ?? null,
87
+ };
88
+ }), ...(ngDevMode ? [{ debugName: "pageItems" }] : /* istanbul ignore next */ []));
89
+ canGoPrevious = computed(() => this.activePage() > 1, ...(ngDevMode ? [{ debugName: "canGoPrevious" }] : /* istanbul ignore next */ []));
90
+ canGoNext = computed(() => this.activePage() < this.pageCount(), ...(ngDevMode ? [{ debugName: "canGoNext" }] : /* istanbul ignore next */ []));
91
+ canZoomOut = computed(() => this.zoom() > this.getScaleBounds().min, ...(ngDevMode ? [{ debugName: "canZoomOut" }] : /* istanbul ignore next */ []));
92
+ canZoomIn = computed(() => this.zoom() < this.getScaleBounds().max, ...(ngDevMode ? [{ debugName: "canZoomIn" }] : /* istanbul ignore next */ []));
93
+ zoomLabel = computed(() => `${Math.round(this.zoom() * 100)}%`, ...(ngDevMode ? [{ debugName: "zoomLabel" }] : /* istanbul ignore next */ []));
94
+ engine = null;
95
+ pdfDocument = null;
96
+ registry = null;
97
+ pageIntersectionObserver = null;
98
+ visiblePageRatios = new Map();
99
+ loadToken = 0;
100
+ renderToken = 0;
101
+ scrollSyncFrame = null;
102
+ programmaticScrollTargetPage = null;
103
+ programmaticScrollTimeout = null;
104
+ pageObserverFrame = null;
105
+ pageObserverTimeout = null;
106
+ visiblePageRenderFrame = null;
107
+ pageObserverRefreshAttempts = 0;
108
+ selectionStart = null;
109
+ isViewInitialized = false;
110
+ programmaticScrollMinDuration = 900;
111
+ programmaticScrollMaxDuration = 6000;
112
+ constructor() {
113
+ this.destroyRef.onDestroy(() => {
114
+ this.loadToken++;
115
+ this.renderToken++;
116
+ this.cancelScrollSyncFrame();
117
+ this.clearProgrammaticScrollLock();
118
+ this.cancelPageObserverFrame();
119
+ this.cancelVisiblePageRenderFrame();
120
+ this.disconnectPageObserver();
121
+ this.revokeRenderedPages();
122
+ this.revokeThumbnailPages();
123
+ void this.closeDocument();
124
+ void this.registry?.destroy();
125
+ });
126
+ effect(() => {
127
+ const source = this.src();
128
+ const wasmUrl = this.wasmUrl();
129
+ if (!this.isViewInitialized || !this.isBrowser) {
130
+ return;
131
+ }
132
+ untracked(() => {
133
+ void this.loadDocument(source, wasmUrl);
134
+ });
135
+ });
136
+ effect(() => {
137
+ const scale = this.sanitizeScale(this.scale());
138
+ if (untracked(() => this.zoom()) !== scale) {
139
+ untracked(() => this.setZoom(scale));
140
+ }
141
+ });
142
+ effect(() => {
143
+ const requestedPage = this.sanitizePage(this.page());
144
+ if (requestedPage !== untracked(() => this.activePage())) {
145
+ this.activePage.set(requestedPage);
146
+ }
147
+ });
148
+ effect(() => {
149
+ const zoom = this.zoom();
150
+ const renderAll = this.renderAll();
151
+ const activePage = renderAll ? untracked(() => this.activePage()) : this.activePage();
152
+ const withAnnotations = this.withAnnotations();
153
+ const withForms = this.withForms();
154
+ if (!this.pdfDocument || !this.engine || this.isLoading()) {
155
+ return;
156
+ }
157
+ untracked(() => {
158
+ this.applyInstantZoom(zoom);
159
+ this.scheduleVisiblePagesRender({ zoom, activePage, renderAll, withAnnotations, withForms });
160
+ });
161
+ });
162
+ effect(() => {
163
+ const withAnnotations = this.withAnnotations();
164
+ const isLoading = this.isLoading();
165
+ const pageCount = this.pageCount();
166
+ if (isLoading || pageCount === 0 || !this.pdfDocument || !this.engine) {
167
+ return;
168
+ }
169
+ untracked(() => {
170
+ void this.renderThumbnails(this.loadToken, { withAnnotations });
171
+ });
172
+ });
173
+ }
174
+ ngAfterViewInit() {
175
+ this.isViewInitialized = true;
176
+ if (this.isBrowser) {
177
+ void this.loadDocument(this.src(), this.wasmUrl());
178
+ }
179
+ }
180
+ previousPage() {
181
+ if (!this.canGoPrevious()) {
182
+ return;
183
+ }
184
+ this.setPage(this.activePage() - 1);
185
+ }
186
+ nextPage() {
187
+ if (!this.canGoNext()) {
188
+ return;
189
+ }
190
+ this.setPage(this.activePage() + 1);
191
+ }
192
+ zoomIn() {
193
+ this.setZoom(this.roundZoom(this.zoom() + this.sanitizeZoomStep()));
194
+ }
195
+ zoomOut() {
196
+ this.setZoom(this.roundZoom(this.zoom() - this.sanitizeZoomStep()));
197
+ }
198
+ togglePageList() {
199
+ this.pageListVisible.update((isVisible) => !isVisible);
200
+ }
201
+ setSpreadMode(mode) {
202
+ this.spreadMode.set(mode);
203
+ }
204
+ setScrollLayout(layout) {
205
+ this.scrollLayout.set(layout);
206
+ }
207
+ rotateClockwise() {
208
+ this.renderedPages.update((pages) => [...pages]);
209
+ }
210
+ rotateCounterClockwise() {
211
+ this.renderedPages.update((pages) => [...pages]);
212
+ }
213
+ toggleFullscreen() {
214
+ if (!this.isBrowser) {
215
+ return;
216
+ }
217
+ const target = this.hostElement.nativeElement;
218
+ const currentFullscreenElement = this.document.fullscreenElement;
219
+ if (currentFullscreenElement) {
220
+ void this.document.exitFullscreen?.();
221
+ return;
222
+ }
223
+ void target.requestFullscreen?.();
224
+ }
225
+ setPage(pageNumber) {
226
+ const nextPage = this.clamp(pageNumber, 1, Math.max(this.pageCount(), 1));
227
+ const hasChanged = nextPage !== this.activePage();
228
+ if (!hasChanged) {
229
+ this.scrollToPage(nextPage);
230
+ this.scrollPageListToPage(nextPage);
231
+ return;
232
+ }
233
+ this.activePage.set(nextPage);
234
+ this.pageChanged.emit(nextPage);
235
+ this.scrollToPage(nextPage);
236
+ this.scrollPageListToPage(nextPage);
237
+ }
238
+ onViewerScroll() {
239
+ if (!this.renderAll() || !this.isBrowser || this.renderedPages().length < 2) {
240
+ return;
241
+ }
242
+ if (this.scrollSyncFrame !== null) {
243
+ return;
244
+ }
245
+ this.scrollSyncFrame = this.document.defaultView?.requestAnimationFrame(() => {
246
+ this.scrollSyncFrame = null;
247
+ if (this.isProgrammaticScrollActive()) {
248
+ this.completeProgrammaticScrollIfSettled();
249
+ return;
250
+ }
251
+ this.syncActivePageFromViewport();
252
+ this.scheduleCurrentVisiblePagesRender();
253
+ }) ?? null;
254
+ }
255
+ selectionRectsForPage(pageNumber) {
256
+ return this.selectionRects().filter((rect) => rect.pageNumber === pageNumber);
257
+ }
258
+ isPageImageFresh(page) {
259
+ return !!page.url && Math.abs((page.renderedScale ?? 0) - page.scale) < 0.0001;
260
+ }
261
+ startTextSelection(event, page) {
262
+ if (event.button !== 0 || page.textGlyphs.length === 0) {
263
+ return;
264
+ }
265
+ event.preventDefault();
266
+ event.stopPropagation();
267
+ const surface = event.currentTarget;
268
+ if ('pointerId' in event) {
269
+ surface.setPointerCapture?.(event.pointerId);
270
+ }
271
+ this.selectionStart = this.getSelectionPoint(event, page, surface);
272
+ this.selectionRects.set([]);
273
+ }
274
+ updateTextSelection(event, page) {
275
+ if (!this.selectionStart || this.selectionStart.pageNumber !== page.pageNumber) {
276
+ return;
277
+ }
278
+ event.preventDefault();
279
+ const surface = event.currentTarget;
280
+ const currentPoint = this.getSelectionPoint(event, page, surface);
281
+ if (this.selectionStart.offset === currentPoint.offset) {
282
+ this.selectionRects.set([]);
283
+ return;
284
+ }
285
+ this.selectionRects.set(this.getSelectionRects(page, this.selectionStart.offset, currentPoint.offset));
286
+ }
287
+ finishTextSelection(event, page) {
288
+ if (!this.selectionStart) {
289
+ return;
290
+ }
291
+ this.updateTextSelection(event, page);
292
+ this.selectionStart = null;
293
+ const surface = event.currentTarget;
294
+ if ('pointerId' in event) {
295
+ surface.releasePointerCapture?.(event.pointerId);
296
+ }
297
+ }
298
+ cancelTextSelection() {
299
+ this.selectionStart = null;
300
+ }
301
+ async loadDocument(source, wasmUrl) {
302
+ const token = ++this.loadToken;
303
+ this.renderToken++;
304
+ this.isLoading.set(true);
305
+ this.errorState.set(null);
306
+ this.pageCount.set(0);
307
+ this.selectionStart = null;
308
+ this.selectionRects.set([]);
309
+ this.revokeRenderedPages();
310
+ this.revokeThumbnailPages();
311
+ await this.closeDocument();
312
+ await this.registry?.destroy();
313
+ this.registry = null;
314
+ if (!source) {
315
+ this.isLoading.set(false);
316
+ return;
317
+ }
318
+ try {
319
+ const engine = await this.engineService.getEngine(wasmUrl);
320
+ if (token !== this.loadToken) {
321
+ return;
322
+ }
323
+ this.engine = engine;
324
+ this.registry = await this.engineService.createRegistry(engine, this.zoom());
325
+ if (token !== this.loadToken) {
326
+ return;
327
+ }
328
+ const documentId = this.createDocumentId();
329
+ const pdfDocument = await this.openSource(engine, documentId, source);
330
+ if (token !== this.loadToken) {
331
+ await engine.closeDocument(pdfDocument).toPromise();
332
+ return;
333
+ }
334
+ this.pdfDocument = pdfDocument;
335
+ this.pageCount.set(pdfDocument.pageCount);
336
+ this.activePage.set(this.clamp(this.activePage(), 1, Math.max(pdfDocument.pageCount, 1)));
337
+ this.loaded.emit({ pageCount: pdfDocument.pageCount });
338
+ this.initializePageShells(pdfDocument, this.zoom(), this.renderAll(), this.activePage());
339
+ this.schedulePageObserverRefresh();
340
+ await this.renderVisiblePages(token, ++this.renderToken, {
341
+ zoom: this.zoom(),
342
+ activePage: this.activePage(),
343
+ renderAll: this.renderAll(),
344
+ withAnnotations: this.withAnnotations(),
345
+ withForms: this.withForms(),
346
+ });
347
+ }
348
+ catch (error) {
349
+ if (token === this.loadToken) {
350
+ this.errorState.set(error);
351
+ this.error.emit(error);
352
+ }
353
+ }
354
+ finally {
355
+ if (token === this.loadToken) {
356
+ this.isLoading.set(false);
357
+ }
358
+ }
359
+ }
360
+ initializePageShells(pdfDocument, scale, renderAll, activePage) {
361
+ const previousPages = this.renderedPages();
362
+ const previousPageMap = new Map(previousPages.map((page) => [page.pageNumber, page]));
363
+ const pageNumbers = this.getPageNumbersToDisplay(pdfDocument, renderAll, activePage);
364
+ const nextScale = this.sanitizeScale(scale);
365
+ const nextPages = pageNumbers.map((pageNumber) => {
366
+ const pdfPage = pdfDocument.pages[pageNumber - 1];
367
+ const previousPage = previousPageMap.get(pageNumber);
368
+ const displaySize = this.getPageDisplaySize(pdfPage, nextScale);
369
+ const isFresh = previousPage?.url && Math.abs((previousPage.renderedScale ?? 0) - nextScale) < 0.0001;
370
+ return {
371
+ pageNumber,
372
+ url: isFresh ? previousPage.url : null,
373
+ scale: nextScale,
374
+ renderedScale: isFresh ? previousPage.renderedScale : null,
375
+ width: displaySize.width,
376
+ height: displaySize.height,
377
+ isRendering: false,
378
+ textGlyphs: isFresh ? previousPage.textGlyphs : [],
379
+ };
380
+ });
381
+ const nextUrlSet = new Set(nextPages.map((page) => page.url).filter((url) => !!url));
382
+ for (const page of previousPages) {
383
+ if (page.url && !nextUrlSet.has(page.url)) {
384
+ this.revokeObjectUrl(page.url);
385
+ }
386
+ }
387
+ this.selectionStart = null;
388
+ this.selectionRects.set([]);
389
+ this.disconnectPageObserver();
390
+ this.renderedPages.set(nextPages);
391
+ }
392
+ getPageNumbersToDisplay(pdfDocument, renderAll, activePage) {
393
+ if (!renderAll) {
394
+ return [this.clamp(activePage, 1, pdfDocument.pageCount)];
395
+ }
396
+ return Array.from({ length: pdfDocument.pageCount }, (_, index) => index + 1);
397
+ }
398
+ scheduleVisiblePagesRender(options) {
399
+ const targetWindow = this.document.defaultView;
400
+ if (!targetWindow) {
401
+ return;
402
+ }
403
+ this.cancelVisiblePageRenderFrame();
404
+ const token = this.loadToken;
405
+ const renderToken = ++this.renderToken;
406
+ this.visiblePageRenderFrame = targetWindow.requestAnimationFrame(() => {
407
+ this.visiblePageRenderFrame = null;
408
+ void this.renderVisiblePages(token, renderToken, options);
409
+ });
410
+ }
411
+ scheduleCurrentVisiblePagesRender() {
412
+ this.scheduleVisiblePagesRender({
413
+ zoom: this.zoom(),
414
+ activePage: this.activePage(),
415
+ renderAll: this.renderAll(),
416
+ withAnnotations: this.withAnnotations(),
417
+ withForms: this.withForms(),
418
+ });
419
+ }
420
+ async renderVisiblePages(token, renderToken, options) {
421
+ if (!this.engine || !this.pdfDocument) {
422
+ return;
423
+ }
424
+ const pdfDocument = this.pdfDocument;
425
+ const renderScale = this.sanitizeScale(options.zoom);
426
+ const pageNumbers = this.getPriorityPageNumbers(options.activePage, options.renderAll);
427
+ for (const pageNumber of pageNumbers) {
428
+ if (!this.isRenderCurrent(token, renderToken)) {
429
+ return;
430
+ }
431
+ const currentPage = this.renderedPages().find((page) => page.pageNumber === pageNumber);
432
+ if (!currentPage || Math.abs(currentPage.scale - renderScale) > 0.0001) {
433
+ continue;
434
+ }
435
+ if (currentPage.url && Math.abs((currentPage.renderedScale ?? 0) - renderScale) < 0.0001) {
436
+ continue;
437
+ }
438
+ this.patchRenderedPage(pageNumber, { isRendering: true });
439
+ const page = pdfDocument.pages[pageNumber - 1];
440
+ const blob = await this.engine.renderPage(pdfDocument, page, {
441
+ scaleFactor: renderScale,
442
+ dpr: this.getDevicePixelRatio(),
443
+ withAnnotations: options.withAnnotations,
444
+ withForms: options.withForms,
445
+ }).toPromise();
446
+ if (!this.isRenderCurrent(token, renderToken)) {
447
+ return;
448
+ }
449
+ const displaySize = this.getPageDisplaySize(page, renderScale);
450
+ const textGlyphs = await this.getPageTextGlyphs(pdfDocument, page, renderScale);
451
+ if (!this.isRenderCurrent(token, renderToken)) {
452
+ return;
453
+ }
454
+ const url = this.document.defaultView?.URL.createObjectURL(blob) ?? URL.createObjectURL(blob);
455
+ const previousUrl = this.renderedPages().find((renderedPage) => renderedPage.pageNumber === pageNumber)?.url;
456
+ this.patchRenderedPage(pageNumber, {
457
+ url,
458
+ scale: renderScale,
459
+ renderedScale: renderScale,
460
+ width: displaySize.width,
461
+ height: displaySize.height,
462
+ isRendering: false,
463
+ textGlyphs,
464
+ });
465
+ if (previousUrl && previousUrl !== url) {
466
+ this.revokeObjectUrl(previousUrl);
467
+ }
468
+ this.pageRendered.emit({ pageNumber, url, width: displaySize.width, height: displaySize.height });
469
+ }
470
+ }
471
+ setZoom(scale) {
472
+ const nextZoom = this.sanitizeScale(scale);
473
+ if (this.zoom() !== nextZoom) {
474
+ this.zoom.set(nextZoom);
475
+ }
476
+ this.applyInstantZoom(nextZoom);
477
+ }
478
+ applyInstantZoom(scale) {
479
+ const nextScale = this.sanitizeScale(scale);
480
+ const pdfDocument = this.pdfDocument;
481
+ const pages = this.renderedPages();
482
+ if (!pdfDocument || pages.length === 0) {
483
+ return;
484
+ }
485
+ let hasChanges = false;
486
+ const nextPages = pages.map((page) => {
487
+ if (Math.abs(page.scale - nextScale) < 0.0001) {
488
+ return page;
489
+ }
490
+ hasChanges = true;
491
+ const pdfPage = pdfDocument.pages[page.pageNumber - 1];
492
+ const displaySize = this.getPageDisplaySize(pdfPage, nextScale);
493
+ return {
494
+ ...page,
495
+ url: null,
496
+ scale: nextScale,
497
+ renderedScale: null,
498
+ width: displaySize.width,
499
+ height: displaySize.height,
500
+ isRendering: false,
501
+ textGlyphs: [],
502
+ };
503
+ });
504
+ if (!hasChanges) {
505
+ return;
506
+ }
507
+ for (const page of pages) {
508
+ if (page.url) {
509
+ this.revokeObjectUrl(page.url);
510
+ }
511
+ }
512
+ this.selectionStart = null;
513
+ this.selectionRects.set([]);
514
+ this.renderedPages.set(nextPages);
515
+ this.schedulePageObserverRefresh();
516
+ }
517
+ getPriorityPageNumbers(activePage, renderAll) {
518
+ if (!this.pdfDocument) {
519
+ return [];
520
+ }
521
+ const pageCount = this.pdfDocument.pageCount;
522
+ const visiblePageNumbers = renderAll
523
+ ? this.getVisiblePageNumbers()
524
+ : [this.clamp(activePage, 1, pageCount)];
525
+ const priorityPageNumbers = new Set();
526
+ for (const pageNumber of visiblePageNumbers.length > 0 ? visiblePageNumbers : [activePage]) {
527
+ for (let candidate = pageNumber - 1; candidate <= pageNumber + 1; candidate++) {
528
+ if (candidate >= 1 && candidate <= pageCount) {
529
+ priorityPageNumbers.add(candidate);
530
+ }
531
+ }
532
+ }
533
+ priorityPageNumbers.add(this.clamp(activePage, 1, pageCount));
534
+ return [...priorityPageNumbers].sort((a, b) => {
535
+ const activeDistance = Math.abs(a - activePage) - Math.abs(b - activePage);
536
+ return activeDistance || a - b;
537
+ });
538
+ }
539
+ getVisiblePageNumbers() {
540
+ const container = this.viewerBody()?.nativeElement;
541
+ if (!container) {
542
+ return [];
543
+ }
544
+ const containerRect = container.getBoundingClientRect();
545
+ const prefetchMargin = containerRect.height;
546
+ const pages = Array.from(container.querySelectorAll('[data-ngs-pdf-page]'));
547
+ const visiblePageNumbers = [];
548
+ for (const page of pages) {
549
+ const rect = page.getBoundingClientRect();
550
+ const pageNumber = Number(page.dataset['ngsPdfPage']);
551
+ if (Number.isFinite(pageNumber) &&
552
+ rect.bottom >= containerRect.top - prefetchMargin &&
553
+ rect.top <= containerRect.bottom + prefetchMargin) {
554
+ visiblePageNumbers.push(pageNumber);
555
+ }
556
+ }
557
+ return visiblePageNumbers;
558
+ }
559
+ patchRenderedPage(pageNumber, patch) {
560
+ this.renderedPages.update((pages) => pages.map((page) => page.pageNumber === pageNumber ? { ...page, ...patch } : page));
561
+ }
562
+ async renderThumbnails(token, options) {
563
+ if (!this.engine || !this.pdfDocument) {
564
+ return;
565
+ }
566
+ const pdfDocument = this.pdfDocument;
567
+ const thumbnailScale = 0.18;
568
+ const nextThumbnails = [];
569
+ const previousThumbnails = this.thumbnailPages();
570
+ for (const page of pdfDocument.pages) {
571
+ const blob = await this.engine.renderThumbnail(pdfDocument, page, {
572
+ scaleFactor: thumbnailScale,
573
+ dpr: this.getDevicePixelRatio(),
574
+ withAnnotations: options.withAnnotations,
575
+ }).toPromise();
576
+ if (token !== this.loadToken) {
577
+ const staleUrl = this.document.defaultView?.URL.createObjectURL(blob) ?? URL.createObjectURL(blob);
578
+ this.revokeObjectUrl(staleUrl);
579
+ return;
580
+ }
581
+ const url = this.document.defaultView?.URL.createObjectURL(blob) ?? URL.createObjectURL(blob);
582
+ const pageNumber = page.index + 1;
583
+ const displaySize = this.getPageDisplaySize(page, thumbnailScale);
584
+ nextThumbnails.push({ pageNumber, url, width: displaySize.width, height: displaySize.height });
585
+ }
586
+ if (token !== this.loadToken) {
587
+ for (const thumbnail of nextThumbnails) {
588
+ this.revokeObjectUrl(thumbnail.url);
589
+ }
590
+ return;
591
+ }
592
+ this.thumbnailPages.set(nextThumbnails);
593
+ for (const thumbnail of previousThumbnails) {
594
+ this.revokeObjectUrl(thumbnail.url);
595
+ }
596
+ }
597
+ async getPageTextGlyphs(pdfDocument, page, scale) {
598
+ if (!this.engine) {
599
+ return [];
600
+ }
601
+ try {
602
+ const geometry = await this.engine.getPageGeometry(pdfDocument, page).toPromise();
603
+ const displaySize = this.getPageDisplaySize(page, scale);
604
+ const glyphs = [];
605
+ for (const run of geometry.runs) {
606
+ for (const glyph of run.glyphs) {
607
+ if ((glyph.flags & 2) === 2 || glyph.width <= 0 || glyph.height <= 0) {
608
+ continue;
609
+ }
610
+ const glyphLeft = glyph.x * scale;
611
+ const glyphTop = glyph.y * scale;
612
+ const glyphWidth = glyph.width * scale;
613
+ const glyphHeight = glyph.height * scale;
614
+ const lineHeight = Math.max(glyphHeight, (run.fontSize ?? glyph.height) * this.textSelectionLineHeight * scale);
615
+ const lineTop = this.clamp(glyphTop - (lineHeight - glyphHeight) / 2, 0, displaySize.height);
616
+ const lineBottom = this.clamp(lineTop + lineHeight, 0, displaySize.height);
617
+ const left = this.clamp(glyphLeft, 0, displaySize.width);
618
+ const right = this.clamp(glyphLeft + glyphWidth, 0, displaySize.width);
619
+ glyphs.push({
620
+ index: glyphs.length,
621
+ left,
622
+ top: this.clamp(glyphTop, 0, displaySize.height),
623
+ width: Math.max(0, right - left),
624
+ height: Math.max(0, glyphHeight),
625
+ lineTop,
626
+ lineHeight: Math.max(0, lineBottom - lineTop),
627
+ lineId: -1,
628
+ });
629
+ }
630
+ }
631
+ return this.assignTextGlyphLines(glyphs).map((glyph) => ({
632
+ ...glyph,
633
+ left: this.roundCssPixel(glyph.left),
634
+ top: this.roundCssPixel(glyph.top),
635
+ width: this.roundCssPixel(glyph.width),
636
+ height: this.roundCssPixel(glyph.height),
637
+ lineTop: this.roundCssPixel(glyph.lineTop),
638
+ lineHeight: this.roundCssPixel(glyph.lineHeight),
639
+ }));
640
+ }
641
+ catch {
642
+ return [];
643
+ }
644
+ }
645
+ assignTextGlyphLines(glyphs) {
646
+ const lines = [];
647
+ for (const glyph of [...glyphs].sort((a, b) => a.lineTop - b.lineTop || a.left - b.left)) {
648
+ const glyphBottom = glyph.lineTop + glyph.lineHeight;
649
+ const glyphCenter = glyph.lineTop + glyph.lineHeight / 2;
650
+ let line = lines.find((candidate) => this.rangesOverlap(candidate.top, candidate.bottom, glyph.lineTop, glyphBottom) ||
651
+ Math.abs(candidate.center - glyphCenter) <= Math.max(2, glyph.lineHeight * 0.4));
652
+ if (!line) {
653
+ line = {
654
+ lineId: lines.length,
655
+ glyphs: [],
656
+ top: glyph.lineTop,
657
+ bottom: glyphBottom,
658
+ left: glyph.left,
659
+ right: glyph.left + glyph.width,
660
+ center: glyphCenter,
661
+ };
662
+ lines.push(line);
663
+ }
664
+ glyph.lineId = line.lineId;
665
+ line.glyphs.push(glyph);
666
+ line.top = Math.min(line.top, glyph.lineTop);
667
+ line.bottom = Math.max(line.bottom, glyphBottom);
668
+ line.left = Math.min(line.left, glyph.left);
669
+ line.right = Math.max(line.right, glyph.left + glyph.width);
670
+ line.center = line.top + (line.bottom - line.top) / 2;
671
+ }
672
+ return glyphs;
673
+ }
674
+ getTextLines(glyphs) {
675
+ const lines = new Map();
676
+ for (const glyph of glyphs) {
677
+ const glyphBottom = glyph.lineTop + glyph.lineHeight;
678
+ const glyphRight = glyph.left + glyph.width;
679
+ const existing = lines.get(glyph.lineId);
680
+ if (!existing) {
681
+ lines.set(glyph.lineId, {
682
+ lineId: glyph.lineId,
683
+ glyphs: [glyph],
684
+ top: glyph.lineTop,
685
+ bottom: glyphBottom,
686
+ left: glyph.left,
687
+ right: glyphRight,
688
+ center: glyph.lineTop + glyph.lineHeight / 2,
689
+ });
690
+ continue;
691
+ }
692
+ existing.glyphs.push(glyph);
693
+ existing.top = Math.min(existing.top, glyph.lineTop);
694
+ existing.bottom = Math.max(existing.bottom, glyphBottom);
695
+ existing.left = Math.min(existing.left, glyph.left);
696
+ existing.right = Math.max(existing.right, glyphRight);
697
+ existing.center = existing.top + (existing.bottom - existing.top) / 2;
698
+ }
699
+ return [...lines.values()]
700
+ .map((line) => ({
701
+ ...line,
702
+ glyphs: [...line.glyphs].sort((a, b) => a.left - b.left || a.index - b.index),
703
+ }))
704
+ .sort((a, b) => a.top - b.top || a.left - b.left);
705
+ }
706
+ rangesOverlap(aStart, aEnd, bStart, bEnd) {
707
+ return aStart < bEnd && aEnd > bStart;
708
+ }
709
+ getCaretOffsetAtPoint(page, point) {
710
+ const lines = this.getTextLines(page.textGlyphs);
711
+ if (lines.length === 0) {
712
+ return 0;
713
+ }
714
+ const line = lines.find((candidate) => point.y >= candidate.top && point.y <= candidate.bottom) ??
715
+ lines.reduce((closest, candidate) => Math.abs(candidate.center - point.y) < Math.abs(closest.center - point.y) ? candidate : closest);
716
+ const glyphs = line.glyphs;
717
+ if (glyphs.length === 0) {
718
+ return 0;
719
+ }
720
+ for (const glyph of glyphs) {
721
+ if (point.x < glyph.left + glyph.width / 2) {
722
+ return glyph.index;
723
+ }
724
+ }
725
+ return glyphs[glyphs.length - 1].index + 1;
726
+ }
727
+ getSelectionPoint(event, page, surface) {
728
+ const rect = surface.getBoundingClientRect();
729
+ const x = this.clamp(event.clientX - rect.left, 0, page.width);
730
+ const y = this.clamp(event.clientY - rect.top, 0, page.height);
731
+ return {
732
+ pageNumber: page.pageNumber,
733
+ x,
734
+ y,
735
+ offset: this.getCaretOffsetAtPoint(page, { x, y }),
736
+ };
737
+ }
738
+ getSelectionRects(page, startOffset, endOffset) {
739
+ const rangeStart = Math.min(startOffset, endOffset);
740
+ const rangeEnd = Math.max(startOffset, endOffset);
741
+ const selectedGlyphs = page.textGlyphs.filter((glyph) => glyph.index >= rangeStart && glyph.index < rangeEnd);
742
+ return this.getTextLines(selectedGlyphs).map((line) => {
743
+ const left = this.clamp(line.left - this.textSelectionHorizontalPadding, 0, page.width);
744
+ const top = this.clamp(line.top, 0, page.height);
745
+ const right = this.clamp(line.right + this.textSelectionHorizontalPadding, 0, page.width);
746
+ const bottom = this.clamp(line.bottom, 0, page.height);
747
+ return {
748
+ text: '',
749
+ pageNumber: page.pageNumber,
750
+ left: this.roundCssPixel(left),
751
+ top: this.roundCssPixel(top),
752
+ width: this.roundCssPixel(Math.max(0, right - left)),
753
+ height: this.roundCssPixel(Math.max(0, bottom - top)),
754
+ };
755
+ });
756
+ }
757
+ async openSource(engine, documentId, source) {
758
+ if (typeof source === 'string') {
759
+ return engine.openDocumentUrl({ id: documentId, url: source }).toPromise();
760
+ }
761
+ if (source instanceof Blob) {
762
+ return engine.openDocumentBuffer({ id: documentId, content: await source.arrayBuffer() }).toPromise();
763
+ }
764
+ if (source instanceof Uint8Array) {
765
+ const content = new Uint8Array(source).buffer;
766
+ return engine.openDocumentBuffer({ id: documentId, content }).toPromise();
767
+ }
768
+ return engine.openDocumentBuffer({ id: documentId, content: source }).toPromise();
769
+ }
770
+ async closeDocument() {
771
+ if (!this.engine || !this.pdfDocument) {
772
+ this.pdfDocument = null;
773
+ return;
774
+ }
775
+ const documentToClose = this.pdfDocument;
776
+ this.pdfDocument = null;
777
+ try {
778
+ await this.engine.closeDocument(documentToClose).toPromise();
779
+ }
780
+ catch {
781
+ // Best-effort cleanup; the viewer should not surface cleanup failures as user-facing load errors.
782
+ }
783
+ }
784
+ revokeRenderedPages() {
785
+ this.disconnectPageObserver();
786
+ this.cancelVisiblePageRenderFrame();
787
+ for (const page of this.renderedPages()) {
788
+ if (page.url) {
789
+ this.revokeObjectUrl(page.url);
790
+ }
791
+ }
792
+ this.renderedPages.set([]);
793
+ }
794
+ revokeThumbnailPages() {
795
+ for (const thumbnail of this.thumbnailPages()) {
796
+ this.revokeObjectUrl(thumbnail.url);
797
+ }
798
+ this.thumbnailPages.set([]);
799
+ }
800
+ revokeObjectUrl(url) {
801
+ const targetWindow = this.document.defaultView;
802
+ if (targetWindow) {
803
+ targetWindow.URL.revokeObjectURL(url);
804
+ return;
805
+ }
806
+ URL.revokeObjectURL(url);
807
+ }
808
+ sanitizePage(pageNumber) {
809
+ return this.clamp(Math.trunc(Number.isFinite(pageNumber) ? pageNumber : 1), 1, Math.max(this.pageCount(), 1));
810
+ }
811
+ sanitizeScale(scale) {
812
+ const bounds = this.getScaleBounds();
813
+ return this.clamp(Number.isFinite(scale) ? scale : 1, bounds.min, bounds.max);
814
+ }
815
+ roundZoom(value) {
816
+ return this.sanitizeScale(Math.round(this.sanitizeScale(value) * 100) / 100);
817
+ }
818
+ clamp(value, min, max) {
819
+ return Math.min(Math.max(value, min), max);
820
+ }
821
+ roundCssPixel(value) {
822
+ return Math.round(value * 100) / 100;
823
+ }
824
+ getDevicePixelRatio() {
825
+ return this.document.defaultView?.devicePixelRatio || 1;
826
+ }
827
+ createDocumentId() {
828
+ return `ngs-pdf-${Date.now()}-${Math.random().toString(36).slice(2)}`;
829
+ }
830
+ isRenderCurrent(loadToken, renderToken) {
831
+ return loadToken === this.loadToken && renderToken === this.renderToken;
832
+ }
833
+ cancelScrollSyncFrame() {
834
+ if (this.scrollSyncFrame === null) {
835
+ return;
836
+ }
837
+ this.document.defaultView?.cancelAnimationFrame(this.scrollSyncFrame);
838
+ this.scrollSyncFrame = null;
839
+ }
840
+ cancelPageObserverFrame() {
841
+ if (this.pageObserverTimeout !== null) {
842
+ this.document.defaultView?.clearTimeout(this.pageObserverTimeout);
843
+ this.pageObserverTimeout = null;
844
+ }
845
+ if (this.pageObserverFrame === null) {
846
+ return;
847
+ }
848
+ this.document.defaultView?.cancelAnimationFrame(this.pageObserverFrame);
849
+ this.pageObserverFrame = null;
850
+ }
851
+ cancelVisiblePageRenderFrame() {
852
+ if (this.visiblePageRenderFrame === null) {
853
+ return;
854
+ }
855
+ this.document.defaultView?.cancelAnimationFrame(this.visiblePageRenderFrame);
856
+ this.visiblePageRenderFrame = null;
857
+ }
858
+ disconnectPageObserver() {
859
+ this.cancelPageObserverFrame();
860
+ this.pageIntersectionObserver?.disconnect();
861
+ this.pageIntersectionObserver = null;
862
+ this.visiblePageRatios.clear();
863
+ }
864
+ schedulePageObserverRefresh() {
865
+ const targetWindow = this.document.defaultView;
866
+ if (!this.isBrowser || !targetWindow) {
867
+ return;
868
+ }
869
+ this.cancelPageObserverFrame();
870
+ this.pageObserverRefreshAttempts = 0;
871
+ this.scheduleNextPageObserverRefresh();
872
+ }
873
+ scheduleNextPageObserverRefresh() {
874
+ const targetWindow = this.document.defaultView;
875
+ if (!targetWindow) {
876
+ return;
877
+ }
878
+ this.pageObserverTimeout = targetWindow.setTimeout(() => {
879
+ this.pageObserverTimeout = null;
880
+ this.pageObserverFrame = targetWindow.requestAnimationFrame(() => {
881
+ this.pageObserverFrame = null;
882
+ const isReady = this.setupPageIntersectionObserver();
883
+ if (!isReady && this.pageObserverRefreshAttempts < 10) {
884
+ this.pageObserverRefreshAttempts++;
885
+ this.scheduleNextPageObserverRefresh();
886
+ return;
887
+ }
888
+ this.syncActivePageFromViewport();
889
+ });
890
+ }, 0);
891
+ }
892
+ setupPageIntersectionObserver() {
893
+ const targetWindow = this.document.defaultView;
894
+ const container = this.viewerBody()?.nativeElement;
895
+ if (!targetWindow || !container || !('IntersectionObserver' in targetWindow)) {
896
+ return false;
897
+ }
898
+ const pages = Array.from(container.querySelectorAll('[data-ngs-pdf-page]'));
899
+ if (pages.length === 0) {
900
+ return false;
901
+ }
902
+ this.pageIntersectionObserver?.disconnect();
903
+ this.visiblePageRatios.clear();
904
+ this.pageIntersectionObserver = new targetWindow.IntersectionObserver((entries) => {
905
+ if (this.isProgrammaticScrollActive()) {
906
+ this.scheduleCurrentVisiblePagesRender();
907
+ return;
908
+ }
909
+ for (const entry of entries) {
910
+ const pageNumber = Number(entry.target.getAttribute('data-ngs-pdf-page'));
911
+ if (!Number.isFinite(pageNumber)) {
912
+ continue;
913
+ }
914
+ this.visiblePageRatios.set(pageNumber, entry.intersectionRatio);
915
+ }
916
+ this.syncActivePageFromIntersections();
917
+ this.scheduleCurrentVisiblePagesRender();
918
+ }, {
919
+ root: container,
920
+ threshold: [0, 0.1, 0.25, 0.5, 0.75, 1],
921
+ });
922
+ for (const page of pages) {
923
+ this.pageIntersectionObserver.observe(page);
924
+ }
925
+ return true;
926
+ }
927
+ getScaleBounds() {
928
+ const min = this.sanitizePositiveNumber(this.minScale(), 0.2);
929
+ const max = this.sanitizePositiveNumber(this.maxScale(), 60);
930
+ return min <= max ? { min, max } : { min: max, max: min };
931
+ }
932
+ sanitizeZoomStep() {
933
+ return this.sanitizePositiveNumber(this.zoomStep(), 0.1);
934
+ }
935
+ sanitizePositiveNumber(value, fallback) {
936
+ return Number.isFinite(value) && value > 0 ? value : fallback;
937
+ }
938
+ getPageDisplaySize(page, scale) {
939
+ const isRotatedSideways = page.rotation === 1 || page.rotation === 3;
940
+ const width = isRotatedSideways ? page.size.height : page.size.width;
941
+ const height = isRotatedSideways ? page.size.width : page.size.height;
942
+ return {
943
+ width: Math.max(1, Math.round(width * scale)),
944
+ height: Math.max(1, Math.round(height * scale)),
945
+ };
946
+ }
947
+ scrollToPage(pageNumber) {
948
+ if (!this.renderAll()) {
949
+ return;
950
+ }
951
+ const container = this.viewerBody()?.nativeElement;
952
+ const target = container?.querySelector(`[data-ngs-pdf-page="${pageNumber}"]`);
953
+ if (!container || !target) {
954
+ return;
955
+ }
956
+ const nextScrollTop = this.getPageScrollTop(container, target);
957
+ const scrollDistance = Math.abs(container.scrollTop - nextScrollTop);
958
+ this.startProgrammaticScrollLock(pageNumber, scrollDistance);
959
+ container.scrollTo({
960
+ top: nextScrollTop,
961
+ behavior: 'smooth',
962
+ });
963
+ }
964
+ syncActivePageFromViewport() {
965
+ const pageNumber = this.findActivePageInViewport();
966
+ this.activatePageFromScroll(pageNumber);
967
+ }
968
+ syncActivePageFromIntersections() {
969
+ let activePage = null;
970
+ let activeRatio = 0;
971
+ for (const [pageNumber, ratio] of this.visiblePageRatios) {
972
+ if (ratio > activeRatio) {
973
+ activeRatio = ratio;
974
+ activePage = pageNumber;
975
+ }
976
+ }
977
+ if (!activePage) {
978
+ activePage = this.findActivePageInViewport();
979
+ }
980
+ this.activatePageFromScroll(activePage);
981
+ }
982
+ activatePageFromScroll(pageNumber) {
983
+ if (this.isProgrammaticScrollActive()) {
984
+ return;
985
+ }
986
+ if (!pageNumber || pageNumber === this.activePage()) {
987
+ return;
988
+ }
989
+ this.activePage.set(pageNumber);
990
+ this.pageChanged.emit(pageNumber);
991
+ this.scrollPageListToPage(pageNumber);
992
+ this.scheduleCurrentVisiblePagesRender();
993
+ }
994
+ findActivePageInViewport() {
995
+ const container = this.viewerBody()?.nativeElement;
996
+ if (!container) {
997
+ return null;
998
+ }
999
+ const containerRect = container.getBoundingClientRect();
1000
+ const viewportCenter = containerRect.top + containerRect.height / 2;
1001
+ const pages = Array.from(container.querySelectorAll('[data-ngs-pdf-page]'));
1002
+ let closestPage = null;
1003
+ let closestDistance = Number.POSITIVE_INFINITY;
1004
+ let mostVisiblePage = null;
1005
+ let mostVisibleHeight = 0;
1006
+ for (const page of pages) {
1007
+ const rect = page.getBoundingClientRect();
1008
+ const pageNumber = Number(page.dataset['ngsPdfPage']);
1009
+ if (!Number.isFinite(pageNumber)) {
1010
+ continue;
1011
+ }
1012
+ if (rect.top <= viewportCenter && rect.bottom >= viewportCenter) {
1013
+ return pageNumber;
1014
+ }
1015
+ const visibleHeight = Math.max(0, Math.min(rect.bottom, containerRect.bottom) - Math.max(rect.top, containerRect.top));
1016
+ if (visibleHeight > mostVisibleHeight) {
1017
+ mostVisibleHeight = visibleHeight;
1018
+ mostVisiblePage = pageNumber;
1019
+ }
1020
+ const distance = Math.min(Math.abs(rect.top - viewportCenter), Math.abs(rect.bottom - viewportCenter));
1021
+ if (distance < closestDistance) {
1022
+ closestDistance = distance;
1023
+ closestPage = pageNumber;
1024
+ }
1025
+ }
1026
+ return mostVisiblePage ?? closestPage;
1027
+ }
1028
+ scrollPageListToPage(pageNumber) {
1029
+ const container = this.pageList()?.nativeElement;
1030
+ const target = container?.querySelector(`[data-ngs-pdf-page-button="${pageNumber}"]`);
1031
+ if (!container || !target) {
1032
+ return;
1033
+ }
1034
+ const containerRect = container.getBoundingClientRect();
1035
+ const targetRect = target.getBoundingClientRect();
1036
+ const isAbove = targetRect.top < containerRect.top;
1037
+ const isBelow = targetRect.bottom > containerRect.bottom;
1038
+ if (!isAbove && !isBelow) {
1039
+ return;
1040
+ }
1041
+ container.scrollTo({
1042
+ top: this.getElementScrollTop(container, target, 'center'),
1043
+ behavior: 'smooth',
1044
+ });
1045
+ }
1046
+ getPageScrollTop(container, target) {
1047
+ return this.getElementScrollTop(container, target, 'start');
1048
+ }
1049
+ getElementScrollTop(container, target, align) {
1050
+ const containerRect = container.getBoundingClientRect();
1051
+ const targetRect = target.getBoundingClientRect();
1052
+ const targetTop = container.scrollTop + targetRect.top - containerRect.top;
1053
+ const alignedTop = align === 'center'
1054
+ ? targetTop - container.clientHeight / 2 + targetRect.height / 2
1055
+ : targetTop;
1056
+ return this.clamp(alignedTop, 0, Math.max(0, container.scrollHeight - container.clientHeight));
1057
+ }
1058
+ getProgrammaticScrollTimeout(distance) {
1059
+ return this.clamp(this.programmaticScrollMinDuration + distance * 0.45, this.programmaticScrollMinDuration, this.programmaticScrollMaxDuration);
1060
+ }
1061
+ startProgrammaticScrollLock(pageNumber, distance) {
1062
+ const targetWindow = this.document.defaultView;
1063
+ if (!targetWindow) {
1064
+ return;
1065
+ }
1066
+ this.clearProgrammaticScrollLock();
1067
+ this.programmaticScrollTargetPage = pageNumber;
1068
+ this.programmaticScrollTimeout = targetWindow.setTimeout(() => {
1069
+ this.programmaticScrollTimeout = null;
1070
+ this.completeProgrammaticScroll();
1071
+ }, this.getProgrammaticScrollTimeout(distance));
1072
+ }
1073
+ isProgrammaticScrollActive() {
1074
+ return this.programmaticScrollTargetPage !== null;
1075
+ }
1076
+ completeProgrammaticScrollIfSettled() {
1077
+ const pageNumber = this.programmaticScrollTargetPage;
1078
+ if (pageNumber === null) {
1079
+ return;
1080
+ }
1081
+ const container = this.viewerBody()?.nativeElement;
1082
+ const target = container?.querySelector(`[data-ngs-pdf-page="${pageNumber}"]`);
1083
+ if (!container || !target) {
1084
+ this.completeProgrammaticScroll();
1085
+ return;
1086
+ }
1087
+ if (Math.abs(container.scrollTop - this.getPageScrollTop(container, target)) <= 2) {
1088
+ this.completeProgrammaticScroll();
1089
+ }
1090
+ }
1091
+ completeProgrammaticScroll() {
1092
+ const pageNumber = this.programmaticScrollTargetPage;
1093
+ this.clearProgrammaticScrollLock();
1094
+ if (pageNumber === null) {
1095
+ return;
1096
+ }
1097
+ if (pageNumber !== this.activePage()) {
1098
+ this.activePage.set(pageNumber);
1099
+ this.pageChanged.emit(pageNumber);
1100
+ }
1101
+ this.scrollPageListToPage(pageNumber);
1102
+ }
1103
+ clearProgrammaticScrollLock() {
1104
+ if (this.programmaticScrollTimeout !== null) {
1105
+ this.document.defaultView?.clearTimeout(this.programmaticScrollTimeout);
1106
+ this.programmaticScrollTimeout = null;
1107
+ }
1108
+ this.programmaticScrollTargetPage = null;
1109
+ }
1110
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PdfViewer, deps: [], target: i0.ɵɵFactoryTarget.Component });
1111
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: PdfViewer, isStandalone: true, selector: "ngs-pdf-viewer", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, wasmUrl: { classPropertyName: "wasmUrl", publicName: "wasmUrl", isSignal: true, isRequired: false, transformFunction: null }, page: { classPropertyName: "page", publicName: "page", isSignal: true, isRequired: false, transformFunction: null }, scale: { classPropertyName: "scale", publicName: "scale", isSignal: true, isRequired: false, transformFunction: null }, minScale: { classPropertyName: "minScale", publicName: "minScale", isSignal: true, isRequired: false, transformFunction: null }, maxScale: { classPropertyName: "maxScale", publicName: "maxScale", isSignal: true, isRequired: false, transformFunction: null }, zoomStep: { classPropertyName: "zoomStep", publicName: "zoomStep", isSignal: true, isRequired: false, transformFunction: null }, renderAll: { classPropertyName: "renderAll", publicName: "renderAll", isSignal: true, isRequired: false, transformFunction: null }, showToolbar: { classPropertyName: "showToolbar", publicName: "showToolbar", isSignal: true, isRequired: false, transformFunction: null }, showPageList: { classPropertyName: "showPageList", publicName: "showPageList", isSignal: true, isRequired: false, transformFunction: null }, withAnnotations: { classPropertyName: "withAnnotations", publicName: "withAnnotations", isSignal: true, isRequired: false, transformFunction: null }, withForms: { classPropertyName: "withForms", publicName: "withForms", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loaded: "loaded", pageChanged: "pageChanged", pageRendered: "pageRendered", error: "error" }, host: { properties: { "class.is-loading": "isLoading()", "class.has-toolbar": "showToolbar()", "class.has-page-list": "isPageListVisible()", "class.has-error": "errorState()" }, classAttribute: "ngs-pdf-viewer not-prose" }, viewQueries: [{ propertyName: "viewerBody", first: true, predicate: ["viewerBody"], descendants: true, isSignal: true }, { propertyName: "pageList", first: true, predicate: ["pageList"], descendants: true, isSignal: true }], exportAs: ["ngsPdfViewer"], ngImport: i0, template: "<ngs-panel class=\"pdf-viewer-panel\">\n @if (showToolbar()) {\n <ngs-panel-header flex>\n <div class=\"pdf-viewer-toolbar\" aria-label=\"PDF viewer controls\">\n @if (showPageList()) {\n <button\n ngsIconButton\n type=\"button\"\n [attr.aria-label]=\"pageListVisible() ? 'Hide pages panel' : 'Show pages panel'\"\n [attr.aria-pressed]=\"pageListVisible()\"\n (click)=\"togglePageList()\">\n <ngs-icon\n [name]=\"pageListVisible() ? 'fluent:panel-left-contract-24-regular' : 'fluent:panel-left-expand-24-regular'\" />\n </button>\n }\n <button ngsIconButton type=\"button\" aria-label=\"Previous page\" [disabled]=\"!canGoPrevious()\" (click)=\"previousPage()\">\n <ngs-icon name=\"fluent:chevron-up-24-regular\" />\n </button>\n <span class=\"pdf-viewer-toolbar__page\">{{ activePage() }} / {{ pageCount() || 1 }}</span>\n <button ngsIconButton type=\"button\" aria-label=\"Next page\" [disabled]=\"!canGoNext()\" (click)=\"nextPage()\">\n <ngs-icon name=\"fluent:chevron-down-24-regular\" />\n </button>\n <span class=\"pdf-viewer-toolbar__spacer\"></span>\n <button ngsIconButton type=\"button\" aria-label=\"Zoom out\" [disabled]=\"!canZoomOut()\" (click)=\"zoomOut()\">\n <ngs-icon name=\"fluent:zoom-out-24-regular\" />\n </button>\n <span class=\"pdf-viewer-toolbar__zoom\">{{ zoomLabel() }}</span>\n <button ngsIconButton type=\"button\" aria-label=\"Zoom in\" [disabled]=\"!canZoomIn()\" (click)=\"zoomIn()\">\n <ngs-icon name=\"fluent:zoom-in-24-regular\" />\n </button>\n <button ngsIconButton type=\"button\" aria-label=\"PDF viewer settings\" [ngsMenuTriggerFor]=\"settingsMenu\">\n <ngs-icon name=\"fluent:settings-24-regular\" />\n </button>\n </div>\n\n <ngs-menu #settingsMenu=\"ngsMenu\" xPosition=\"before\">\n <ngs-menu-heading>SPREAD MODE</ngs-menu-heading>\n <button\n ngs-menu-item\n type=\"button\"\n [selected]=\"spreadMode() === 'single'\"\n (click)=\"setSpreadMode('single')\">\n <ngs-icon name=\"fluent:document-24-regular\" />\n <span>Single Page</span>\n </button>\n <button\n ngs-menu-item\n type=\"button\"\n [selected]=\"spreadMode() === 'two-odd'\"\n (click)=\"setSpreadMode('two-odd')\">\n <ngs-icon name=\"fluent:book-open-24-regular\" />\n <span>Two Page (Odd)</span>\n </button>\n <button\n ngs-menu-item\n type=\"button\"\n [selected]=\"spreadMode() === 'two-even'\"\n (click)=\"setSpreadMode('two-even')\">\n <ngs-icon name=\"fluent:document-text-24-regular\" />\n <span>Two Page (Even)</span>\n </button>\n\n <ngs-menu-divider />\n <ngs-menu-heading>SCROLL LAYOUT</ngs-menu-heading>\n <button\n ngs-menu-item\n type=\"button\"\n [selected]=\"scrollLayout() === 'vertical'\"\n (click)=\"setScrollLayout('vertical')\">\n <ngs-icon name=\"fluent:arrow-sort-24-regular\" />\n <span>Vertical</span>\n </button>\n <button\n ngs-menu-item\n type=\"button\"\n [selected]=\"scrollLayout() === 'horizontal'\"\n (click)=\"setScrollLayout('horizontal')\">\n <ngs-icon name=\"fluent:arrow-swap-24-regular\" />\n <span>Horizontal</span>\n </button>\n\n <ngs-menu-divider />\n <ngs-menu-heading>PAGE ROTATION</ngs-menu-heading>\n <button ngs-menu-item type=\"button\" (click)=\"rotateClockwise()\">\n <ngs-icon name=\"fluent:arrow-clockwise-24-regular\" />\n <span>Rotate Clockwise</span>\n </button>\n <button ngs-menu-item type=\"button\" (click)=\"rotateCounterClockwise()\">\n <ngs-icon name=\"fluent:arrow-counterclockwise-24-regular\" />\n <span>Rotate Counter-Clockwise</span>\n </button>\n\n <ngs-menu-divider />\n <button ngs-menu-item type=\"button\" (click)=\"toggleFullscreen()\">\n <ngs-icon name=\"fluent:arrow-maximize-24-regular\" />\n <span>Fullscreen</span>\n </button>\n </ngs-menu>\n </ngs-panel-header>\n }\n\n @if (isPageListVisible()) {\n <ngs-panel-sidebar>\n <div class=\"pdf-viewer-sidebar\" #pageList>\n <div class=\"pdf-viewer-sidebar__title\">Pages</div>\n <div class=\"pdf-viewer-sidebar__pages\" aria-label=\"PDF pages\">\n @for (pageItem of pageItems(); track pageItem.pageNumber) {\n <button\n class=\"pdf-viewer-sidebar__page\"\n type=\"button\"\n [class.is-active]=\"pageItem.pageNumber === activePage()\"\n [attr.aria-current]=\"pageItem.pageNumber === activePage() ? 'page' : null\"\n [attr.aria-label]=\"'Open page ' + pageItem.pageNumber\"\n [attr.data-ngs-pdf-page-button]=\"pageItem.pageNumber\"\n (click)=\"setPage(pageItem.pageNumber)\">\n <span class=\"pdf-viewer-sidebar__thumb\" aria-hidden=\"true\">\n @if (pageItem.thumbnail; as thumbnail) {\n <img\n [src]=\"thumbnail.url\"\n [attr.width]=\"thumbnail.width\"\n [attr.height]=\"thumbnail.height\"\n alt=\"\"\n draggable=\"false\" />\n } @else {\n <ngs-image-placeholder />\n }\n </span>\n <span class=\"pdf-viewer-sidebar__label\">Page {{ pageItem.pageNumber }}</span>\n </button>\n }\n </div>\n </div>\n </ngs-panel-sidebar>\n }\n\n <ngs-panel-content>\n <div class=\"pdf-viewer-body\" #viewerBody (scroll)=\"onViewerScroll()\">\n <ngs-block-loader [loading]=\"isLoading()\">Loading PDF...</ngs-block-loader>\n @if (errorState()) {\n <div class=\"pdf-viewer-state pdf-viewer-state--error\">\n <ngs-icon name=\"fluent:error-circle-24-regular\" />\n <span>PDF could not be loaded.</span>\n </div>\n } @else if (!src()) {\n <div class=\"pdf-viewer-state\">\n <ngs-icon name=\"fluent:document-pdf-24-regular\" />\n <span>No PDF selected.</span>\n </div>\n } @else {\n <div class=\"pdf-viewer-pages\">\n @for (pdfPage of renderedPages(); track pdfPage.pageNumber) {\n <article\n class=\"pdf-viewer-page\"\n [attr.aria-label]=\"'PDF page ' + pdfPage.pageNumber\"\n [attr.data-ngs-pdf-page]=\"pdfPage.pageNumber\">\n <div\n class=\"pdf-viewer-page__surface\"\n [style.width.px]=\"pdfPage.width\"\n [style.height.px]=\"pdfPage.height\"\n (pointerdown)=\"startTextSelection($event, pdfPage)\"\n (pointermove)=\"updateTextSelection($event, pdfPage)\"\n (pointerup)=\"finishTextSelection($event, pdfPage)\"\n (pointercancel)=\"cancelTextSelection()\"\n (mousedown)=\"startTextSelection($event, pdfPage)\"\n (mousemove)=\"updateTextSelection($event, pdfPage)\"\n (mouseup)=\"finishTextSelection($event, pdfPage)\">\n @if (isPageImageFresh(pdfPage) && pdfPage.url; as pageUrl) {\n <img\n [src]=\"pageUrl\"\n [alt]=\"'PDF page ' + pdfPage.pageNumber\"\n [attr.width]=\"pdfPage.width\"\n [attr.height]=\"pdfPage.height\"\n draggable=\"false\" />\n } @else {\n <div class=\"pdf-viewer-page__placeholder\" aria-hidden=\"true\">\n @if (pdfPage.isRendering) {\n <span></span>\n }\n </div>\n }\n @if (selectionRectsForPage(pdfPage.pageNumber); as selectionRects) {\n @if (selectionRects.length > 0) {\n <div class=\"pdf-viewer-page__selection-layer\" aria-hidden=\"true\">\n @for (selectionRect of selectionRects; track selectionRect.left + '-' + selectionRect.top + '-' + $index) {\n <div\n class=\"pdf-viewer-page__selection-rect\"\n [style.left.px]=\"selectionRect.left\"\n [style.top.px]=\"selectionRect.top\"\n [style.width.px]=\"selectionRect.width\"\n [style.height.px]=\"selectionRect.height\"></div>\n }\n </div>\n }\n }\n </div>\n </article>\n }\n </div>\n }\n </div>\n </ngs-panel-content>\n</ngs-panel>\n", styles: [":host{--ngs-pdf-viewer-height: 640px;--ngs-pdf-viewer-background: var(--ngs-color-surface-container-low, #f5f6f8);--ngs-pdf-viewer-page-background: var(--ngs-color-surface, #ffffff);--ngs-pdf-viewer-page-shadow: var(--ngs-shadow-md, 0 8px 24px rgba(15, 23, 42, .12));--ngs-pdf-viewer-border-color: var(--ngs-color-outline-variant, rgba(15, 23, 42, .12));--ngs-pdf-viewer-radius: var(--ngs-radius-md, 10px);--ngs-pdf-viewer-sidebar-width: 152px;--ngs-pdf-viewer-selection-background: rgb(33 150 243 / .34);display:block;height:var(--ngs-pdf-viewer-height);min-height:320px;overflow:hidden;border:1px solid var(--ngs-pdf-viewer-border-color);border-radius:var(--ngs-pdf-viewer-radius);background:var(--ngs-pdf-viewer-background);color:var(--ngs-color-on-surface, #111827)}:host ngs-panel{height:100%;overflow:hidden;border-radius:inherit;background:var(--ngs-pdf-viewer-background)}:host ngs-panel-header{min-width:0;border-bottom:1px solid var(--ngs-pdf-viewer-border-color);background:var(--ngs-color-surface, #ffffff)}:host ngs-panel-sidebar{width:var(--ngs-pdf-viewer-sidebar-width);min-width:var(--ngs-pdf-viewer-sidebar-width);border-right:1px solid var(--ngs-pdf-viewer-border-color);background:var(--ngs-color-surface, #ffffff)}:host ngs-panel-content{min-width:0;min-height:0;overflow:hidden;background:var(--ngs-pdf-viewer-background)}:host .pdf-viewer-toolbar{display:flex;align-items:center;gap:var(--ngs-spacing-1, .25rem);width:100%;padding:0 var(--ngs-spacing-2, .5rem)}:host .pdf-viewer-toolbar__page,:host .pdf-viewer-toolbar__zoom{min-width:76px;text-align:center;font-size:var(--ngs-font-size-sm, .875rem);font-weight:500;color:var(--ngs-color-on-surface-variant, #4b5563)}:host .pdf-viewer-toolbar__spacer{flex:1 1 auto}:host .pdf-viewer-sidebar{display:flex;flex-direction:column;gap:var(--ngs-spacing-2, .5rem);height:100%;min-height:0;padding:var(--ngs-spacing-3, .75rem);overflow:auto}:host .pdf-viewer-sidebar__title{padding:0 var(--ngs-spacing-1, .25rem);font-size:var(--ngs-font-size-xs, .75rem);font-weight:600;color:var(--ngs-color-on-surface-variant, #6b7280)}:host .pdf-viewer-sidebar__pages{display:flex;flex-direction:column;gap:var(--ngs-spacing-2, .5rem);min-height:0}:host .pdf-viewer-sidebar__page{display:flex;flex-direction:column;align-items:stretch;gap:var(--ngs-spacing-1, .25rem);width:100%;padding:var(--ngs-spacing-1, .25rem);border:1px solid transparent;border-radius:calc(var(--ngs-pdf-viewer-radius) - 4px);background:transparent;color:var(--ngs-color-on-surface-variant, #4b5563);cursor:pointer;text-align:left}:host .pdf-viewer-sidebar__page:hover .pdf-viewer-sidebar__thumb{border-color:var(--ngs-color-outline, rgba(15, 23, 42, .24))}:host .pdf-viewer-sidebar__page:focus-visible{outline:2px solid var(--ngs-color-primary, #155eef);outline-offset:2px}:host .pdf-viewer-sidebar__page.is-active{color:var(--ngs-color-on-surface, #111827)}:host .pdf-viewer-sidebar__page.is-active .pdf-viewer-sidebar__thumb{border-color:var(--ngs-color-primary, #155eef);background:var(--ngs-color-primary-container, rgba(21, 94, 239, .08));box-shadow:0 0 0 2px var(--ngs-color-primary-container, rgba(21, 94, 239, .14)),var(--ngs-shadow-xs, 0 1px 2px rgba(15, 23, 42, .08))}:host .pdf-viewer-sidebar__thumb{display:flex;align-items:center;justify-content:center;aspect-ratio:3/4;width:100%;overflow:hidden;border:1px solid var(--ngs-pdf-viewer-border-color);border-radius:calc(var(--ngs-pdf-viewer-radius) - 6px);background:var(--ngs-pdf-viewer-page-background);box-shadow:var(--ngs-shadow-xs, 0 1px 2px rgba(15, 23, 42, .08))}:host .pdf-viewer-sidebar__thumb img{display:block;width:100%;height:100%;object-fit:contain;-webkit-user-select:none;user-select:none}:host .pdf-viewer-sidebar__thumb ngs-image-placeholder{width:100%;height:100%}:host .pdf-viewer-sidebar__thumb ngs-image-placeholder ::ng-deep svg{width:64%;min-width:0}:host .pdf-viewer-sidebar__thumb ngs-icon{font-size:1.375rem;color:var(--ngs-color-on-surface-variant, #6b7280)}:host .pdf-viewer-sidebar__label{display:block;overflow:hidden;font-size:var(--ngs-font-size-xs, .75rem);font-weight:500;line-height:1.25;text-align:center;text-overflow:ellipsis;white-space:nowrap}:host .pdf-viewer-body{position:absolute;inset:0;overflow:auto}:host .pdf-viewer-pages{display:flex;flex-direction:column;align-items:center;gap:var(--ngs-spacing-6, 1.5rem);min-height:100%;padding:var(--ngs-spacing-6, 1.5rem)}:host .pdf-viewer-page{width:max-content;max-width:none}:host .pdf-viewer-page__surface{position:relative;overflow:hidden;cursor:text;-webkit-user-select:none;user-select:none;border-radius:calc(var(--ngs-pdf-viewer-radius) - 4px);background:var(--ngs-pdf-viewer-page-background);box-shadow:var(--ngs-pdf-viewer-page-shadow);touch-action:none}:host .pdf-viewer-page__surface img{display:block;width:100%;height:100%;max-width:none;-webkit-user-select:none;user-select:none;pointer-events:none}:host .pdf-viewer-page__placeholder{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:var(--ngs-pdf-viewer-page-background);pointer-events:none}:host .pdf-viewer-page__placeholder span{width:32px;height:32px;border:2px solid var(--ngs-color-outline-variant, rgba(15, 23, 42, .12));border-top-color:var(--ngs-color-primary, #155eef);border-radius:999px;animation:ngs-pdf-viewer-page-spin .8s linear infinite}:host .pdf-viewer-page__selection-layer{position:absolute;inset:0;z-index:1;overflow:hidden;isolation:isolate;mix-blend-mode:multiply;forced-color-adjust:none;pointer-events:none}:host .pdf-viewer-page__selection-rect{position:absolute;background:var(--ngs-pdf-viewer-selection-background);pointer-events:none}:host .pdf-viewer-state{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--ngs-spacing-3, .75rem);padding:var(--ngs-spacing-6, 1.5rem);color:var(--ngs-color-on-surface-variant, #6b7280);text-align:center}:host .pdf-viewer-state ngs-icon{font-size:2rem}:host .pdf-viewer-state--error{color:var(--ngs-color-danger, #dc2626)}@keyframes ngs-pdf-viewer-page-spin{to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "component", type: BlockLoader, selector: "ngs-block-loader", inputs: ["loading", "spinnerDiameter", "spinnerStrokeWidth"], exportAs: ["ngsBlockLoader"] }, { kind: "component", type: Button, selector: " button[ngsButton], button[ngsIconButton], a[ngsButton], a[ngsIconButton] ", inputs: ["ngsButton", "ngsIconButton", "loading", "disabled", "disabledInteractive", "disableRipple", "reverse", "fullWidth", "hideTextOnMobile"], exportAs: ["ngsButton"] }, { kind: "component", type: Icon, selector: "ngs-icon", inputs: ["name"], exportAs: ["ngsIcon"] }, { kind: "component", type: ImagePlaceholder, selector: "ngs-image-placeholder", exportAs: ["ngsImagePlaceholder"] }, { kind: "component", type: Menu, selector: "ngs-menu", inputs: ["role", "classList", "xPosition", "yPosition"], outputs: ["closed"], exportAs: ["ngsMenu"] }, { kind: "component", type: MenuDivider, selector: "ngs-menu-divider" }, { kind: "component", type: MenuHeading, selector: "ngs-menu-heading" }, { kind: "component", type: MenuItem, selector: "ngs-menu-item, [ngs-menu-item]", inputs: ["disabled", "role", "selected"], outputs: ["_triggered"], exportAs: ["ngsMenuItem"] }, { kind: "directive", type: MenuTrigger, selector: "[ngsMenuTriggerFor]", inputs: ["ngsMenuTriggerFor", "ngsMenuTriggerData", "ngsMenuDisabled", "xPosition", "yPosition", "ngsMenuTriggerRestoreFocus"], outputs: ["menuOpened", "menuClosed"], exportAs: ["ngsMenuTrigger"] }, { kind: "component", type: Panel, selector: "ngs-panel", inputs: ["absolute"], exportAs: ["ngsPanel"] }, { kind: "component", type: PanelContent, selector: "ngs-panel-content", exportAs: ["ngsPanelContent"] }, { kind: "component", type: PanelHeader, selector: "ngs-panel-header", inputs: ["flex", "autoHeight"], exportAs: ["ngsPanelHeader"] }, { kind: "component", type: PanelSidebar, selector: "ngs-panel-sidebar" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1112
+ }
1113
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PdfViewer, decorators: [{
1114
+ type: Component,
1115
+ args: [{ selector: 'ngs-pdf-viewer', exportAs: 'ngsPdfViewer', standalone: true, imports: [
1116
+ BlockLoader,
1117
+ Button,
1118
+ Icon,
1119
+ ImagePlaceholder,
1120
+ Menu,
1121
+ MenuDivider,
1122
+ MenuHeading,
1123
+ MenuItem,
1124
+ MenuTrigger,
1125
+ Panel,
1126
+ PanelContent,
1127
+ PanelHeader,
1128
+ PanelSidebar,
1129
+ ], changeDetection: ChangeDetectionStrategy.OnPush, host: {
1130
+ class: 'ngs-pdf-viewer not-prose',
1131
+ '[class.is-loading]': 'isLoading()',
1132
+ '[class.has-toolbar]': 'showToolbar()',
1133
+ '[class.has-page-list]': 'isPageListVisible()',
1134
+ '[class.has-error]': 'errorState()',
1135
+ }, template: "<ngs-panel class=\"pdf-viewer-panel\">\n @if (showToolbar()) {\n <ngs-panel-header flex>\n <div class=\"pdf-viewer-toolbar\" aria-label=\"PDF viewer controls\">\n @if (showPageList()) {\n <button\n ngsIconButton\n type=\"button\"\n [attr.aria-label]=\"pageListVisible() ? 'Hide pages panel' : 'Show pages panel'\"\n [attr.aria-pressed]=\"pageListVisible()\"\n (click)=\"togglePageList()\">\n <ngs-icon\n [name]=\"pageListVisible() ? 'fluent:panel-left-contract-24-regular' : 'fluent:panel-left-expand-24-regular'\" />\n </button>\n }\n <button ngsIconButton type=\"button\" aria-label=\"Previous page\" [disabled]=\"!canGoPrevious()\" (click)=\"previousPage()\">\n <ngs-icon name=\"fluent:chevron-up-24-regular\" />\n </button>\n <span class=\"pdf-viewer-toolbar__page\">{{ activePage() }} / {{ pageCount() || 1 }}</span>\n <button ngsIconButton type=\"button\" aria-label=\"Next page\" [disabled]=\"!canGoNext()\" (click)=\"nextPage()\">\n <ngs-icon name=\"fluent:chevron-down-24-regular\" />\n </button>\n <span class=\"pdf-viewer-toolbar__spacer\"></span>\n <button ngsIconButton type=\"button\" aria-label=\"Zoom out\" [disabled]=\"!canZoomOut()\" (click)=\"zoomOut()\">\n <ngs-icon name=\"fluent:zoom-out-24-regular\" />\n </button>\n <span class=\"pdf-viewer-toolbar__zoom\">{{ zoomLabel() }}</span>\n <button ngsIconButton type=\"button\" aria-label=\"Zoom in\" [disabled]=\"!canZoomIn()\" (click)=\"zoomIn()\">\n <ngs-icon name=\"fluent:zoom-in-24-regular\" />\n </button>\n <button ngsIconButton type=\"button\" aria-label=\"PDF viewer settings\" [ngsMenuTriggerFor]=\"settingsMenu\">\n <ngs-icon name=\"fluent:settings-24-regular\" />\n </button>\n </div>\n\n <ngs-menu #settingsMenu=\"ngsMenu\" xPosition=\"before\">\n <ngs-menu-heading>SPREAD MODE</ngs-menu-heading>\n <button\n ngs-menu-item\n type=\"button\"\n [selected]=\"spreadMode() === 'single'\"\n (click)=\"setSpreadMode('single')\">\n <ngs-icon name=\"fluent:document-24-regular\" />\n <span>Single Page</span>\n </button>\n <button\n ngs-menu-item\n type=\"button\"\n [selected]=\"spreadMode() === 'two-odd'\"\n (click)=\"setSpreadMode('two-odd')\">\n <ngs-icon name=\"fluent:book-open-24-regular\" />\n <span>Two Page (Odd)</span>\n </button>\n <button\n ngs-menu-item\n type=\"button\"\n [selected]=\"spreadMode() === 'two-even'\"\n (click)=\"setSpreadMode('two-even')\">\n <ngs-icon name=\"fluent:document-text-24-regular\" />\n <span>Two Page (Even)</span>\n </button>\n\n <ngs-menu-divider />\n <ngs-menu-heading>SCROLL LAYOUT</ngs-menu-heading>\n <button\n ngs-menu-item\n type=\"button\"\n [selected]=\"scrollLayout() === 'vertical'\"\n (click)=\"setScrollLayout('vertical')\">\n <ngs-icon name=\"fluent:arrow-sort-24-regular\" />\n <span>Vertical</span>\n </button>\n <button\n ngs-menu-item\n type=\"button\"\n [selected]=\"scrollLayout() === 'horizontal'\"\n (click)=\"setScrollLayout('horizontal')\">\n <ngs-icon name=\"fluent:arrow-swap-24-regular\" />\n <span>Horizontal</span>\n </button>\n\n <ngs-menu-divider />\n <ngs-menu-heading>PAGE ROTATION</ngs-menu-heading>\n <button ngs-menu-item type=\"button\" (click)=\"rotateClockwise()\">\n <ngs-icon name=\"fluent:arrow-clockwise-24-regular\" />\n <span>Rotate Clockwise</span>\n </button>\n <button ngs-menu-item type=\"button\" (click)=\"rotateCounterClockwise()\">\n <ngs-icon name=\"fluent:arrow-counterclockwise-24-regular\" />\n <span>Rotate Counter-Clockwise</span>\n </button>\n\n <ngs-menu-divider />\n <button ngs-menu-item type=\"button\" (click)=\"toggleFullscreen()\">\n <ngs-icon name=\"fluent:arrow-maximize-24-regular\" />\n <span>Fullscreen</span>\n </button>\n </ngs-menu>\n </ngs-panel-header>\n }\n\n @if (isPageListVisible()) {\n <ngs-panel-sidebar>\n <div class=\"pdf-viewer-sidebar\" #pageList>\n <div class=\"pdf-viewer-sidebar__title\">Pages</div>\n <div class=\"pdf-viewer-sidebar__pages\" aria-label=\"PDF pages\">\n @for (pageItem of pageItems(); track pageItem.pageNumber) {\n <button\n class=\"pdf-viewer-sidebar__page\"\n type=\"button\"\n [class.is-active]=\"pageItem.pageNumber === activePage()\"\n [attr.aria-current]=\"pageItem.pageNumber === activePage() ? 'page' : null\"\n [attr.aria-label]=\"'Open page ' + pageItem.pageNumber\"\n [attr.data-ngs-pdf-page-button]=\"pageItem.pageNumber\"\n (click)=\"setPage(pageItem.pageNumber)\">\n <span class=\"pdf-viewer-sidebar__thumb\" aria-hidden=\"true\">\n @if (pageItem.thumbnail; as thumbnail) {\n <img\n [src]=\"thumbnail.url\"\n [attr.width]=\"thumbnail.width\"\n [attr.height]=\"thumbnail.height\"\n alt=\"\"\n draggable=\"false\" />\n } @else {\n <ngs-image-placeholder />\n }\n </span>\n <span class=\"pdf-viewer-sidebar__label\">Page {{ pageItem.pageNumber }}</span>\n </button>\n }\n </div>\n </div>\n </ngs-panel-sidebar>\n }\n\n <ngs-panel-content>\n <div class=\"pdf-viewer-body\" #viewerBody (scroll)=\"onViewerScroll()\">\n <ngs-block-loader [loading]=\"isLoading()\">Loading PDF...</ngs-block-loader>\n @if (errorState()) {\n <div class=\"pdf-viewer-state pdf-viewer-state--error\">\n <ngs-icon name=\"fluent:error-circle-24-regular\" />\n <span>PDF could not be loaded.</span>\n </div>\n } @else if (!src()) {\n <div class=\"pdf-viewer-state\">\n <ngs-icon name=\"fluent:document-pdf-24-regular\" />\n <span>No PDF selected.</span>\n </div>\n } @else {\n <div class=\"pdf-viewer-pages\">\n @for (pdfPage of renderedPages(); track pdfPage.pageNumber) {\n <article\n class=\"pdf-viewer-page\"\n [attr.aria-label]=\"'PDF page ' + pdfPage.pageNumber\"\n [attr.data-ngs-pdf-page]=\"pdfPage.pageNumber\">\n <div\n class=\"pdf-viewer-page__surface\"\n [style.width.px]=\"pdfPage.width\"\n [style.height.px]=\"pdfPage.height\"\n (pointerdown)=\"startTextSelection($event, pdfPage)\"\n (pointermove)=\"updateTextSelection($event, pdfPage)\"\n (pointerup)=\"finishTextSelection($event, pdfPage)\"\n (pointercancel)=\"cancelTextSelection()\"\n (mousedown)=\"startTextSelection($event, pdfPage)\"\n (mousemove)=\"updateTextSelection($event, pdfPage)\"\n (mouseup)=\"finishTextSelection($event, pdfPage)\">\n @if (isPageImageFresh(pdfPage) && pdfPage.url; as pageUrl) {\n <img\n [src]=\"pageUrl\"\n [alt]=\"'PDF page ' + pdfPage.pageNumber\"\n [attr.width]=\"pdfPage.width\"\n [attr.height]=\"pdfPage.height\"\n draggable=\"false\" />\n } @else {\n <div class=\"pdf-viewer-page__placeholder\" aria-hidden=\"true\">\n @if (pdfPage.isRendering) {\n <span></span>\n }\n </div>\n }\n @if (selectionRectsForPage(pdfPage.pageNumber); as selectionRects) {\n @if (selectionRects.length > 0) {\n <div class=\"pdf-viewer-page__selection-layer\" aria-hidden=\"true\">\n @for (selectionRect of selectionRects; track selectionRect.left + '-' + selectionRect.top + '-' + $index) {\n <div\n class=\"pdf-viewer-page__selection-rect\"\n [style.left.px]=\"selectionRect.left\"\n [style.top.px]=\"selectionRect.top\"\n [style.width.px]=\"selectionRect.width\"\n [style.height.px]=\"selectionRect.height\"></div>\n }\n </div>\n }\n }\n </div>\n </article>\n }\n </div>\n }\n </div>\n </ngs-panel-content>\n</ngs-panel>\n", styles: [":host{--ngs-pdf-viewer-height: 640px;--ngs-pdf-viewer-background: var(--ngs-color-surface-container-low, #f5f6f8);--ngs-pdf-viewer-page-background: var(--ngs-color-surface, #ffffff);--ngs-pdf-viewer-page-shadow: var(--ngs-shadow-md, 0 8px 24px rgba(15, 23, 42, .12));--ngs-pdf-viewer-border-color: var(--ngs-color-outline-variant, rgba(15, 23, 42, .12));--ngs-pdf-viewer-radius: var(--ngs-radius-md, 10px);--ngs-pdf-viewer-sidebar-width: 152px;--ngs-pdf-viewer-selection-background: rgb(33 150 243 / .34);display:block;height:var(--ngs-pdf-viewer-height);min-height:320px;overflow:hidden;border:1px solid var(--ngs-pdf-viewer-border-color);border-radius:var(--ngs-pdf-viewer-radius);background:var(--ngs-pdf-viewer-background);color:var(--ngs-color-on-surface, #111827)}:host ngs-panel{height:100%;overflow:hidden;border-radius:inherit;background:var(--ngs-pdf-viewer-background)}:host ngs-panel-header{min-width:0;border-bottom:1px solid var(--ngs-pdf-viewer-border-color);background:var(--ngs-color-surface, #ffffff)}:host ngs-panel-sidebar{width:var(--ngs-pdf-viewer-sidebar-width);min-width:var(--ngs-pdf-viewer-sidebar-width);border-right:1px solid var(--ngs-pdf-viewer-border-color);background:var(--ngs-color-surface, #ffffff)}:host ngs-panel-content{min-width:0;min-height:0;overflow:hidden;background:var(--ngs-pdf-viewer-background)}:host .pdf-viewer-toolbar{display:flex;align-items:center;gap:var(--ngs-spacing-1, .25rem);width:100%;padding:0 var(--ngs-spacing-2, .5rem)}:host .pdf-viewer-toolbar__page,:host .pdf-viewer-toolbar__zoom{min-width:76px;text-align:center;font-size:var(--ngs-font-size-sm, .875rem);font-weight:500;color:var(--ngs-color-on-surface-variant, #4b5563)}:host .pdf-viewer-toolbar__spacer{flex:1 1 auto}:host .pdf-viewer-sidebar{display:flex;flex-direction:column;gap:var(--ngs-spacing-2, .5rem);height:100%;min-height:0;padding:var(--ngs-spacing-3, .75rem);overflow:auto}:host .pdf-viewer-sidebar__title{padding:0 var(--ngs-spacing-1, .25rem);font-size:var(--ngs-font-size-xs, .75rem);font-weight:600;color:var(--ngs-color-on-surface-variant, #6b7280)}:host .pdf-viewer-sidebar__pages{display:flex;flex-direction:column;gap:var(--ngs-spacing-2, .5rem);min-height:0}:host .pdf-viewer-sidebar__page{display:flex;flex-direction:column;align-items:stretch;gap:var(--ngs-spacing-1, .25rem);width:100%;padding:var(--ngs-spacing-1, .25rem);border:1px solid transparent;border-radius:calc(var(--ngs-pdf-viewer-radius) - 4px);background:transparent;color:var(--ngs-color-on-surface-variant, #4b5563);cursor:pointer;text-align:left}:host .pdf-viewer-sidebar__page:hover .pdf-viewer-sidebar__thumb{border-color:var(--ngs-color-outline, rgba(15, 23, 42, .24))}:host .pdf-viewer-sidebar__page:focus-visible{outline:2px solid var(--ngs-color-primary, #155eef);outline-offset:2px}:host .pdf-viewer-sidebar__page.is-active{color:var(--ngs-color-on-surface, #111827)}:host .pdf-viewer-sidebar__page.is-active .pdf-viewer-sidebar__thumb{border-color:var(--ngs-color-primary, #155eef);background:var(--ngs-color-primary-container, rgba(21, 94, 239, .08));box-shadow:0 0 0 2px var(--ngs-color-primary-container, rgba(21, 94, 239, .14)),var(--ngs-shadow-xs, 0 1px 2px rgba(15, 23, 42, .08))}:host .pdf-viewer-sidebar__thumb{display:flex;align-items:center;justify-content:center;aspect-ratio:3/4;width:100%;overflow:hidden;border:1px solid var(--ngs-pdf-viewer-border-color);border-radius:calc(var(--ngs-pdf-viewer-radius) - 6px);background:var(--ngs-pdf-viewer-page-background);box-shadow:var(--ngs-shadow-xs, 0 1px 2px rgba(15, 23, 42, .08))}:host .pdf-viewer-sidebar__thumb img{display:block;width:100%;height:100%;object-fit:contain;-webkit-user-select:none;user-select:none}:host .pdf-viewer-sidebar__thumb ngs-image-placeholder{width:100%;height:100%}:host .pdf-viewer-sidebar__thumb ngs-image-placeholder ::ng-deep svg{width:64%;min-width:0}:host .pdf-viewer-sidebar__thumb ngs-icon{font-size:1.375rem;color:var(--ngs-color-on-surface-variant, #6b7280)}:host .pdf-viewer-sidebar__label{display:block;overflow:hidden;font-size:var(--ngs-font-size-xs, .75rem);font-weight:500;line-height:1.25;text-align:center;text-overflow:ellipsis;white-space:nowrap}:host .pdf-viewer-body{position:absolute;inset:0;overflow:auto}:host .pdf-viewer-pages{display:flex;flex-direction:column;align-items:center;gap:var(--ngs-spacing-6, 1.5rem);min-height:100%;padding:var(--ngs-spacing-6, 1.5rem)}:host .pdf-viewer-page{width:max-content;max-width:none}:host .pdf-viewer-page__surface{position:relative;overflow:hidden;cursor:text;-webkit-user-select:none;user-select:none;border-radius:calc(var(--ngs-pdf-viewer-radius) - 4px);background:var(--ngs-pdf-viewer-page-background);box-shadow:var(--ngs-pdf-viewer-page-shadow);touch-action:none}:host .pdf-viewer-page__surface img{display:block;width:100%;height:100%;max-width:none;-webkit-user-select:none;user-select:none;pointer-events:none}:host .pdf-viewer-page__placeholder{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:var(--ngs-pdf-viewer-page-background);pointer-events:none}:host .pdf-viewer-page__placeholder span{width:32px;height:32px;border:2px solid var(--ngs-color-outline-variant, rgba(15, 23, 42, .12));border-top-color:var(--ngs-color-primary, #155eef);border-radius:999px;animation:ngs-pdf-viewer-page-spin .8s linear infinite}:host .pdf-viewer-page__selection-layer{position:absolute;inset:0;z-index:1;overflow:hidden;isolation:isolate;mix-blend-mode:multiply;forced-color-adjust:none;pointer-events:none}:host .pdf-viewer-page__selection-rect{position:absolute;background:var(--ngs-pdf-viewer-selection-background);pointer-events:none}:host .pdf-viewer-state{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--ngs-spacing-3, .75rem);padding:var(--ngs-spacing-6, 1.5rem);color:var(--ngs-color-on-surface-variant, #6b7280);text-align:center}:host .pdf-viewer-state ngs-icon{font-size:2rem}:host .pdf-viewer-state--error{color:var(--ngs-color-danger, #dc2626)}@keyframes ngs-pdf-viewer-page-spin{to{transform:rotate(360deg)}}\n"] }]
1136
+ }], ctorParameters: () => [], propDecorators: { viewerBody: [{ type: i0.ViewChild, args: ['viewerBody', { isSignal: true }] }], pageList: [{ type: i0.ViewChild, args: ['pageList', { isSignal: true }] }], src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], wasmUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "wasmUrl", required: false }] }], page: [{ type: i0.Input, args: [{ isSignal: true, alias: "page", required: false }] }], scale: [{ type: i0.Input, args: [{ isSignal: true, alias: "scale", required: false }] }], minScale: [{ type: i0.Input, args: [{ isSignal: true, alias: "minScale", required: false }] }], maxScale: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxScale", required: false }] }], zoomStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomStep", required: false }] }], renderAll: [{ type: i0.Input, args: [{ isSignal: true, alias: "renderAll", required: false }] }], showToolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToolbar", required: false }] }], showPageList: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPageList", required: false }] }], withAnnotations: [{ type: i0.Input, args: [{ isSignal: true, alias: "withAnnotations", required: false }] }], withForms: [{ type: i0.Input, args: [{ isSignal: true, alias: "withForms", required: false }] }], loaded: [{ type: i0.Output, args: ["loaded"] }], pageChanged: [{ type: i0.Output, args: ["pageChanged"] }], pageRendered: [{ type: i0.Output, args: ["pageRendered"] }], error: [{ type: i0.Output, args: ["error"] }] } });
1137
+
1138
+ /**
1139
+ * Generated bundle index. Do not edit.
1140
+ */
1141
+
1142
+ export { PdfViewer, PdfViewerEngineService };
1143
+ //# sourceMappingURL=ngstarter-ui-components-pdf-viewer.mjs.map