@aprova.ch/ngx-next-pdf-viewer 20.0.8
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/README.md +145 -0
- package/fesm2022/aprova.ch-ngx-next-pdf-viewer.mjs +744 -0
- package/fesm2022/aprova.ch-ngx-next-pdf-viewer.mjs.map +1 -0
- package/index.d.ts +213 -0
- package/package.json +51 -0
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, EventEmitter, HostListener, ViewChild, Output, Input, Inject, Component } from '@angular/core';
|
|
3
|
+
import * as i1 from '@angular/common';
|
|
4
|
+
import { CommonModule } from '@angular/common';
|
|
5
|
+
import * as i2 from '@angular/forms';
|
|
6
|
+
import { FormsModule } from '@angular/forms';
|
|
7
|
+
import * as i3 from '@angular/material/icon';
|
|
8
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
9
|
+
import * as i4 from '@angular/material/button';
|
|
10
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
11
|
+
import * as i5 from '@angular/material/tooltip';
|
|
12
|
+
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
13
|
+
import { MatInputModule } from '@angular/material/input';
|
|
14
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
15
|
+
import * as pdfjsLib from 'pdfjs-dist';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* URL to the PDF.js worker script.
|
|
19
|
+
*
|
|
20
|
+
* Default: `'assets/pdf.worker.min.mjs'`
|
|
21
|
+
*
|
|
22
|
+
* Override in your application by providing this token:
|
|
23
|
+
* ```ts
|
|
24
|
+
* providers: [
|
|
25
|
+
* { provide: NGX_PDF_WORKER_SRC, useValue: 'assets/pdf.worker.min.mjs' }
|
|
26
|
+
* ]
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
const NGX_PDF_WORKER_SRC = new InjectionToken('NGX_PDF_WORKER_SRC', { factory: () => 'assets/pdf.worker.min.mjs' });
|
|
30
|
+
|
|
31
|
+
// ──────────────────────────────────────────────
|
|
32
|
+
// Component
|
|
33
|
+
// ──────────────────────────────────────────────
|
|
34
|
+
class NgxNextPdfViewerComponent {
|
|
35
|
+
static { this.DEFAULT_LABELS = {
|
|
36
|
+
zoomOut: 'Zoom out',
|
|
37
|
+
zoomIn: 'Zoom in',
|
|
38
|
+
rotateLeft: 'Rotate left',
|
|
39
|
+
rotateRight: 'Rotate right',
|
|
40
|
+
previousPage: 'Previous page',
|
|
41
|
+
nextPage: 'Next page',
|
|
42
|
+
search: 'Search',
|
|
43
|
+
previousResult: 'Previous result',
|
|
44
|
+
nextResult: 'Next result',
|
|
45
|
+
previousAnnotation: 'Previous annotation',
|
|
46
|
+
nextAnnotation: 'Next annotation',
|
|
47
|
+
selectArea: 'Select area',
|
|
48
|
+
selectText: 'Select & copy text',
|
|
49
|
+
searchPlaceholder: 'Search in PDF…',
|
|
50
|
+
noResults: 'No results',
|
|
51
|
+
loading: 'Loading PDF…',
|
|
52
|
+
noDocument: 'No PDF loaded',
|
|
53
|
+
loadError: 'Failed to load PDF',
|
|
54
|
+
}; }
|
|
55
|
+
get l() {
|
|
56
|
+
return { ...NgxNextPdfViewerComponent.DEFAULT_LABELS, ...this.labels };
|
|
57
|
+
}
|
|
58
|
+
constructor(cdr, zone, el, workerSrc) {
|
|
59
|
+
this.cdr = cdr;
|
|
60
|
+
this.zone = zone;
|
|
61
|
+
this.el = el;
|
|
62
|
+
/** Bounding-box annotations to overlay */
|
|
63
|
+
this.annotations = [];
|
|
64
|
+
/** Items shown in the context menu after drag-selecting boxes */
|
|
65
|
+
this.contextMenuItems = [];
|
|
66
|
+
/** Fill colour for a single-clicked box (CSS colour string) */
|
|
67
|
+
this.highlightingColor = 'rgba(255, 200, 0, 0.35)';
|
|
68
|
+
/** Fill colour for drag-selected boxes (CSS colour string) */
|
|
69
|
+
this.selectedColor = 'rgba(0, 120, 215, 0.35)';
|
|
70
|
+
/** Stroke colour for all annotation polygons */
|
|
71
|
+
this.annotationStroke = '#e97332';
|
|
72
|
+
/** Initial zoom: numeric level (1.0 = 100 %) or 'fit-to-page' | 'fit-to-width' | 'fit-to-height' */
|
|
73
|
+
this.initialZoom = 1.5;
|
|
74
|
+
/** Override any subset of the viewer's user-visible strings */
|
|
75
|
+
this.labels = {};
|
|
76
|
+
// ── Outputs ─────────────────────────────────
|
|
77
|
+
/** Fired when the user clicks a single annotation box */
|
|
78
|
+
this.boxClicked = new EventEmitter();
|
|
79
|
+
/** Fired when the user picks a context-menu item after drag-selection */
|
|
80
|
+
this.boxesSelected = new EventEmitter();
|
|
81
|
+
// ── PDF state ────────────────────────────────
|
|
82
|
+
this.pdfDoc = null;
|
|
83
|
+
this.renderedPages = [];
|
|
84
|
+
this.currentPage = 1;
|
|
85
|
+
this.totalPages = 0;
|
|
86
|
+
this.zoom = 1.5;
|
|
87
|
+
this.userRotation = 0;
|
|
88
|
+
// ── Annotation state ─────────────────────────
|
|
89
|
+
this.clickedBoxKey = null;
|
|
90
|
+
this.multiSelectedKeys = new Set();
|
|
91
|
+
// ── Drag-select state ────────────────────────
|
|
92
|
+
this.drag = { active: false, pageNum: 0, startX: 0, startY: 0, currentX: 0, currentY: 0 };
|
|
93
|
+
// ── Context-menu state ───────────────────────
|
|
94
|
+
this.ctxVisible = false;
|
|
95
|
+
this.ctxX = 0;
|
|
96
|
+
this.ctxY = 0;
|
|
97
|
+
// ── Search state ─────────────────────────────
|
|
98
|
+
this.searchVisible = false;
|
|
99
|
+
this.searchQuery = '';
|
|
100
|
+
this.searchMatches = [];
|
|
101
|
+
this.searchIndex = -1;
|
|
102
|
+
// ── Selection mode ───────────────────────────
|
|
103
|
+
this.selectionMode = 'select-area';
|
|
104
|
+
// ── Loading ──────────────────────────────────
|
|
105
|
+
this.loading = false;
|
|
106
|
+
this.loadError = false;
|
|
107
|
+
this.renderTasks = new Map();
|
|
108
|
+
this.textLayers = new Map();
|
|
109
|
+
pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;
|
|
110
|
+
}
|
|
111
|
+
ngAfterViewInit() {
|
|
112
|
+
if (typeof this.initialZoom === 'number') {
|
|
113
|
+
this.zoom = this.initialZoom;
|
|
114
|
+
}
|
|
115
|
+
// Ctrl+scroll zooming – requires passive:false to call preventDefault()
|
|
116
|
+
this.zone.runOutsideAngular(() => {
|
|
117
|
+
this.wheelListener = (e) => {
|
|
118
|
+
if (!e.ctrlKey)
|
|
119
|
+
return;
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
this.zone.run(() => e.deltaY < 0 ? this.zoomIn() : this.zoomOut());
|
|
122
|
+
};
|
|
123
|
+
this.el.nativeElement.addEventListener('wheel', this.wheelListener, { passive: false });
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
ngOnChanges(changes) {
|
|
127
|
+
if (changes['blob'] && this.blob) {
|
|
128
|
+
console.log('load new file');
|
|
129
|
+
this.loadPdf(this.blob);
|
|
130
|
+
}
|
|
131
|
+
if (changes['annotations'] && !changes['blob']) {
|
|
132
|
+
console.log('set new annotations', this.annotations.length);
|
|
133
|
+
this.cdr.detectChanges();
|
|
134
|
+
if (this.annotations.length === 1) {
|
|
135
|
+
this.scrollToBox(this.annotations[0]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
ngOnDestroy() {
|
|
140
|
+
if (this.wheelListener) {
|
|
141
|
+
this.el.nativeElement.removeEventListener('wheel', this.wheelListener);
|
|
142
|
+
}
|
|
143
|
+
this.removeAllTextLayers();
|
|
144
|
+
this.pdfDoc?.destroy();
|
|
145
|
+
}
|
|
146
|
+
// ─────────────────────────────────────────────
|
|
147
|
+
// PDF loading & rendering
|
|
148
|
+
// ─────────────────────────────────────────────
|
|
149
|
+
async loadPdf(blob) {
|
|
150
|
+
this.loading = true;
|
|
151
|
+
this.loadError = false;
|
|
152
|
+
this.renderedPages = [];
|
|
153
|
+
this.clickedBoxKey = null;
|
|
154
|
+
this.multiSelectedKeys.clear();
|
|
155
|
+
this.searchMatches = [];
|
|
156
|
+
this.searchIndex = -1;
|
|
157
|
+
this.removeAllTextLayers();
|
|
158
|
+
this.cdr.detectChanges();
|
|
159
|
+
try {
|
|
160
|
+
this.pdfDoc?.destroy();
|
|
161
|
+
const buf = await blob.arrayBuffer();
|
|
162
|
+
this.pdfDoc = await pdfjsLib.getDocument({ data: buf }).promise;
|
|
163
|
+
this.totalPages = this.pdfDoc.numPages;
|
|
164
|
+
this.currentPage = 1;
|
|
165
|
+
if (typeof this.initialZoom !== 'number') {
|
|
166
|
+
this.zoom = await this.computeFitZoom(this.initialZoom);
|
|
167
|
+
}
|
|
168
|
+
this.loading = false;
|
|
169
|
+
this.cdr.detectChanges();
|
|
170
|
+
for (let p = 1; p <= this.totalPages; p++) {
|
|
171
|
+
await this.renderPage(p);
|
|
172
|
+
}
|
|
173
|
+
if (this.annotations.length === 1) {
|
|
174
|
+
this.scrollToBox(this.annotations[0]);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
this.loading = false;
|
|
179
|
+
this.loadError = true;
|
|
180
|
+
}
|
|
181
|
+
this.cdr.detectChanges();
|
|
182
|
+
}
|
|
183
|
+
async computeFitZoom(mode) {
|
|
184
|
+
if (!this.pdfDoc)
|
|
185
|
+
return 1;
|
|
186
|
+
const page = await this.pdfDoc.getPage(1);
|
|
187
|
+
const rotation = (page.rotate + this.userRotation) % 360;
|
|
188
|
+
const vp = page.getViewport({ scale: 1, rotation });
|
|
189
|
+
page.cleanup();
|
|
190
|
+
const container = this.pdfContainer?.nativeElement;
|
|
191
|
+
if (!container)
|
|
192
|
+
return 1;
|
|
193
|
+
// Subtract the 16 px padding on each side defined in npv-scroll-inner
|
|
194
|
+
const availW = container.clientWidth - 32;
|
|
195
|
+
const availH = container.clientHeight - 32;
|
|
196
|
+
const scaleW = availW / vp.width;
|
|
197
|
+
const scaleH = availH / vp.height;
|
|
198
|
+
switch (mode) {
|
|
199
|
+
case 'fit-to-width': return scaleW;
|
|
200
|
+
case 'fit-to-height': return scaleH;
|
|
201
|
+
case 'fit-to-page': return Math.min(scaleW, scaleH);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async renderPage(pageNum) {
|
|
205
|
+
if (!this.pdfDoc)
|
|
206
|
+
return;
|
|
207
|
+
const page = await this.pdfDoc.getPage(pageNum);
|
|
208
|
+
const totalRotation = (page.rotate + this.userRotation) % 360;
|
|
209
|
+
const viewport = page.getViewport({ scale: this.zoom, rotation: totalRotation });
|
|
210
|
+
// HiDPI: render at physical pixel density, display at CSS size
|
|
211
|
+
const dpr = window.devicePixelRatio || 1;
|
|
212
|
+
const hiResViewport = page.getViewport({ scale: this.zoom * dpr, rotation: totalRotation });
|
|
213
|
+
const canvas = document.getElementById(`npv-canvas-${pageNum}`);
|
|
214
|
+
if (!canvas)
|
|
215
|
+
return;
|
|
216
|
+
canvas.width = hiResViewport.width;
|
|
217
|
+
canvas.height = hiResViewport.height;
|
|
218
|
+
canvas.style.width = `${viewport.width}px`;
|
|
219
|
+
canvas.style.height = `${viewport.height}px`;
|
|
220
|
+
if (this.renderTasks.has(pageNum)) {
|
|
221
|
+
try {
|
|
222
|
+
this.renderTasks.get(pageNum).cancel();
|
|
223
|
+
}
|
|
224
|
+
catch (_) { }
|
|
225
|
+
}
|
|
226
|
+
const ctx = canvas.getContext('2d');
|
|
227
|
+
const task = page.render({ canvasContext: ctx, viewport: hiResViewport });
|
|
228
|
+
this.renderTasks.set(pageNum, task);
|
|
229
|
+
try {
|
|
230
|
+
await task.promise;
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
if (e?.name !== 'RenderingCancelledException')
|
|
234
|
+
console.error(e);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this.renderTasks.delete(pageNum);
|
|
238
|
+
const existing = this.renderedPages.find(r => r.pageNum === pageNum);
|
|
239
|
+
if (existing) {
|
|
240
|
+
existing.width = viewport.width;
|
|
241
|
+
existing.height = viewport.height;
|
|
242
|
+
existing.rotation = totalRotation;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
this.renderedPages.push({ pageNum, width: viewport.width, height: viewport.height, rotation: totalRotation });
|
|
246
|
+
}
|
|
247
|
+
this.cdr.detectChanges();
|
|
248
|
+
page.cleanup();
|
|
249
|
+
}
|
|
250
|
+
async rerenderAll() {
|
|
251
|
+
if (!this.pdfDoc)
|
|
252
|
+
return;
|
|
253
|
+
if (this.selectionMode === 'select-text') {
|
|
254
|
+
this.removeAllTextLayers();
|
|
255
|
+
}
|
|
256
|
+
for (let p = 1; p <= this.totalPages; p++) {
|
|
257
|
+
await this.renderPage(p);
|
|
258
|
+
if (this.selectionMode === 'select-text') {
|
|
259
|
+
await this.renderTextLayer(p);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
this.cdr.detectChanges();
|
|
263
|
+
}
|
|
264
|
+
// ─────────────────────────────────────────────
|
|
265
|
+
// Coordinate transformation
|
|
266
|
+
// ─────────────────────────────────────────────
|
|
267
|
+
/** Converts a polygon (y=0 at top, any consistent unit) to SVG canvas coordinates */
|
|
268
|
+
transformPolygon(polygon, pdfWidth, pdfHeight, canvasWidth, canvasHeight, rotation) {
|
|
269
|
+
const norm = ((rotation % 360) + 360) % 360;
|
|
270
|
+
const result = [];
|
|
271
|
+
for (let i = 0; i < polygon.length; i += 2) {
|
|
272
|
+
const px = polygon[i];
|
|
273
|
+
const py = polygon[i + 1];
|
|
274
|
+
let cx, cy;
|
|
275
|
+
switch (norm) {
|
|
276
|
+
case 90:
|
|
277
|
+
cx = (pdfHeight - py) * canvasWidth / pdfHeight;
|
|
278
|
+
cy = px * canvasHeight / pdfWidth;
|
|
279
|
+
break;
|
|
280
|
+
case 180:
|
|
281
|
+
cx = (pdfWidth - px) * canvasWidth / pdfWidth;
|
|
282
|
+
cy = (pdfHeight - py) * canvasHeight / pdfHeight;
|
|
283
|
+
break;
|
|
284
|
+
case 270:
|
|
285
|
+
cx = py * canvasWidth / pdfHeight;
|
|
286
|
+
cy = (pdfWidth - px) * canvasHeight / pdfWidth;
|
|
287
|
+
break;
|
|
288
|
+
default:
|
|
289
|
+
cx = px * canvasWidth / pdfWidth;
|
|
290
|
+
cy = py * canvasHeight / pdfHeight;
|
|
291
|
+
}
|
|
292
|
+
result.push(cx, cy);
|
|
293
|
+
}
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
getSvgPoints(box, pi) {
|
|
297
|
+
const pts = this.transformPolygon(box.polygon, box.width, box.height, pi.width, pi.height, pi.rotation);
|
|
298
|
+
const pairs = [];
|
|
299
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
300
|
+
pairs.push(`${pts[i].toFixed(2)},${pts[i + 1].toFixed(2)}`);
|
|
301
|
+
}
|
|
302
|
+
return pairs.join(' ');
|
|
303
|
+
}
|
|
304
|
+
polygonBBox(pts) {
|
|
305
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
306
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
307
|
+
minX = Math.min(minX, pts[i]);
|
|
308
|
+
maxX = Math.max(maxX, pts[i]);
|
|
309
|
+
minY = Math.min(minY, pts[i + 1]);
|
|
310
|
+
maxY = Math.max(maxY, pts[i + 1]);
|
|
311
|
+
}
|
|
312
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
313
|
+
}
|
|
314
|
+
boxKey(box) {
|
|
315
|
+
return JSON.stringify({ page: box.page, polygon: box.polygon });
|
|
316
|
+
}
|
|
317
|
+
// ─────────────────────────────────────────────
|
|
318
|
+
// Template helpers
|
|
319
|
+
// ─────────────────────────────────────────────
|
|
320
|
+
getPageInfo(pageNum) {
|
|
321
|
+
return this.renderedPages.find(r => r.pageNum === pageNum);
|
|
322
|
+
}
|
|
323
|
+
getBoxesForPage(pageNum) {
|
|
324
|
+
return this.annotations.filter(b => b.page === pageNum);
|
|
325
|
+
}
|
|
326
|
+
isClicked(box) {
|
|
327
|
+
return this.clickedBoxKey === this.boxKey(box);
|
|
328
|
+
}
|
|
329
|
+
isMultiSelected(box) {
|
|
330
|
+
return this.multiSelectedKeys.has(this.boxKey(box));
|
|
331
|
+
}
|
|
332
|
+
pageRange() {
|
|
333
|
+
return Array.from({ length: this.totalPages }, (_, i) => i + 1);
|
|
334
|
+
}
|
|
335
|
+
getDragRect(pageNum) {
|
|
336
|
+
if (!this.drag.active || this.drag.pageNum !== pageNum)
|
|
337
|
+
return null;
|
|
338
|
+
return {
|
|
339
|
+
x: Math.min(this.drag.startX, this.drag.currentX),
|
|
340
|
+
y: Math.min(this.drag.startY, this.drag.currentY),
|
|
341
|
+
w: Math.abs(this.drag.currentX - this.drag.startX),
|
|
342
|
+
h: Math.abs(this.drag.currentY - this.drag.startY)
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
getSearchMatchesForPage(pageNum) {
|
|
346
|
+
return this.searchMatches.filter(m => m.pageNum === pageNum);
|
|
347
|
+
}
|
|
348
|
+
getSearchIndexOffset(pageNum) {
|
|
349
|
+
return this.searchMatches.findIndex(m => m.pageNum === pageNum);
|
|
350
|
+
}
|
|
351
|
+
isCurrentSearchMatch(pageNum, localIndex) {
|
|
352
|
+
const offset = this.getSearchIndexOffset(pageNum);
|
|
353
|
+
return offset >= 0 && offset + localIndex === this.searchIndex;
|
|
354
|
+
}
|
|
355
|
+
searchMatchPoints(m) {
|
|
356
|
+
const pairs = [];
|
|
357
|
+
for (let i = 0; i < m.polygon.length; i += 2) {
|
|
358
|
+
pairs.push(`${m.polygon[i].toFixed(2)},${m.polygon[i + 1].toFixed(2)}`);
|
|
359
|
+
}
|
|
360
|
+
return pairs.join(' ');
|
|
361
|
+
}
|
|
362
|
+
// ─────────────────────────────────────────────
|
|
363
|
+
// Box interaction
|
|
364
|
+
// ─────────────────────────────────────────────
|
|
365
|
+
onBoxClick(event, box) {
|
|
366
|
+
event.stopPropagation();
|
|
367
|
+
this.ctxVisible = false;
|
|
368
|
+
this.multiSelectedKeys.clear();
|
|
369
|
+
const key = this.boxKey(box);
|
|
370
|
+
const wasSelected = this.clickedBoxKey === key;
|
|
371
|
+
this.clickedBoxKey = wasSelected ? null : key;
|
|
372
|
+
this.cdr.detectChanges();
|
|
373
|
+
this.boxClicked.emit({ box, selected: !wasSelected });
|
|
374
|
+
}
|
|
375
|
+
// ─────────────────────────────────────────────
|
|
376
|
+
// Drag-select
|
|
377
|
+
// ─────────────────────────────────────────────
|
|
378
|
+
onSvgMouseDown(event, pageNum) {
|
|
379
|
+
if (this.selectionMode === 'select-text')
|
|
380
|
+
return;
|
|
381
|
+
if (event.button !== 0)
|
|
382
|
+
return;
|
|
383
|
+
const tag = event.target.tagName?.toLowerCase();
|
|
384
|
+
if (tag === 'polygon' || tag === 'title')
|
|
385
|
+
return;
|
|
386
|
+
this.ctxVisible = false;
|
|
387
|
+
this.clickedBoxKey = null;
|
|
388
|
+
const pt = this.svgPoint(event, pageNum);
|
|
389
|
+
if (!pt)
|
|
390
|
+
return;
|
|
391
|
+
this.drag = { active: true, pageNum, startX: pt.x, startY: pt.y, currentX: pt.x, currentY: pt.y };
|
|
392
|
+
}
|
|
393
|
+
onDocMouseMove(event) {
|
|
394
|
+
if (!this.drag.active)
|
|
395
|
+
return;
|
|
396
|
+
const pt = this.svgPoint(event, this.drag.pageNum);
|
|
397
|
+
if (!pt)
|
|
398
|
+
return;
|
|
399
|
+
this.drag.currentX = pt.x;
|
|
400
|
+
this.drag.currentY = pt.y;
|
|
401
|
+
this.cdr.detectChanges();
|
|
402
|
+
}
|
|
403
|
+
onDocMouseUp(event) {
|
|
404
|
+
if (!this.drag.active)
|
|
405
|
+
return;
|
|
406
|
+
const rect = this.getDragRect(this.drag.pageNum);
|
|
407
|
+
this.drag.active = false;
|
|
408
|
+
if (!rect || (rect.w < 5 && rect.h < 5)) {
|
|
409
|
+
this.cdr.detectChanges();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const pageInfo = this.getPageInfo(this.drag.pageNum);
|
|
413
|
+
if (!pageInfo) {
|
|
414
|
+
this.cdr.detectChanges();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const hits = [];
|
|
418
|
+
for (const box of this.annotations) {
|
|
419
|
+
const pts = this.transformPolygon(box.polygon, box.width, box.height, pageInfo.width, pageInfo.height, pageInfo.rotation);
|
|
420
|
+
const bb = this.polygonBBox(pts);
|
|
421
|
+
if (this.rectsIntersect(rect, bb))
|
|
422
|
+
hits.push(box);
|
|
423
|
+
}
|
|
424
|
+
this.multiSelectedKeys = new Set(hits.map(b => this.boxKey(b)));
|
|
425
|
+
if (hits.length > 0 && this.contextMenuItems.length > 0) {
|
|
426
|
+
this.ctxX = event.clientX;
|
|
427
|
+
this.ctxY = event.clientY;
|
|
428
|
+
this.ctxVisible = true;
|
|
429
|
+
}
|
|
430
|
+
this.cdr.detectChanges();
|
|
431
|
+
}
|
|
432
|
+
svgPoint(event, pageNum) {
|
|
433
|
+
const svg = document.getElementById(`npv-svg-${pageNum}`);
|
|
434
|
+
if (!svg)
|
|
435
|
+
return null;
|
|
436
|
+
const r = svg.getBoundingClientRect();
|
|
437
|
+
return { x: event.clientX - r.left, y: event.clientY - r.top };
|
|
438
|
+
}
|
|
439
|
+
rectsIntersect(a, b) {
|
|
440
|
+
return !(a.x + a.w < b.x || b.x + b.w < a.x || a.y + a.h < b.y || b.y + b.h < a.y);
|
|
441
|
+
}
|
|
442
|
+
// ─────────────────────────────────────────────
|
|
443
|
+
// Context menu
|
|
444
|
+
// ─────────────────────────────────────────────
|
|
445
|
+
onContextMenuAction(item) {
|
|
446
|
+
this.ctxVisible = false;
|
|
447
|
+
const selected = this.annotations.filter(b => this.multiSelectedKeys.has(this.boxKey(b)));
|
|
448
|
+
this.boxesSelected.emit({ boxes: selected, action: item.action });
|
|
449
|
+
this.multiSelectedKeys.clear();
|
|
450
|
+
this.cdr.detectChanges();
|
|
451
|
+
}
|
|
452
|
+
onDocClick() {
|
|
453
|
+
if (this.ctxVisible) {
|
|
454
|
+
this.ctxVisible = false;
|
|
455
|
+
this.cdr.detectChanges();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// ─────────────────────────────────────────────
|
|
459
|
+
// Toolbar – zoom
|
|
460
|
+
// ─────────────────────────────────────────────
|
|
461
|
+
zoomIn() { this.zoom = Math.min(this.zoom + 0.25, 5); this.rerenderAll(); }
|
|
462
|
+
zoomOut() { this.zoom = Math.max(this.zoom - 0.25, 0.25); this.rerenderAll(); }
|
|
463
|
+
get zoomPercent() { return Math.round(this.zoom * 100); }
|
|
464
|
+
// ─────────────────────────────────────────────
|
|
465
|
+
// Toolbar – rotation
|
|
466
|
+
// ─────────────────────────────────────────────
|
|
467
|
+
rotateLeft() { this.userRotation = (this.userRotation + 270) % 360; this.rerenderAll(); }
|
|
468
|
+
rotateRight() { this.userRotation = (this.userRotation + 90) % 360; this.rerenderAll(); }
|
|
469
|
+
// ─────────────────────────────────────────────
|
|
470
|
+
// Toolbar – page navigation
|
|
471
|
+
// ─────────────────────────────────────────────
|
|
472
|
+
goToPrevPage() { if (this.currentPage > 1)
|
|
473
|
+
this.scrollToPageNum(this.currentPage - 1); }
|
|
474
|
+
goToNextPage() { if (this.currentPage < this.totalPages)
|
|
475
|
+
this.scrollToPageNum(this.currentPage + 1); }
|
|
476
|
+
scrollToPageNum(pageNum) {
|
|
477
|
+
this.currentPage = pageNum;
|
|
478
|
+
document.getElementById(`npv-page-${pageNum}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
479
|
+
}
|
|
480
|
+
scrollToBox(box) {
|
|
481
|
+
this.currentPage = box.page;
|
|
482
|
+
console.log('scroll to page', this.currentPage);
|
|
483
|
+
const pageEl = document.getElementById(`npv-page-${box.page}`);
|
|
484
|
+
if (!pageEl) {
|
|
485
|
+
console.log('no page element found, stop scrolling');
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const pi = this.getPageInfo(box.page);
|
|
489
|
+
if (!pi) {
|
|
490
|
+
pageEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
491
|
+
console.log('scroll into view done');
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const pts = this.transformPolygon(box.polygon, box.width, box.height, pi.width, pi.height, pi.rotation);
|
|
495
|
+
const bb = this.polygonBBox(pts);
|
|
496
|
+
const top = pageEl.offsetTop + bb.y - window.innerHeight / 2;
|
|
497
|
+
this.pdfContainer?.nativeElement.scrollTo({ top, behavior: 'smooth' });
|
|
498
|
+
console.log('scrolling to element done');
|
|
499
|
+
}
|
|
500
|
+
// ─────────────────────────────────────────────
|
|
501
|
+
// Toolbar – search
|
|
502
|
+
// ─────────────────────────────────────────────
|
|
503
|
+
toggleSearch() {
|
|
504
|
+
this.searchVisible = !this.searchVisible;
|
|
505
|
+
if (!this.searchVisible) {
|
|
506
|
+
this.searchQuery = '';
|
|
507
|
+
this.searchMatches = [];
|
|
508
|
+
this.searchIndex = -1;
|
|
509
|
+
this.cdr.detectChanges();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async onSearchInput() {
|
|
513
|
+
const q = this.searchQuery.trim().toLowerCase();
|
|
514
|
+
this.searchMatches = [];
|
|
515
|
+
this.searchIndex = -1;
|
|
516
|
+
if (!q || !this.pdfDoc) {
|
|
517
|
+
this.cdr.detectChanges();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
for (let p = 1; p <= this.totalPages; p++) {
|
|
521
|
+
const pi = this.getPageInfo(p);
|
|
522
|
+
if (!pi)
|
|
523
|
+
continue;
|
|
524
|
+
const page = await this.pdfDoc.getPage(p);
|
|
525
|
+
const text = await page.getTextContent();
|
|
526
|
+
const vp = page.getViewport({ scale: this.zoom, rotation: pi.rotation });
|
|
527
|
+
for (const item of text.items) {
|
|
528
|
+
if (!item.str)
|
|
529
|
+
continue;
|
|
530
|
+
const idx = item.str.toLowerCase().indexOf(q);
|
|
531
|
+
if (idx < 0)
|
|
532
|
+
continue;
|
|
533
|
+
const charW = (item.width / item.str.length) || 6;
|
|
534
|
+
const itemH = Math.abs(item.transform[3]) || 10;
|
|
535
|
+
const x0 = item.transform[4] + idx * charW;
|
|
536
|
+
const y0 = item.transform[5];
|
|
537
|
+
const t = vp.transform;
|
|
538
|
+
const applyT = (x, y) => [t[0] * x + t[2] * y + t[4], t[1] * x + t[3] * y + t[5]];
|
|
539
|
+
const [cx0, cy0] = applyT(x0, y0 + itemH);
|
|
540
|
+
const [cx1, cy1] = applyT(x0 + q.length * charW, y0);
|
|
541
|
+
this.searchMatches.push({
|
|
542
|
+
pageNum: p,
|
|
543
|
+
polygon: [cx0, cy0, cx1, cy0, cx1, cy1, cx0, cy1]
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
page.cleanup();
|
|
547
|
+
}
|
|
548
|
+
if (this.searchMatches.length > 0) {
|
|
549
|
+
this.searchIndex = 0;
|
|
550
|
+
this.scrollToSearchMatch(0);
|
|
551
|
+
}
|
|
552
|
+
this.cdr.detectChanges();
|
|
553
|
+
}
|
|
554
|
+
searchNext() {
|
|
555
|
+
if (!this.searchMatches.length)
|
|
556
|
+
return;
|
|
557
|
+
this.searchIndex = (this.searchIndex + 1) % this.searchMatches.length;
|
|
558
|
+
this.scrollToSearchMatch(this.searchIndex);
|
|
559
|
+
this.cdr.detectChanges();
|
|
560
|
+
}
|
|
561
|
+
searchPrev() {
|
|
562
|
+
if (!this.searchMatches.length)
|
|
563
|
+
return;
|
|
564
|
+
this.searchIndex = (this.searchIndex - 1 + this.searchMatches.length) % this.searchMatches.length;
|
|
565
|
+
this.scrollToSearchMatch(this.searchIndex);
|
|
566
|
+
this.cdr.detectChanges();
|
|
567
|
+
}
|
|
568
|
+
scrollToSearchMatch(idx) {
|
|
569
|
+
const m = this.searchMatches[idx];
|
|
570
|
+
if (!m)
|
|
571
|
+
return;
|
|
572
|
+
document.getElementById(`npv-page-${m.pageNum}`)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
573
|
+
}
|
|
574
|
+
// ─────────────────────────────────────────────
|
|
575
|
+
// Toolbar – annotation navigation
|
|
576
|
+
// ─────────────────────────────────────────────
|
|
577
|
+
get annotationIndex() {
|
|
578
|
+
if (!this.clickedBoxKey)
|
|
579
|
+
return -1;
|
|
580
|
+
return this.annotations.findIndex(b => this.boxKey(b) === this.clickedBoxKey);
|
|
581
|
+
}
|
|
582
|
+
goToPrevAnnotation() {
|
|
583
|
+
if (!this.annotations.length)
|
|
584
|
+
return;
|
|
585
|
+
const prev = (this.annotationIndex - 1 + this.annotations.length) % this.annotations.length;
|
|
586
|
+
this.selectAnnotation(prev);
|
|
587
|
+
}
|
|
588
|
+
goToNextAnnotation() {
|
|
589
|
+
if (!this.annotations.length)
|
|
590
|
+
return;
|
|
591
|
+
const next = (this.annotationIndex + 1) % this.annotations.length;
|
|
592
|
+
this.selectAnnotation(next);
|
|
593
|
+
}
|
|
594
|
+
selectAnnotation(index) {
|
|
595
|
+
const box = this.annotations[index];
|
|
596
|
+
if (!box)
|
|
597
|
+
return;
|
|
598
|
+
this.multiSelectedKeys.clear();
|
|
599
|
+
this.ctxVisible = false;
|
|
600
|
+
this.clickedBoxKey = this.boxKey(box);
|
|
601
|
+
this.cdr.detectChanges();
|
|
602
|
+
this.scrollToBox(box);
|
|
603
|
+
this.boxClicked.emit({ box, selected: true });
|
|
604
|
+
}
|
|
605
|
+
// ─────────────────────────────────────────────
|
|
606
|
+
// Toolbar – selection mode
|
|
607
|
+
// ─────────────────────────────────────────────
|
|
608
|
+
toggleSelectionMode() {
|
|
609
|
+
this.selectionMode = this.selectionMode === 'select-area' ? 'select-text' : 'select-area';
|
|
610
|
+
if (this.selectionMode === 'select-text') {
|
|
611
|
+
this.renderedPages.forEach(rp => this.renderTextLayer(rp.pageNum));
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
this.removeAllTextLayers();
|
|
615
|
+
}
|
|
616
|
+
this.cdr.detectChanges();
|
|
617
|
+
}
|
|
618
|
+
async renderTextLayer(pageNum) {
|
|
619
|
+
if (!this.pdfDoc)
|
|
620
|
+
return;
|
|
621
|
+
const pi = this.getPageInfo(pageNum);
|
|
622
|
+
if (!pi)
|
|
623
|
+
return;
|
|
624
|
+
const wrapper = document.getElementById(`npv-page-${pageNum}`);
|
|
625
|
+
if (!wrapper)
|
|
626
|
+
return;
|
|
627
|
+
this.removeTextLayer(pageNum);
|
|
628
|
+
const page = await this.pdfDoc.getPage(pageNum);
|
|
629
|
+
const totalRotation = (page.rotate + this.userRotation) % 360;
|
|
630
|
+
const viewport = page.getViewport({ scale: this.zoom, rotation: totalRotation });
|
|
631
|
+
const div = document.createElement('div');
|
|
632
|
+
div.className = 'npv-text-layer';
|
|
633
|
+
div.style.width = `${pi.width}px`;
|
|
634
|
+
div.style.height = `${pi.height}px`;
|
|
635
|
+
wrapper.appendChild(div);
|
|
636
|
+
const textLayer = new pdfjsLib.TextLayer({
|
|
637
|
+
textContentSource: page.streamTextContent(),
|
|
638
|
+
container: div,
|
|
639
|
+
viewport,
|
|
640
|
+
});
|
|
641
|
+
this.textLayers.set(pageNum, textLayer);
|
|
642
|
+
try {
|
|
643
|
+
await textLayer.render();
|
|
644
|
+
}
|
|
645
|
+
catch (_) { }
|
|
646
|
+
page.cleanup();
|
|
647
|
+
}
|
|
648
|
+
removeTextLayer(pageNum) {
|
|
649
|
+
const tl = this.textLayers.get(pageNum);
|
|
650
|
+
if (tl) {
|
|
651
|
+
try {
|
|
652
|
+
tl.cancel();
|
|
653
|
+
}
|
|
654
|
+
catch (_) { }
|
|
655
|
+
this.textLayers.delete(pageNum);
|
|
656
|
+
}
|
|
657
|
+
const wrapper = document.getElementById(`npv-page-${pageNum}`);
|
|
658
|
+
wrapper?.querySelector('.npv-text-layer')?.remove();
|
|
659
|
+
}
|
|
660
|
+
removeAllTextLayers() {
|
|
661
|
+
for (const pageNum of Array.from(this.textLayers.keys())) {
|
|
662
|
+
this.removeTextLayer(pageNum);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// ─────────────────────────────────────────────
|
|
666
|
+
// Scroll tracking
|
|
667
|
+
// ─────────────────────────────────────────────
|
|
668
|
+
onContainerScroll() {
|
|
669
|
+
if (!this.pdfContainer)
|
|
670
|
+
return;
|
|
671
|
+
const top = this.pdfContainer.nativeElement.scrollTop;
|
|
672
|
+
let closest = 1, minDist = Infinity;
|
|
673
|
+
for (let p = 1; p <= this.totalPages; p++) {
|
|
674
|
+
const el = document.getElementById(`npv-page-${p}`);
|
|
675
|
+
if (!el)
|
|
676
|
+
continue;
|
|
677
|
+
const dist = Math.abs(el.offsetTop - top);
|
|
678
|
+
if (dist < minDist) {
|
|
679
|
+
minDist = dist;
|
|
680
|
+
closest = p;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (this.currentPage !== closest) {
|
|
684
|
+
this.currentPage = closest;
|
|
685
|
+
this.cdr.detectChanges();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NgxNextPdfViewerComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: i0.NgZone }, { token: i0.ElementRef }, { token: NGX_PDF_WORKER_SRC }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
689
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: NgxNextPdfViewerComponent, isStandalone: true, selector: "ngx-next-pdf-viewer", inputs: { blob: "blob", annotations: "annotations", contextMenuItems: "contextMenuItems", highlightingColor: "highlightingColor", selectedColor: "selectedColor", annotationStroke: "annotationStroke", initialZoom: "initialZoom", labels: "labels" }, outputs: { boxClicked: "boxClicked", boxesSelected: "boxesSelected" }, host: { listeners: { "mousemove": "onDocMouseMove($event)", "mouseup": "onDocMouseUp($event)", "document:click": "onDocClick()" } }, viewQueries: [{ propertyName: "pdfContainer", first: true, predicate: ["pdfContainer"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"npv-root\">\n\n <!-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Toolbar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"npv-toolbar\">\n\n <button mat-icon-button [matTooltip]=\"l.zoomOut\" (click)=\"zoomOut()\" [disabled]=\"zoom <= 0.25\">\n <mat-icon>zoom_out</mat-icon>\n </button>\n <span class=\"npv-label\">{{ zoomPercent }}%</span>\n <button mat-icon-button [matTooltip]=\"l.zoomIn\" (click)=\"zoomIn()\" [disabled]=\"zoom >= 5\">\n <mat-icon>zoom_in</mat-icon>\n </button>\n\n <div class=\"npv-sep\"></div>\n\n <button mat-icon-button [matTooltip]=\"l.rotateLeft\" (click)=\"rotateLeft()\">\n <mat-icon>rotate_left</mat-icon>\n </button>\n <button mat-icon-button [matTooltip]=\"l.rotateRight\" (click)=\"rotateRight()\">\n <mat-icon>rotate_right</mat-icon>\n </button>\n\n <div class=\"npv-sep\"></div>\n\n <button mat-icon-button [matTooltip]=\"l.previousPage\" (click)=\"goToPrevPage()\" [disabled]=\"currentPage <= 1\">\n <mat-icon>chevron_left</mat-icon>\n </button>\n <span class=\"npv-label\">{{ currentPage }} / {{ totalPages }}</span>\n <button mat-icon-button [matTooltip]=\"l.nextPage\" (click)=\"goToNextPage()\" [disabled]=\"currentPage >= totalPages\">\n <mat-icon>chevron_right</mat-icon>\n </button>\n\n <ng-container *ngIf=\"annotations.length\">\n <div class=\"npv-sep\"></div>\n\n <button mat-icon-button [matTooltip]=\"l.previousAnnotation\" (click)=\"goToPrevAnnotation()\">\n <mat-icon>navigate_before</mat-icon>\n </button>\n <span class=\"npv-label\">\n {{ annotationIndex >= 0 ? annotationIndex + 1 : '\u2013' }} / {{ annotations.length }}\n </span>\n <button mat-icon-button [matTooltip]=\"l.nextAnnotation\" (click)=\"goToNextAnnotation()\">\n <mat-icon>navigate_next</mat-icon>\n </button>\n </ng-container>\n\n <div class=\"npv-sep\"></div>\n\n <button mat-icon-button\n [matTooltip]=\"selectionMode === 'select-area' ? l.selectText : l.selectArea\"\n (click)=\"toggleSelectionMode()\"\n [class.npv-active]=\"selectionMode === 'select-text'\">\n <mat-icon>text_fields</mat-icon>\n </button>\n\n <div class=\"npv-sep\"></div>\n\n <button mat-icon-button [matTooltip]=\"l.search\" (click)=\"toggleSearch()\" [class.npv-active]=\"searchVisible\">\n <mat-icon>search</mat-icon>\n </button>\n\n <ng-container *ngIf=\"searchVisible\">\n <input\n class=\"npv-search-input\"\n type=\"text\"\n [placeholder]=\"l.searchPlaceholder\"\n [(ngModel)]=\"searchQuery\"\n (input)=\"onSearchInput()\"\n (keydown.enter)=\"searchNext()\"\n (keydown.shift.enter)=\"searchPrev()\"\n autofocus\n />\n <button mat-icon-button [matTooltip]=\"l.previousResult\"\n (click)=\"searchPrev()\" [disabled]=\"!searchMatches.length\">\n <mat-icon>expand_less</mat-icon>\n </button>\n <button mat-icon-button [matTooltip]=\"l.nextResult\"\n (click)=\"searchNext()\" [disabled]=\"!searchMatches.length\">\n <mat-icon>expand_more</mat-icon>\n </button>\n <span class=\"npv-search-count\" *ngIf=\"searchMatches.length\">\n {{ searchIndex + 1 }} / {{ searchMatches.length }}\n </span>\n <span class=\"npv-search-count npv-no-match\" *ngIf=\"searchQuery && !searchMatches.length\">\n {{ l.noResults }}\n </span>\n </ng-container>\n\n </div>\n\n <!-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"npv-loading\" *ngIf=\"loading\">\n <span class=\"npv-spinner\"></span> {{ l.loading }}\n </div>\n\n <!-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Scroll area \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"npv-scroll-area\" #pdfContainer\n (scroll)=\"onContainerScroll()\" (click)=\"ctxVisible = false\">\n <div class=\"npv-scroll-inner\">\n\n <ng-container *ngFor=\"let p of pageRange()\">\n <div class=\"npv-page-wrapper\" [id]=\"'npv-page-' + p\">\n\n <canvas [id]=\"'npv-canvas-' + p\" class=\"npv-canvas\"></canvas>\n\n <svg *ngIf=\"getPageInfo(p) as pi\"\n [id]=\"'npv-svg-' + p\"\n class=\"npv-overlay\"\n [class.npv-overlay--text-mode]=\"selectionMode === 'select-text'\"\n [attr.width]=\"pi.width\" [attr.height]=\"pi.height\"\n [attr.viewBox]=\"'0 0 ' + pi.width + ' ' + pi.height\"\n (mousedown)=\"onSvgMouseDown($event, p)\">\n\n <!-- Hit area for drag selection on empty page space -->\n <rect x=\"0\" y=\"0\" [attr.width]=\"pi.width\" [attr.height]=\"pi.height\"\n fill=\"transparent\"></rect>\n\n <!-- Annotation boxes -->\n <polygon\n *ngFor=\"let box of getBoxesForPage(p)\"\n [attr.points]=\"getSvgPoints(box, pi)\"\n class=\"npv-box\"\n [class.npv-box--clicked]=\"isClicked(box)\"\n [class.npv-box--multi]=\"isMultiSelected(box)\"\n [style.fill]=\"isClicked(box) ? highlightingColor : (isMultiSelected(box) ? selectedColor : 'transparent')\"\n [style.stroke]=\"annotationStroke\"\n (click)=\"onBoxClick($event, box)\">\n <title>{{ box.content || '' }}</title>\n </polygon>\n\n <!-- Search highlights -->\n <polygon\n *ngFor=\"let m of getSearchMatchesForPage(p); let mi = index\"\n [attr.points]=\"searchMatchPoints(m)\"\n class=\"npv-search-highlight\"\n [class.npv-search-highlight--current]=\"isCurrentSearchMatch(p, mi)\">\n </polygon>\n\n <!-- Drag selection rectangle -->\n <rect *ngIf=\"getDragRect(p) as dr\"\n [attr.x]=\"dr.x\" [attr.y]=\"dr.y\"\n [attr.width]=\"dr.w\" [attr.height]=\"dr.h\"\n class=\"npv-drag-rect\">\n </rect>\n\n </svg>\n </div>\n </ng-container>\n\n <div class=\"npv-placeholder npv-placeholder--error\" *ngIf=\"!loading && loadError\">\n {{ l.loadError }}\n </div>\n <div class=\"npv-placeholder\" *ngIf=\"!loading && !loadError && totalPages === 0\">\n {{ l.noDocument }}\n </div>\n\n </div><!-- /npv-scroll-inner -->\n </div>\n\n <!-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Context menu \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div *ngIf=\"ctxVisible && contextMenuItems.length\"\n class=\"npv-context-menu\"\n [style.left.px]=\"ctxX\" [style.top.px]=\"ctxY\"\n (click)=\"$event.stopPropagation()\">\n <div *ngFor=\"let item of contextMenuItems\"\n class=\"npv-context-item\"\n (click)=\"onContextMenuAction(item)\">\n {{ item.label }}\n </div>\n </div>\n\n</div>\n", styles: [".npv-root{display:flex;flex-direction:column;height:100%;overflow:hidden;background:#e9ecee;position:relative;font-family:Roboto,sans-serif}.npv-toolbar{display:flex;align-items:center;gap:2px;padding:4px 8px;background:#fff;color:#000;min-height:48px;flex-shrink:0;z-index:10;box-shadow:0 2px 4px #0006}.npv-toolbar button.mat-icon-button{color:#505050}.npv-toolbar button.mat-icon-button:hover:not([disabled]){color:#fff;background:#ffffff1a}.npv-toolbar button.mat-icon-button[disabled]{opacity:.35}.npv-toolbar button.mat-icon-button.npv-active{color:#90caf9;background:#90caf91a}.npv-toolbar .npv-sep{width:1px;height:24px;background:#00000026;margin:0 4px}.npv-toolbar .npv-label{font-size:13px;min-width:50px;text-align:center;color:#505050;-webkit-user-select:none;user-select:none}.npv-toolbar .npv-search-input{background:#0000000f;border:1px solid rgba(0,0,0,.2);border-radius:4px;color:#000;font-size:13px;padding:4px 8px;outline:none;width:180px}.npv-toolbar .npv-search-input::placeholder{color:#0006}.npv-toolbar .npv-search-input:focus{border-color:#1976d2}.npv-toolbar .npv-search-count{font-size:12px;color:#607d8b;white-space:nowrap;min-width:60px}.npv-toolbar .npv-search-count.npv-no-match{color:#e53935}.npv-loading{display:flex;align-items:center;justify-content:center;gap:10px;padding:16px;color:#666;font-size:14px;flex-shrink:0}.npv-spinner{width:20px;height:20px;border:3px solid rgba(0,0,0,.1);border-top-color:#1976d2;border-radius:50%;animation:npv-spin .8s linear infinite}@keyframes npv-spin{to{transform:rotate(360deg)}}.npv-scroll-area{flex:1;overflow:auto}.npv-scroll-inner{display:flex;flex-direction:column;align-items:center;min-width:max-content;padding:16px;gap:16px}.npv-page-wrapper{position:relative;display:inline-block;box-shadow:0 4px 12px #00000080;line-height:0}.npv-canvas{display:block}.npv-overlay{position:absolute;top:0;left:0;pointer-events:all;overflow:visible;cursor:crosshair;user-select:none;-webkit-user-select:none}.npv-overlay.npv-overlay--text-mode{pointer-events:none;cursor:text}:host ::ng-deep .npv-text-layer{position:absolute;top:0;left:0;overflow:hidden;line-height:1;-webkit-text-size-adjust:none;text-size-adjust:none;forced-color-adjust:none}:host ::ng-deep .npv-text-layer span,:host ::ng-deep .npv-text-layer br{color:transparent;position:absolute;white-space:pre;cursor:text;transform-origin:0% 0%}:host ::ng-deep .npv-text-layer ::selection{background:#0078d740;color:transparent}.npv-box{pointer-events:all;stroke-width:2;fill:transparent;cursor:pointer;transition:fill .15s ease,stroke-width .15s ease}.npv-box:hover{stroke-width:3;filter:drop-shadow(0 0 3px rgba(233,115,50,.7))}.npv-box.npv-box--clicked{stroke-width:3}.npv-box.npv-box--multi{stroke-width:2.5}.npv-search-highlight{fill:#ffeb3b59;stroke:#ffc800cc;stroke-width:1.5;pointer-events:none}.npv-search-highlight.npv-search-highlight--current{fill:#ff980073;stroke:#ff6400e6;stroke-width:2}.npv-drag-rect{fill:#0078d71a;stroke:#0078d7b3;stroke-width:1.5;stroke-dasharray:5 3;pointer-events:none}.npv-placeholder{color:#aaa;font-size:14px;padding:40px;text-align:center}.npv-placeholder.npv-placeholder--error{color:#e53935}.npv-context-menu{position:fixed;z-index:9999;background:#fff;border:1px solid rgba(0,0,0,.12);border-radius:4px;box-shadow:0 4px 16px #0003;padding:4px 0;min-width:160px}.npv-context-item{padding:10px 16px;font-size:14px;cursor:pointer;color:#000000de;-webkit-user-select:none;user-select:none}.npv-context-item:hover{background:#0000000d}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "ngmodule", type: MatFormFieldModule }] }); }
|
|
690
|
+
}
|
|
691
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NgxNextPdfViewerComponent, decorators: [{
|
|
692
|
+
type: Component,
|
|
693
|
+
args: [{ selector: 'ngx-next-pdf-viewer', standalone: true, imports: [
|
|
694
|
+
CommonModule,
|
|
695
|
+
FormsModule,
|
|
696
|
+
MatIconModule,
|
|
697
|
+
MatButtonModule,
|
|
698
|
+
MatTooltipModule,
|
|
699
|
+
MatInputModule,
|
|
700
|
+
MatFormFieldModule
|
|
701
|
+
], template: "<div class=\"npv-root\">\n\n <!-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Toolbar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"npv-toolbar\">\n\n <button mat-icon-button [matTooltip]=\"l.zoomOut\" (click)=\"zoomOut()\" [disabled]=\"zoom <= 0.25\">\n <mat-icon>zoom_out</mat-icon>\n </button>\n <span class=\"npv-label\">{{ zoomPercent }}%</span>\n <button mat-icon-button [matTooltip]=\"l.zoomIn\" (click)=\"zoomIn()\" [disabled]=\"zoom >= 5\">\n <mat-icon>zoom_in</mat-icon>\n </button>\n\n <div class=\"npv-sep\"></div>\n\n <button mat-icon-button [matTooltip]=\"l.rotateLeft\" (click)=\"rotateLeft()\">\n <mat-icon>rotate_left</mat-icon>\n </button>\n <button mat-icon-button [matTooltip]=\"l.rotateRight\" (click)=\"rotateRight()\">\n <mat-icon>rotate_right</mat-icon>\n </button>\n\n <div class=\"npv-sep\"></div>\n\n <button mat-icon-button [matTooltip]=\"l.previousPage\" (click)=\"goToPrevPage()\" [disabled]=\"currentPage <= 1\">\n <mat-icon>chevron_left</mat-icon>\n </button>\n <span class=\"npv-label\">{{ currentPage }} / {{ totalPages }}</span>\n <button mat-icon-button [matTooltip]=\"l.nextPage\" (click)=\"goToNextPage()\" [disabled]=\"currentPage >= totalPages\">\n <mat-icon>chevron_right</mat-icon>\n </button>\n\n <ng-container *ngIf=\"annotations.length\">\n <div class=\"npv-sep\"></div>\n\n <button mat-icon-button [matTooltip]=\"l.previousAnnotation\" (click)=\"goToPrevAnnotation()\">\n <mat-icon>navigate_before</mat-icon>\n </button>\n <span class=\"npv-label\">\n {{ annotationIndex >= 0 ? annotationIndex + 1 : '\u2013' }} / {{ annotations.length }}\n </span>\n <button mat-icon-button [matTooltip]=\"l.nextAnnotation\" (click)=\"goToNextAnnotation()\">\n <mat-icon>navigate_next</mat-icon>\n </button>\n </ng-container>\n\n <div class=\"npv-sep\"></div>\n\n <button mat-icon-button\n [matTooltip]=\"selectionMode === 'select-area' ? l.selectText : l.selectArea\"\n (click)=\"toggleSelectionMode()\"\n [class.npv-active]=\"selectionMode === 'select-text'\">\n <mat-icon>text_fields</mat-icon>\n </button>\n\n <div class=\"npv-sep\"></div>\n\n <button mat-icon-button [matTooltip]=\"l.search\" (click)=\"toggleSearch()\" [class.npv-active]=\"searchVisible\">\n <mat-icon>search</mat-icon>\n </button>\n\n <ng-container *ngIf=\"searchVisible\">\n <input\n class=\"npv-search-input\"\n type=\"text\"\n [placeholder]=\"l.searchPlaceholder\"\n [(ngModel)]=\"searchQuery\"\n (input)=\"onSearchInput()\"\n (keydown.enter)=\"searchNext()\"\n (keydown.shift.enter)=\"searchPrev()\"\n autofocus\n />\n <button mat-icon-button [matTooltip]=\"l.previousResult\"\n (click)=\"searchPrev()\" [disabled]=\"!searchMatches.length\">\n <mat-icon>expand_less</mat-icon>\n </button>\n <button mat-icon-button [matTooltip]=\"l.nextResult\"\n (click)=\"searchNext()\" [disabled]=\"!searchMatches.length\">\n <mat-icon>expand_more</mat-icon>\n </button>\n <span class=\"npv-search-count\" *ngIf=\"searchMatches.length\">\n {{ searchIndex + 1 }} / {{ searchMatches.length }}\n </span>\n <span class=\"npv-search-count npv-no-match\" *ngIf=\"searchQuery && !searchMatches.length\">\n {{ l.noResults }}\n </span>\n </ng-container>\n\n </div>\n\n <!-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"npv-loading\" *ngIf=\"loading\">\n <span class=\"npv-spinner\"></span> {{ l.loading }}\n </div>\n\n <!-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Scroll area \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"npv-scroll-area\" #pdfContainer\n (scroll)=\"onContainerScroll()\" (click)=\"ctxVisible = false\">\n <div class=\"npv-scroll-inner\">\n\n <ng-container *ngFor=\"let p of pageRange()\">\n <div class=\"npv-page-wrapper\" [id]=\"'npv-page-' + p\">\n\n <canvas [id]=\"'npv-canvas-' + p\" class=\"npv-canvas\"></canvas>\n\n <svg *ngIf=\"getPageInfo(p) as pi\"\n [id]=\"'npv-svg-' + p\"\n class=\"npv-overlay\"\n [class.npv-overlay--text-mode]=\"selectionMode === 'select-text'\"\n [attr.width]=\"pi.width\" [attr.height]=\"pi.height\"\n [attr.viewBox]=\"'0 0 ' + pi.width + ' ' + pi.height\"\n (mousedown)=\"onSvgMouseDown($event, p)\">\n\n <!-- Hit area for drag selection on empty page space -->\n <rect x=\"0\" y=\"0\" [attr.width]=\"pi.width\" [attr.height]=\"pi.height\"\n fill=\"transparent\"></rect>\n\n <!-- Annotation boxes -->\n <polygon\n *ngFor=\"let box of getBoxesForPage(p)\"\n [attr.points]=\"getSvgPoints(box, pi)\"\n class=\"npv-box\"\n [class.npv-box--clicked]=\"isClicked(box)\"\n [class.npv-box--multi]=\"isMultiSelected(box)\"\n [style.fill]=\"isClicked(box) ? highlightingColor : (isMultiSelected(box) ? selectedColor : 'transparent')\"\n [style.stroke]=\"annotationStroke\"\n (click)=\"onBoxClick($event, box)\">\n <title>{{ box.content || '' }}</title>\n </polygon>\n\n <!-- Search highlights -->\n <polygon\n *ngFor=\"let m of getSearchMatchesForPage(p); let mi = index\"\n [attr.points]=\"searchMatchPoints(m)\"\n class=\"npv-search-highlight\"\n [class.npv-search-highlight--current]=\"isCurrentSearchMatch(p, mi)\">\n </polygon>\n\n <!-- Drag selection rectangle -->\n <rect *ngIf=\"getDragRect(p) as dr\"\n [attr.x]=\"dr.x\" [attr.y]=\"dr.y\"\n [attr.width]=\"dr.w\" [attr.height]=\"dr.h\"\n class=\"npv-drag-rect\">\n </rect>\n\n </svg>\n </div>\n </ng-container>\n\n <div class=\"npv-placeholder npv-placeholder--error\" *ngIf=\"!loading && loadError\">\n {{ l.loadError }}\n </div>\n <div class=\"npv-placeholder\" *ngIf=\"!loading && !loadError && totalPages === 0\">\n {{ l.noDocument }}\n </div>\n\n </div><!-- /npv-scroll-inner -->\n </div>\n\n <!-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Context menu \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div *ngIf=\"ctxVisible && contextMenuItems.length\"\n class=\"npv-context-menu\"\n [style.left.px]=\"ctxX\" [style.top.px]=\"ctxY\"\n (click)=\"$event.stopPropagation()\">\n <div *ngFor=\"let item of contextMenuItems\"\n class=\"npv-context-item\"\n (click)=\"onContextMenuAction(item)\">\n {{ item.label }}\n </div>\n </div>\n\n</div>\n", styles: [".npv-root{display:flex;flex-direction:column;height:100%;overflow:hidden;background:#e9ecee;position:relative;font-family:Roboto,sans-serif}.npv-toolbar{display:flex;align-items:center;gap:2px;padding:4px 8px;background:#fff;color:#000;min-height:48px;flex-shrink:0;z-index:10;box-shadow:0 2px 4px #0006}.npv-toolbar button.mat-icon-button{color:#505050}.npv-toolbar button.mat-icon-button:hover:not([disabled]){color:#fff;background:#ffffff1a}.npv-toolbar button.mat-icon-button[disabled]{opacity:.35}.npv-toolbar button.mat-icon-button.npv-active{color:#90caf9;background:#90caf91a}.npv-toolbar .npv-sep{width:1px;height:24px;background:#00000026;margin:0 4px}.npv-toolbar .npv-label{font-size:13px;min-width:50px;text-align:center;color:#505050;-webkit-user-select:none;user-select:none}.npv-toolbar .npv-search-input{background:#0000000f;border:1px solid rgba(0,0,0,.2);border-radius:4px;color:#000;font-size:13px;padding:4px 8px;outline:none;width:180px}.npv-toolbar .npv-search-input::placeholder{color:#0006}.npv-toolbar .npv-search-input:focus{border-color:#1976d2}.npv-toolbar .npv-search-count{font-size:12px;color:#607d8b;white-space:nowrap;min-width:60px}.npv-toolbar .npv-search-count.npv-no-match{color:#e53935}.npv-loading{display:flex;align-items:center;justify-content:center;gap:10px;padding:16px;color:#666;font-size:14px;flex-shrink:0}.npv-spinner{width:20px;height:20px;border:3px solid rgba(0,0,0,.1);border-top-color:#1976d2;border-radius:50%;animation:npv-spin .8s linear infinite}@keyframes npv-spin{to{transform:rotate(360deg)}}.npv-scroll-area{flex:1;overflow:auto}.npv-scroll-inner{display:flex;flex-direction:column;align-items:center;min-width:max-content;padding:16px;gap:16px}.npv-page-wrapper{position:relative;display:inline-block;box-shadow:0 4px 12px #00000080;line-height:0}.npv-canvas{display:block}.npv-overlay{position:absolute;top:0;left:0;pointer-events:all;overflow:visible;cursor:crosshair;user-select:none;-webkit-user-select:none}.npv-overlay.npv-overlay--text-mode{pointer-events:none;cursor:text}:host ::ng-deep .npv-text-layer{position:absolute;top:0;left:0;overflow:hidden;line-height:1;-webkit-text-size-adjust:none;text-size-adjust:none;forced-color-adjust:none}:host ::ng-deep .npv-text-layer span,:host ::ng-deep .npv-text-layer br{color:transparent;position:absolute;white-space:pre;cursor:text;transform-origin:0% 0%}:host ::ng-deep .npv-text-layer ::selection{background:#0078d740;color:transparent}.npv-box{pointer-events:all;stroke-width:2;fill:transparent;cursor:pointer;transition:fill .15s ease,stroke-width .15s ease}.npv-box:hover{stroke-width:3;filter:drop-shadow(0 0 3px rgba(233,115,50,.7))}.npv-box.npv-box--clicked{stroke-width:3}.npv-box.npv-box--multi{stroke-width:2.5}.npv-search-highlight{fill:#ffeb3b59;stroke:#ffc800cc;stroke-width:1.5;pointer-events:none}.npv-search-highlight.npv-search-highlight--current{fill:#ff980073;stroke:#ff6400e6;stroke-width:2}.npv-drag-rect{fill:#0078d71a;stroke:#0078d7b3;stroke-width:1.5;stroke-dasharray:5 3;pointer-events:none}.npv-placeholder{color:#aaa;font-size:14px;padding:40px;text-align:center}.npv-placeholder.npv-placeholder--error{color:#e53935}.npv-context-menu{position:fixed;z-index:9999;background:#fff;border:1px solid rgba(0,0,0,.12);border-radius:4px;box-shadow:0 4px 16px #0003;padding:4px 0;min-width:160px}.npv-context-item{padding:10px 16px;font-size:14px;cursor:pointer;color:#000000de;-webkit-user-select:none;user-select:none}.npv-context-item:hover{background:#0000000d}\n"] }]
|
|
702
|
+
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: i0.ElementRef }, { type: undefined, decorators: [{
|
|
703
|
+
type: Inject,
|
|
704
|
+
args: [NGX_PDF_WORKER_SRC]
|
|
705
|
+
}] }], propDecorators: { blob: [{
|
|
706
|
+
type: Input
|
|
707
|
+
}], annotations: [{
|
|
708
|
+
type: Input
|
|
709
|
+
}], contextMenuItems: [{
|
|
710
|
+
type: Input
|
|
711
|
+
}], highlightingColor: [{
|
|
712
|
+
type: Input
|
|
713
|
+
}], selectedColor: [{
|
|
714
|
+
type: Input
|
|
715
|
+
}], annotationStroke: [{
|
|
716
|
+
type: Input
|
|
717
|
+
}], initialZoom: [{
|
|
718
|
+
type: Input
|
|
719
|
+
}], labels: [{
|
|
720
|
+
type: Input
|
|
721
|
+
}], boxClicked: [{
|
|
722
|
+
type: Output
|
|
723
|
+
}], boxesSelected: [{
|
|
724
|
+
type: Output
|
|
725
|
+
}], pdfContainer: [{
|
|
726
|
+
type: ViewChild,
|
|
727
|
+
args: ['pdfContainer']
|
|
728
|
+
}], onDocMouseMove: [{
|
|
729
|
+
type: HostListener,
|
|
730
|
+
args: ['mousemove', ['$event']]
|
|
731
|
+
}], onDocMouseUp: [{
|
|
732
|
+
type: HostListener,
|
|
733
|
+
args: ['mouseup', ['$event']]
|
|
734
|
+
}], onDocClick: [{
|
|
735
|
+
type: HostListener,
|
|
736
|
+
args: ['document:click']
|
|
737
|
+
}] } });
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Generated bundle index. Do not edit.
|
|
741
|
+
*/
|
|
742
|
+
|
|
743
|
+
export { NGX_PDF_WORKER_SRC, NgxNextPdfViewerComponent };
|
|
744
|
+
//# sourceMappingURL=aprova.ch-ngx-next-pdf-viewer.mjs.map
|