arilekh-report-viewer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,1087 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { ViewChild, Input, ChangeDetectionStrategy, Component, Injectable, EventEmitter, Output, ViewChildren } from '@angular/core';
|
|
3
|
+
import * as i3 from '@angular/common';
|
|
4
|
+
import { CommonModule } from '@angular/common';
|
|
5
|
+
import * as i2 from '@angular/forms';
|
|
6
|
+
import { FormsModule } from '@angular/forms';
|
|
7
|
+
import { BehaviorSubject, Subject, Observable } from 'rxjs';
|
|
8
|
+
import { tap, takeUntil, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
|
9
|
+
import * as i1 from '@angular/common/http';
|
|
10
|
+
import { HttpParams } from '@angular/common/http';
|
|
11
|
+
|
|
12
|
+
const PT_TO_PX$1 = 1.333; // 1pt = 1.333px at 96dpi
|
|
13
|
+
class PageRendererComponent {
|
|
14
|
+
page;
|
|
15
|
+
scale = PT_TO_PX$1;
|
|
16
|
+
pageEl;
|
|
17
|
+
Math = Math;
|
|
18
|
+
get widthPx() { return (this.page?.widthPt ?? 595) * this.scale; }
|
|
19
|
+
get heightPx() { return (this.page?.heightPt ?? 842) * this.scale; }
|
|
20
|
+
ngOnChanges(c) { }
|
|
21
|
+
// ── Style builders ────────────────────────────────────────────────────────
|
|
22
|
+
textStyle(el) {
|
|
23
|
+
const s = el.style;
|
|
24
|
+
const rot = el.rotation ? `rotate(${el.rotation}deg)` : '';
|
|
25
|
+
const vAlign = el.verticalAlign === 'middle' ? 'center'
|
|
26
|
+
: el.verticalAlign === 'bottom' ? 'flex-end'
|
|
27
|
+
: 'flex-start';
|
|
28
|
+
return {
|
|
29
|
+
position: 'absolute',
|
|
30
|
+
left: `${el.x * this.scale}px`,
|
|
31
|
+
top: `${el.y * this.scale}px`,
|
|
32
|
+
width: `${el.width * this.scale}px`,
|
|
33
|
+
height: `${el.height * this.scale}px`,
|
|
34
|
+
overflow: 'hidden',
|
|
35
|
+
fontSize: `${(s?.fontSize ?? 9) * this.scale}px`,
|
|
36
|
+
fontFamily: s?.fontFamily ?? 'inherit',
|
|
37
|
+
fontWeight: s?.bold ? 'bold' : 'normal',
|
|
38
|
+
fontStyle: s?.italic ? 'italic' : 'normal',
|
|
39
|
+
textDecoration: s?.underline ? 'underline' : 'none',
|
|
40
|
+
color: s?.foreColor ?? '#000',
|
|
41
|
+
background: s?.backColor ?? 'transparent',
|
|
42
|
+
textAlign: el.alignment ?? 'left',
|
|
43
|
+
display: 'flex',
|
|
44
|
+
flexDirection: 'column',
|
|
45
|
+
justifyContent: vAlign,
|
|
46
|
+
paddingLeft: `${(s?.paddingLeft ?? 0) * this.scale}px`,
|
|
47
|
+
paddingRight: `${(s?.paddingRight ?? 0) * this.scale}px`,
|
|
48
|
+
transform: rot,
|
|
49
|
+
transformOrigin: rot ? 'center center' : '',
|
|
50
|
+
boxSizing: 'border-box',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
rectStyle(el) {
|
|
54
|
+
const rot = el.rotation ? `rotate(${el.rotation}deg)` : '';
|
|
55
|
+
return {
|
|
56
|
+
position: 'absolute',
|
|
57
|
+
left: `${el.x * this.scale}px`,
|
|
58
|
+
top: `${el.y * this.scale}px`,
|
|
59
|
+
width: `${el.width * this.scale}px`,
|
|
60
|
+
height: `${el.height * this.scale}px`,
|
|
61
|
+
background: el.fillColor ?? 'transparent',
|
|
62
|
+
border: el.strokeColor
|
|
63
|
+
? `${(el.strokeWidth ?? 1) * this.scale}px solid ${el.strokeColor}`
|
|
64
|
+
: 'none',
|
|
65
|
+
borderRadius: el.borderRadius ? `${el.borderRadius}%` : '0',
|
|
66
|
+
transform: rot,
|
|
67
|
+
transformOrigin: rot ? 'center center' : '',
|
|
68
|
+
boxSizing: 'border-box',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
lineSvgStyle(el) {
|
|
72
|
+
const x1 = el.x, y1 = el.y;
|
|
73
|
+
const x2 = el.x2 ?? el.x, y2 = el.y2 ?? el.y;
|
|
74
|
+
const left = Math.min(x1, x2) * this.scale;
|
|
75
|
+
const top = Math.min(y1, y2) * this.scale;
|
|
76
|
+
const width = Math.max(Math.abs(x2 - x1) * this.scale, (el.strokeWidth ?? 1) * this.scale + 2);
|
|
77
|
+
const height = Math.max(Math.abs(y2 - y1) * this.scale, (el.strokeWidth ?? 1) * this.scale + 2);
|
|
78
|
+
const rot = el.rotation ? `rotate(${el.rotation}deg)` : '';
|
|
79
|
+
return {
|
|
80
|
+
position: 'absolute',
|
|
81
|
+
left: `${left}px`, top: `${top}px`,
|
|
82
|
+
width: `${width}px`, height: `${height}px`,
|
|
83
|
+
transform: rot, transformOrigin: rot ? 'center center' : '',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
lineX1(el) {
|
|
87
|
+
return (el.x * this.scale) - Math.min(el.x, el.x2 ?? el.x) * this.scale;
|
|
88
|
+
}
|
|
89
|
+
lineY1(el) {
|
|
90
|
+
return (el.y * this.scale) - Math.min(el.y, el.y2 ?? el.y) * this.scale;
|
|
91
|
+
}
|
|
92
|
+
lineX2(el) {
|
|
93
|
+
return ((el.x2 ?? el.x) * this.scale) - Math.min(el.x, el.x2 ?? el.x) * this.scale;
|
|
94
|
+
}
|
|
95
|
+
lineY2(el) {
|
|
96
|
+
return ((el.y2 ?? el.y) * this.scale) - Math.min(el.y, el.y2 ?? el.y) * this.scale;
|
|
97
|
+
}
|
|
98
|
+
ellipseSvgStyle(el) {
|
|
99
|
+
const rot = el.rotation ? `rotate(${el.rotation}deg)` : '';
|
|
100
|
+
return {
|
|
101
|
+
position: 'absolute',
|
|
102
|
+
left: `${el.x * this.scale}px`, top: `${el.y * this.scale}px`,
|
|
103
|
+
width: `${el.width * this.scale}px`, height: `${el.height * this.scale}px`,
|
|
104
|
+
transform: rot, transformOrigin: rot ? 'center center' : '',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
imageStyle(el) {
|
|
108
|
+
const rot = el.rotation ? `rotate(${el.rotation}deg)` : '';
|
|
109
|
+
return {
|
|
110
|
+
position: 'absolute',
|
|
111
|
+
left: `${el.x * this.scale}px`,
|
|
112
|
+
top: `${el.y * this.scale}px`,
|
|
113
|
+
width: `${el.width * this.scale}px`,
|
|
114
|
+
height: `${el.height * this.scale}px`,
|
|
115
|
+
objectFit: el.stretch ?? 'contain',
|
|
116
|
+
transform: rot, transformOrigin: rot ? 'center center' : '',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
openLink(url) {
|
|
120
|
+
window.open(url, '_blank', 'noopener');
|
|
121
|
+
}
|
|
122
|
+
/** Expose the page div for screenshot (called by parent). */
|
|
123
|
+
getPageElement() {
|
|
124
|
+
return this.pageEl?.nativeElement;
|
|
125
|
+
}
|
|
126
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: PageRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
127
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: PageRendererComponent, isStandalone: true, selector: "arv-page-renderer", inputs: { page: "page", scale: "scale" }, viewQueries: [{ propertyName: "pageEl", first: true, predicate: ["pageEl"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
|
|
128
|
+
<div class="arv-page"
|
|
129
|
+
#pageEl
|
|
130
|
+
[style.width.px]="widthPx"
|
|
131
|
+
[style.height.px]="heightPx"
|
|
132
|
+
[attr.data-page]="page?.pageNumber">
|
|
133
|
+
|
|
134
|
+
@if (page) {
|
|
135
|
+
@for (el of page.elements; track $index) {
|
|
136
|
+
<!-- TEXT -->
|
|
137
|
+
@if (el.type === 'text') {
|
|
138
|
+
<div [style]="textStyle(el)"
|
|
139
|
+
[class.arv-hyperlink]="!!el.hyperlinkUrl"
|
|
140
|
+
(click)="el.hyperlinkUrl && openLink(el.hyperlinkUrl)">
|
|
141
|
+
{{ el.text }}
|
|
142
|
+
</div>
|
|
143
|
+
}
|
|
144
|
+
<!-- RECT -->
|
|
145
|
+
@else if (el.type === 'rect') {
|
|
146
|
+
<div [style]="rectStyle(el)"></div>
|
|
147
|
+
}
|
|
148
|
+
<!-- LINE -->
|
|
149
|
+
@else if (el.type === 'line') {
|
|
150
|
+
<svg [style]="lineSvgStyle(el)" xmlns="http://www.w3.org/2000/svg">
|
|
151
|
+
<line
|
|
152
|
+
[attr.x1]="lineX1(el)" [attr.y1]="lineY1(el)"
|
|
153
|
+
[attr.x2]="lineX2(el)" [attr.y2]="lineY2(el)"
|
|
154
|
+
[attr.stroke]="el.strokeColor || '#000'"
|
|
155
|
+
[attr.stroke-width]="(el.strokeWidth || 1) * scale" />
|
|
156
|
+
</svg>
|
|
157
|
+
}
|
|
158
|
+
<!-- ELLIPSE -->
|
|
159
|
+
@else if (el.type === 'ellipse') {
|
|
160
|
+
<svg [style]="ellipseSvgStyle(el)" xmlns="http://www.w3.org/2000/svg">
|
|
161
|
+
<ellipse
|
|
162
|
+
[attr.cx]="(el.width * scale / 2)"
|
|
163
|
+
[attr.cy]="(el.height * scale / 2)"
|
|
164
|
+
[attr.rx]="Math.max(0, el.width * scale / 2 - (el.strokeWidth||1)*scale/2)"
|
|
165
|
+
[attr.ry]="Math.max(0, el.height * scale / 2 - (el.strokeWidth||1)*scale/2)"
|
|
166
|
+
[attr.fill]="el.fillColor || 'none'"
|
|
167
|
+
[attr.stroke]="el.strokeColor || '#000'"
|
|
168
|
+
[attr.stroke-width]="(el.strokeWidth || 1) * scale" />
|
|
169
|
+
</svg>
|
|
170
|
+
}
|
|
171
|
+
<!-- IMAGE -->
|
|
172
|
+
@else if (el.type === 'image') {
|
|
173
|
+
<img [style]="imageStyle(el)"
|
|
174
|
+
[src]="el.src"
|
|
175
|
+
alt="" />
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
</div>
|
|
180
|
+
`, isInline: true, styles: [":host{display:block}.arv-page{position:relative;background:#fff;overflow:hidden;box-shadow:0 2px 12px #0000004d}.arv-hyperlink{cursor:pointer}.arv-hyperlink:hover{opacity:.8}svg{overflow:visible}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
181
|
+
}
|
|
182
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: PageRendererComponent, decorators: [{
|
|
183
|
+
type: Component,
|
|
184
|
+
args: [{ selector: 'arv-page-renderer', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
185
|
+
<div class="arv-page"
|
|
186
|
+
#pageEl
|
|
187
|
+
[style.width.px]="widthPx"
|
|
188
|
+
[style.height.px]="heightPx"
|
|
189
|
+
[attr.data-page]="page?.pageNumber">
|
|
190
|
+
|
|
191
|
+
@if (page) {
|
|
192
|
+
@for (el of page.elements; track $index) {
|
|
193
|
+
<!-- TEXT -->
|
|
194
|
+
@if (el.type === 'text') {
|
|
195
|
+
<div [style]="textStyle(el)"
|
|
196
|
+
[class.arv-hyperlink]="!!el.hyperlinkUrl"
|
|
197
|
+
(click)="el.hyperlinkUrl && openLink(el.hyperlinkUrl)">
|
|
198
|
+
{{ el.text }}
|
|
199
|
+
</div>
|
|
200
|
+
}
|
|
201
|
+
<!-- RECT -->
|
|
202
|
+
@else if (el.type === 'rect') {
|
|
203
|
+
<div [style]="rectStyle(el)"></div>
|
|
204
|
+
}
|
|
205
|
+
<!-- LINE -->
|
|
206
|
+
@else if (el.type === 'line') {
|
|
207
|
+
<svg [style]="lineSvgStyle(el)" xmlns="http://www.w3.org/2000/svg">
|
|
208
|
+
<line
|
|
209
|
+
[attr.x1]="lineX1(el)" [attr.y1]="lineY1(el)"
|
|
210
|
+
[attr.x2]="lineX2(el)" [attr.y2]="lineY2(el)"
|
|
211
|
+
[attr.stroke]="el.strokeColor || '#000'"
|
|
212
|
+
[attr.stroke-width]="(el.strokeWidth || 1) * scale" />
|
|
213
|
+
</svg>
|
|
214
|
+
}
|
|
215
|
+
<!-- ELLIPSE -->
|
|
216
|
+
@else if (el.type === 'ellipse') {
|
|
217
|
+
<svg [style]="ellipseSvgStyle(el)" xmlns="http://www.w3.org/2000/svg">
|
|
218
|
+
<ellipse
|
|
219
|
+
[attr.cx]="(el.width * scale / 2)"
|
|
220
|
+
[attr.cy]="(el.height * scale / 2)"
|
|
221
|
+
[attr.rx]="Math.max(0, el.width * scale / 2 - (el.strokeWidth||1)*scale/2)"
|
|
222
|
+
[attr.ry]="Math.max(0, el.height * scale / 2 - (el.strokeWidth||1)*scale/2)"
|
|
223
|
+
[attr.fill]="el.fillColor || 'none'"
|
|
224
|
+
[attr.stroke]="el.strokeColor || '#000'"
|
|
225
|
+
[attr.stroke-width]="(el.strokeWidth || 1) * scale" />
|
|
226
|
+
</svg>
|
|
227
|
+
}
|
|
228
|
+
<!-- IMAGE -->
|
|
229
|
+
@else if (el.type === 'image') {
|
|
230
|
+
<img [style]="imageStyle(el)"
|
|
231
|
+
[src]="el.src"
|
|
232
|
+
alt="" />
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
</div>
|
|
237
|
+
`, styles: [":host{display:block}.arv-page{position:relative;background:#fff;overflow:hidden;box-shadow:0 2px 12px #0000004d}.arv-hyperlink{cursor:pointer}.arv-hyperlink:hover{opacity:.8}svg{overflow:visible}\n"] }]
|
|
238
|
+
}], propDecorators: { page: [{
|
|
239
|
+
type: Input
|
|
240
|
+
}], scale: [{
|
|
241
|
+
type: Input
|
|
242
|
+
}], pageEl: [{
|
|
243
|
+
type: ViewChild,
|
|
244
|
+
args: ['pageEl']
|
|
245
|
+
}] } });
|
|
246
|
+
|
|
247
|
+
// ── Service ────────────────────────────────────────────────────────────────
|
|
248
|
+
class ReportViewerApiService {
|
|
249
|
+
http;
|
|
250
|
+
baseUrl = '';
|
|
251
|
+
// ── Observable state ───────────────────────────────────────────────────────
|
|
252
|
+
_session = new BehaviorSubject(null);
|
|
253
|
+
_loading = new BehaviorSubject(false);
|
|
254
|
+
_error = new BehaviorSubject(null);
|
|
255
|
+
_pageCache = new Map();
|
|
256
|
+
_navigateTo$ = new Subject();
|
|
257
|
+
session$ = this._session.asObservable();
|
|
258
|
+
loading$ = this._loading.asObservable();
|
|
259
|
+
error$ = this._error.asObservable();
|
|
260
|
+
navigateTo$ = this._navigateTo$.asObservable();
|
|
261
|
+
// ── Configuration ──────────────────────────────────────────────────────────
|
|
262
|
+
configure(apiBaseUrl) {
|
|
263
|
+
this.baseUrl = apiBaseUrl.replace(/\/$/, '');
|
|
264
|
+
}
|
|
265
|
+
get sessionId() {
|
|
266
|
+
return this._session.value?.sessionId ?? null;
|
|
267
|
+
}
|
|
268
|
+
get pageCount() {
|
|
269
|
+
return this._session.value?.pageCount ?? 0;
|
|
270
|
+
}
|
|
271
|
+
get pageWidthPt() {
|
|
272
|
+
return this._session.value?.pageWidthPt ?? 595;
|
|
273
|
+
}
|
|
274
|
+
get pageHeightPt() {
|
|
275
|
+
return this._session.value?.pageHeightPt ?? 842;
|
|
276
|
+
}
|
|
277
|
+
// ── Render ────────────────────────────────────────────────────────────────
|
|
278
|
+
constructor(http) {
|
|
279
|
+
this.http = http;
|
|
280
|
+
}
|
|
281
|
+
render(req) {
|
|
282
|
+
this._loading.next(true);
|
|
283
|
+
this._error.next(null);
|
|
284
|
+
this._pageCache.clear();
|
|
285
|
+
this._session.next(null);
|
|
286
|
+
return this.http
|
|
287
|
+
.post(`${this.baseUrl}/api/reports/render`, req)
|
|
288
|
+
.pipe(tap({
|
|
289
|
+
next: r => { this._session.next(r); this._loading.next(false); },
|
|
290
|
+
error: e => {
|
|
291
|
+
this._error.next(e?.error?.error ?? 'Render failed');
|
|
292
|
+
this._loading.next(false);
|
|
293
|
+
}
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
// ── Page retrieval (cached) ────────────────────────────────────────────────
|
|
297
|
+
getPage(pageNumber) {
|
|
298
|
+
const id = this.sessionId;
|
|
299
|
+
if (!id)
|
|
300
|
+
throw new Error('No active session.');
|
|
301
|
+
// Check cache first
|
|
302
|
+
const cached = this._pageCache.get(pageNumber);
|
|
303
|
+
if (cached) {
|
|
304
|
+
return new Observable(o => { o.next(cached); o.complete(); });
|
|
305
|
+
}
|
|
306
|
+
return this.http
|
|
307
|
+
.get(`${this.baseUrl}/api/reports/${id}/pages/${pageNumber}`)
|
|
308
|
+
.pipe(tap(p => this._pageCache.set(pageNumber, p)));
|
|
309
|
+
}
|
|
310
|
+
getPageRange(from, to) {
|
|
311
|
+
const id = this.sessionId;
|
|
312
|
+
if (!id)
|
|
313
|
+
throw new Error('No active session.');
|
|
314
|
+
const params = new HttpParams()
|
|
315
|
+
.set('from', from.toString())
|
|
316
|
+
.set('to', to.toString());
|
|
317
|
+
return this.http.get(`${this.baseUrl}/api/reports/${id}/pages`, { params });
|
|
318
|
+
}
|
|
319
|
+
// ── Thumbnails ────────────────────────────────────────────────────────────
|
|
320
|
+
getThumbnails() {
|
|
321
|
+
const id = this.sessionId;
|
|
322
|
+
return this.http.get(`${this.baseUrl}/api/reports/${id}/thumbnails`);
|
|
323
|
+
}
|
|
324
|
+
// ── Search ────────────────────────────────────────────────────────────────
|
|
325
|
+
search(query) {
|
|
326
|
+
const id = this.sessionId;
|
|
327
|
+
const params = new HttpParams().set('q', query);
|
|
328
|
+
return this.http.get(`${this.baseUrl}/api/reports/${id}/search`, { params });
|
|
329
|
+
}
|
|
330
|
+
// ── Navigation helper ─────────────────────────────────────────────────────
|
|
331
|
+
navigateTo(page) {
|
|
332
|
+
this._navigateTo$.next(Math.max(1, Math.min(page, this.pageCount)));
|
|
333
|
+
}
|
|
334
|
+
// ── Cache management ──────────────────────────────────────────────────────
|
|
335
|
+
evictPage(pageNumber) {
|
|
336
|
+
this._pageCache.delete(pageNumber);
|
|
337
|
+
}
|
|
338
|
+
clearCache() {
|
|
339
|
+
this._pageCache.clear();
|
|
340
|
+
}
|
|
341
|
+
isPageCached(pageNumber) {
|
|
342
|
+
return this._pageCache.has(pageNumber);
|
|
343
|
+
}
|
|
344
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ReportViewerApiService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
345
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ReportViewerApiService, providedIn: 'root' });
|
|
346
|
+
}
|
|
347
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ReportViewerApiService, decorators: [{
|
|
348
|
+
type: Injectable,
|
|
349
|
+
args: [{ providedIn: 'root' }]
|
|
350
|
+
}], ctorParameters: () => [{ type: i1.HttpClient }] });
|
|
351
|
+
|
|
352
|
+
class LeftPanelComponent {
|
|
353
|
+
api;
|
|
354
|
+
cdr;
|
|
355
|
+
currentPage = 1;
|
|
356
|
+
pageSelected = new EventEmitter();
|
|
357
|
+
thumbnails;
|
|
358
|
+
searchQuery = '';
|
|
359
|
+
lastQuery = '';
|
|
360
|
+
searching = false;
|
|
361
|
+
searchResults = [];
|
|
362
|
+
destroy$ = new Subject();
|
|
363
|
+
search$ = new Subject();
|
|
364
|
+
constructor(api, cdr) {
|
|
365
|
+
this.api = api;
|
|
366
|
+
this.cdr = cdr;
|
|
367
|
+
}
|
|
368
|
+
ngOnInit() {
|
|
369
|
+
// Load thumbnails when session is ready
|
|
370
|
+
this.api.session$
|
|
371
|
+
.pipe(takeUntil(this.destroy$))
|
|
372
|
+
.subscribe(s => {
|
|
373
|
+
if (s)
|
|
374
|
+
this.loadThumbnails();
|
|
375
|
+
});
|
|
376
|
+
// Debounced search
|
|
377
|
+
this.search$.pipe(takeUntil(this.destroy$), debounceTime(400), distinctUntilChanged(), switchMap(q => {
|
|
378
|
+
if (!q.trim()) {
|
|
379
|
+
this.searchResults = [];
|
|
380
|
+
this.searching = false;
|
|
381
|
+
this.cdr.markForCheck();
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
this.searching = true;
|
|
385
|
+
this.cdr.markForCheck();
|
|
386
|
+
return this.api.search(q);
|
|
387
|
+
})).subscribe({
|
|
388
|
+
next: r => {
|
|
389
|
+
this.searchResults = r.results;
|
|
390
|
+
this.lastQuery = r.query;
|
|
391
|
+
this.searching = false;
|
|
392
|
+
this.cdr.markForCheck();
|
|
393
|
+
},
|
|
394
|
+
error: () => { this.searching = false; this.cdr.markForCheck(); }
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
ngOnDestroy() {
|
|
398
|
+
this.destroy$.next();
|
|
399
|
+
this.destroy$.complete();
|
|
400
|
+
}
|
|
401
|
+
loadThumbnails() {
|
|
402
|
+
this.api.getThumbnails()
|
|
403
|
+
.pipe(takeUntil(this.destroy$))
|
|
404
|
+
.subscribe(t => {
|
|
405
|
+
this.thumbnails = t;
|
|
406
|
+
this.cdr.markForCheck();
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
onSearchChange(q) {
|
|
410
|
+
this.search$.next(q);
|
|
411
|
+
}
|
|
412
|
+
goTo(page) {
|
|
413
|
+
this.pageSelected.emit(page);
|
|
414
|
+
}
|
|
415
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: LeftPanelComponent, deps: [{ token: ReportViewerApiService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
416
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: LeftPanelComponent, isStandalone: true, selector: "arv-left-panel", inputs: { currentPage: "currentPage" }, outputs: { pageSelected: "pageSelected" }, ngImport: i0, template: `
|
|
417
|
+
<div class="arv-left">
|
|
418
|
+
|
|
419
|
+
<!-- Header -->
|
|
420
|
+
<div class="arv-left__header">
|
|
421
|
+
<span class="arv-left__title">Pages</span>
|
|
422
|
+
<span class="arv-left__badge">{{ api.pageCount }}</span>
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
<!-- Search -->
|
|
426
|
+
<div class="arv-left__search">
|
|
427
|
+
<input class="arv-input"
|
|
428
|
+
placeholder="Search in report…"
|
|
429
|
+
[(ngModel)]="searchQuery"
|
|
430
|
+
(ngModelChange)="onSearchChange($event)" />
|
|
431
|
+
@if (searching) {
|
|
432
|
+
<span class="arv-spinner-sm"></span>
|
|
433
|
+
}
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
<!-- Search results -->
|
|
437
|
+
@if (searchResults.length > 0) {
|
|
438
|
+
<div class="arv-left__section-title">
|
|
439
|
+
{{ searchResults.length }} result(s) for "{{ lastQuery }}"
|
|
440
|
+
</div>
|
|
441
|
+
<div class="arv-left__list">
|
|
442
|
+
@for (hit of searchResults; track hit.pageNumber) {
|
|
443
|
+
<div class="arv-bm arv-bm--search"
|
|
444
|
+
[class.arv-bm--active]="currentPage === hit.pageNumber"
|
|
445
|
+
(click)="goTo(hit.pageNumber)">
|
|
446
|
+
<span class="arv-bm__icon">🔍</span>
|
|
447
|
+
<div class="arv-bm__col">
|
|
448
|
+
<span class="arv-bm__label">Page {{ hit.pageNumber }}</span>
|
|
449
|
+
<span class="arv-bm__snippet">{{ hit.snippet }}</span>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
}
|
|
453
|
+
</div>
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
<!-- Page thumbnails -->
|
|
457
|
+
@if (searchResults.length === 0) {
|
|
458
|
+
<div class="arv-left__section-title">All Pages</div>
|
|
459
|
+
<div class="arv-left__list">
|
|
460
|
+
@if (thumbnails) {
|
|
461
|
+
@for (pg of thumbnails.pages; track pg.pageNumber) {
|
|
462
|
+
<div class="arv-bm arv-thumb"
|
|
463
|
+
[class.arv-bm--active]="currentPage === pg.pageNumber"
|
|
464
|
+
(click)="goTo(pg.pageNumber)">
|
|
465
|
+
<!-- Mini thumbnail placeholder -->
|
|
466
|
+
<div class="arv-thumb__box">
|
|
467
|
+
<span class="arv-thumb__num">{{ pg.pageNumber }}</span>
|
|
468
|
+
</div>
|
|
469
|
+
<div class="arv-bm__col">
|
|
470
|
+
<span class="arv-bm__label">Page {{ pg.pageNumber }}</span>
|
|
471
|
+
@if (pg.firstText) {
|
|
472
|
+
<span class="arv-bm__snippet">{{ pg.firstText | slice:0:40 }}</span>
|
|
473
|
+
}
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
}
|
|
477
|
+
} @else {
|
|
478
|
+
<div class="arv-left__loading">Loading…</div>
|
|
479
|
+
}
|
|
480
|
+
</div>
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
<!-- Footer info -->
|
|
484
|
+
<div class="arv-left__footer">
|
|
485
|
+
<span>{{ api.pageCount }} pages</span>
|
|
486
|
+
@if (api.pageCount > 0) {
|
|
487
|
+
<span>· Page {{ currentPage }}</span>
|
|
488
|
+
}
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
`, isInline: true, styles: [".arv-left{width:200px;flex-shrink:0;display:flex;flex-direction:column;background:var(--arv-surface, #2a2a3e);border-right:1px solid var(--arv-border, #3d3d5c);overflow:hidden;height:100%}.arv-left__header{display:flex;align-items:center;justify-content:space-between;padding:8px 10px 4px;font-weight:700;font-size:11px;color:var(--arv-text-muted, #888);text-transform:uppercase;flex-shrink:0}.arv-left__badge{font-size:9px;background:var(--arv-border, #3d3d5c);border-radius:8px;padding:1px 6px;color:var(--arv-text, #ccc)}.arv-left__search{display:flex;align-items:center;gap:4px;padding:4px 8px;flex-shrink:0}.arv-input{flex:1;padding:4px 8px;background:var(--arv-surface2, #313148);border:1px solid var(--arv-border, #3d3d5c);border-radius:4px;color:var(--arv-text, #ccc);font-size:11px;outline:none;width:100%;box-sizing:border-box}.arv-left__section-title{font-size:9px;color:var(--arv-text-muted, #888);text-transform:uppercase;padding:4px 10px 2px;flex-shrink:0}.arv-left__list{flex:1;overflow-y:auto;min-height:0}.arv-left__loading{padding:12px;font-size:11px;color:var(--arv-text-muted, #888)}.arv-bm{display:flex;align-items:flex-start;gap:6px;padding:5px 10px;cursor:pointer;font-size:11px;border-radius:2px;margin:1px 4px}.arv-bm:hover{background:var(--arv-surface2, #313148)}.arv-bm--active{background:#5b8ef026;color:var(--arv-accent, #5b8ef0)}.arv-bm__col{display:flex;flex-direction:column;gap:2px;overflow:hidden}.arv-bm__label{font-size:11px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.arv-bm__snippet{font-size:9px;color:var(--arv-text-muted, #888);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.arv-bm__icon{flex-shrink:0;font-size:12px}.arv-thumb__box{width:30px;height:40px;background:#fff;border:1px solid var(--arv-border, #3d3d5c);border-radius:2px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.arv-thumb__num{font-size:8px;color:#999}.arv-left__footer{padding:6px 10px;font-size:10px;color:var(--arv-text-muted, #888);border-top:1px solid var(--arv-border, #3d3d5c);flex-shrink:0;display:flex;gap:4px}.arv-spinner-sm{width:14px;height:14px;border:2px solid var(--arv-border, #3d3d5c);border-top-color:var(--arv-accent, #5b8ef0);border-radius:50%;animation:arv-spin .7s linear infinite;flex-shrink:0}@keyframes arv-spin{to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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: "pipe", type: i3.SlicePipe, name: "slice" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
492
|
+
}
|
|
493
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: LeftPanelComponent, decorators: [{
|
|
494
|
+
type: Component,
|
|
495
|
+
args: [{ selector: 'arv-left-panel', standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
496
|
+
<div class="arv-left">
|
|
497
|
+
|
|
498
|
+
<!-- Header -->
|
|
499
|
+
<div class="arv-left__header">
|
|
500
|
+
<span class="arv-left__title">Pages</span>
|
|
501
|
+
<span class="arv-left__badge">{{ api.pageCount }}</span>
|
|
502
|
+
</div>
|
|
503
|
+
|
|
504
|
+
<!-- Search -->
|
|
505
|
+
<div class="arv-left__search">
|
|
506
|
+
<input class="arv-input"
|
|
507
|
+
placeholder="Search in report…"
|
|
508
|
+
[(ngModel)]="searchQuery"
|
|
509
|
+
(ngModelChange)="onSearchChange($event)" />
|
|
510
|
+
@if (searching) {
|
|
511
|
+
<span class="arv-spinner-sm"></span>
|
|
512
|
+
}
|
|
513
|
+
</div>
|
|
514
|
+
|
|
515
|
+
<!-- Search results -->
|
|
516
|
+
@if (searchResults.length > 0) {
|
|
517
|
+
<div class="arv-left__section-title">
|
|
518
|
+
{{ searchResults.length }} result(s) for "{{ lastQuery }}"
|
|
519
|
+
</div>
|
|
520
|
+
<div class="arv-left__list">
|
|
521
|
+
@for (hit of searchResults; track hit.pageNumber) {
|
|
522
|
+
<div class="arv-bm arv-bm--search"
|
|
523
|
+
[class.arv-bm--active]="currentPage === hit.pageNumber"
|
|
524
|
+
(click)="goTo(hit.pageNumber)">
|
|
525
|
+
<span class="arv-bm__icon">🔍</span>
|
|
526
|
+
<div class="arv-bm__col">
|
|
527
|
+
<span class="arv-bm__label">Page {{ hit.pageNumber }}</span>
|
|
528
|
+
<span class="arv-bm__snippet">{{ hit.snippet }}</span>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
}
|
|
532
|
+
</div>
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
<!-- Page thumbnails -->
|
|
536
|
+
@if (searchResults.length === 0) {
|
|
537
|
+
<div class="arv-left__section-title">All Pages</div>
|
|
538
|
+
<div class="arv-left__list">
|
|
539
|
+
@if (thumbnails) {
|
|
540
|
+
@for (pg of thumbnails.pages; track pg.pageNumber) {
|
|
541
|
+
<div class="arv-bm arv-thumb"
|
|
542
|
+
[class.arv-bm--active]="currentPage === pg.pageNumber"
|
|
543
|
+
(click)="goTo(pg.pageNumber)">
|
|
544
|
+
<!-- Mini thumbnail placeholder -->
|
|
545
|
+
<div class="arv-thumb__box">
|
|
546
|
+
<span class="arv-thumb__num">{{ pg.pageNumber }}</span>
|
|
547
|
+
</div>
|
|
548
|
+
<div class="arv-bm__col">
|
|
549
|
+
<span class="arv-bm__label">Page {{ pg.pageNumber }}</span>
|
|
550
|
+
@if (pg.firstText) {
|
|
551
|
+
<span class="arv-bm__snippet">{{ pg.firstText | slice:0:40 }}</span>
|
|
552
|
+
}
|
|
553
|
+
</div>
|
|
554
|
+
</div>
|
|
555
|
+
}
|
|
556
|
+
} @else {
|
|
557
|
+
<div class="arv-left__loading">Loading…</div>
|
|
558
|
+
}
|
|
559
|
+
</div>
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
<!-- Footer info -->
|
|
563
|
+
<div class="arv-left__footer">
|
|
564
|
+
<span>{{ api.pageCount }} pages</span>
|
|
565
|
+
@if (api.pageCount > 0) {
|
|
566
|
+
<span>· Page {{ currentPage }}</span>
|
|
567
|
+
}
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
`, styles: [".arv-left{width:200px;flex-shrink:0;display:flex;flex-direction:column;background:var(--arv-surface, #2a2a3e);border-right:1px solid var(--arv-border, #3d3d5c);overflow:hidden;height:100%}.arv-left__header{display:flex;align-items:center;justify-content:space-between;padding:8px 10px 4px;font-weight:700;font-size:11px;color:var(--arv-text-muted, #888);text-transform:uppercase;flex-shrink:0}.arv-left__badge{font-size:9px;background:var(--arv-border, #3d3d5c);border-radius:8px;padding:1px 6px;color:var(--arv-text, #ccc)}.arv-left__search{display:flex;align-items:center;gap:4px;padding:4px 8px;flex-shrink:0}.arv-input{flex:1;padding:4px 8px;background:var(--arv-surface2, #313148);border:1px solid var(--arv-border, #3d3d5c);border-radius:4px;color:var(--arv-text, #ccc);font-size:11px;outline:none;width:100%;box-sizing:border-box}.arv-left__section-title{font-size:9px;color:var(--arv-text-muted, #888);text-transform:uppercase;padding:4px 10px 2px;flex-shrink:0}.arv-left__list{flex:1;overflow-y:auto;min-height:0}.arv-left__loading{padding:12px;font-size:11px;color:var(--arv-text-muted, #888)}.arv-bm{display:flex;align-items:flex-start;gap:6px;padding:5px 10px;cursor:pointer;font-size:11px;border-radius:2px;margin:1px 4px}.arv-bm:hover{background:var(--arv-surface2, #313148)}.arv-bm--active{background:#5b8ef026;color:var(--arv-accent, #5b8ef0)}.arv-bm__col{display:flex;flex-direction:column;gap:2px;overflow:hidden}.arv-bm__label{font-size:11px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.arv-bm__snippet{font-size:9px;color:var(--arv-text-muted, #888);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.arv-bm__icon{flex-shrink:0;font-size:12px}.arv-thumb__box{width:30px;height:40px;background:#fff;border:1px solid var(--arv-border, #3d3d5c);border-radius:2px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.arv-thumb__num{font-size:8px;color:#999}.arv-left__footer{padding:6px 10px;font-size:10px;color:var(--arv-text-muted, #888);border-top:1px solid var(--arv-border, #3d3d5c);flex-shrink:0;display:flex;gap:4px}.arv-spinner-sm{width:14px;height:14px;border:2px solid var(--arv-border, #3d3d5c);border-top-color:var(--arv-accent, #5b8ef0);border-radius:50%;animation:arv-spin .7s linear infinite;flex-shrink:0}@keyframes arv-spin{to{transform:rotate(360deg)}}\n"] }]
|
|
571
|
+
}], ctorParameters: () => [{ type: ReportViewerApiService }, { type: i0.ChangeDetectorRef }], propDecorators: { currentPage: [{
|
|
572
|
+
type: Input
|
|
573
|
+
}], pageSelected: [{
|
|
574
|
+
type: Output
|
|
575
|
+
}] } });
|
|
576
|
+
|
|
577
|
+
const PT_TO_PX = 1.333;
|
|
578
|
+
const WINDOW_RADIUS = 5; // render ±5 pages around current
|
|
579
|
+
class ReportViewerComponent {
|
|
580
|
+
api;
|
|
581
|
+
cdr;
|
|
582
|
+
/** API base URL, e.g. 'http://localhost:5000' */
|
|
583
|
+
set apiBaseUrl(url) {
|
|
584
|
+
if (url)
|
|
585
|
+
this.api.configure(url);
|
|
586
|
+
}
|
|
587
|
+
/** Light or dark theme */
|
|
588
|
+
theme = 'dark';
|
|
589
|
+
/** If provided, render this report on init */
|
|
590
|
+
renderRequest;
|
|
591
|
+
scrollEl;
|
|
592
|
+
pageRenderers;
|
|
593
|
+
currentPage = 1;
|
|
594
|
+
scale = PT_TO_PX;
|
|
595
|
+
pageCache = new Map();
|
|
596
|
+
destroy$ = new Subject();
|
|
597
|
+
printPage; // undefined = all, number = single page
|
|
598
|
+
constructor(api, cdr) {
|
|
599
|
+
this.api = api;
|
|
600
|
+
this.cdr = cdr;
|
|
601
|
+
}
|
|
602
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
603
|
+
ngOnInit() {
|
|
604
|
+
// Subscribe to external navigation commands
|
|
605
|
+
this.api.navigateTo$
|
|
606
|
+
.pipe(takeUntil(this.destroy$))
|
|
607
|
+
.subscribe(p => this.navigateTo(p));
|
|
608
|
+
// Auto-render if input provided
|
|
609
|
+
if (this.renderRequest) {
|
|
610
|
+
this.api.render(this.renderRequest)
|
|
611
|
+
.pipe(takeUntil(this.destroy$))
|
|
612
|
+
.subscribe(() => { this.currentPage = 1; this.cdr.markForCheck(); });
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
ngAfterViewInit() { }
|
|
616
|
+
ngOnDestroy() {
|
|
617
|
+
this.destroy$.next();
|
|
618
|
+
this.destroy$.complete();
|
|
619
|
+
}
|
|
620
|
+
// ── Page numbers ───────────────────────────────────────────────────────────
|
|
621
|
+
get pageNumbers() {
|
|
622
|
+
const count = this.api.pageCount;
|
|
623
|
+
return count > 0 ? Array.from({ length: count }, (_, i) => i + 1) : [];
|
|
624
|
+
}
|
|
625
|
+
get pageWidthPx() { return this.api.pageWidthPt * this.scale; }
|
|
626
|
+
get pageHeightPx() { return this.api.pageHeightPt * this.scale; }
|
|
627
|
+
get pageWrapHeight() { return this.pageHeightPx + 32; }
|
|
628
|
+
get pageListWidth() { return this.pageWidthPx + 40; }
|
|
629
|
+
get zoomPercent() { return Math.round(this.scale / PT_TO_PX * 100); }
|
|
630
|
+
isInWindow(p) {
|
|
631
|
+
return Math.abs(p - this.currentPage) <= WINDOW_RADIUS;
|
|
632
|
+
}
|
|
633
|
+
getPage(p) {
|
|
634
|
+
if (!this.pageCache.has(p)) {
|
|
635
|
+
// Trigger async load
|
|
636
|
+
this.loadPage(p);
|
|
637
|
+
return undefined;
|
|
638
|
+
}
|
|
639
|
+
return this.pageCache.get(p);
|
|
640
|
+
}
|
|
641
|
+
loadPage(p) {
|
|
642
|
+
if (this.pageCache.has(p))
|
|
643
|
+
return;
|
|
644
|
+
this.api.getPage(p)
|
|
645
|
+
.pipe(takeUntil(this.destroy$))
|
|
646
|
+
.subscribe(page => {
|
|
647
|
+
this.pageCache.set(p, page);
|
|
648
|
+
this.cdr.markForCheck();
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
// Preload window pages whenever currentPage changes
|
|
652
|
+
preloadWindow() {
|
|
653
|
+
const from = Math.max(1, this.currentPage - WINDOW_RADIUS);
|
|
654
|
+
const to = Math.min(this.api.pageCount, this.currentPage + WINDOW_RADIUS);
|
|
655
|
+
for (let p = from; p <= to; p++)
|
|
656
|
+
this.loadPage(p);
|
|
657
|
+
}
|
|
658
|
+
// ── Navigation ─────────────────────────────────────────────────────────────
|
|
659
|
+
navigateTo(page) {
|
|
660
|
+
this.currentPage = Math.max(1, Math.min(page, this.api.pageCount));
|
|
661
|
+
this.preloadWindow();
|
|
662
|
+
this.cdr.markForCheck();
|
|
663
|
+
setTimeout(() => this.scrollToPage(this.currentPage), 50);
|
|
664
|
+
}
|
|
665
|
+
onPageInputChange(e) {
|
|
666
|
+
const v = parseInt(e.target.value, 10);
|
|
667
|
+
if (!isNaN(v))
|
|
668
|
+
this.navigateTo(v);
|
|
669
|
+
}
|
|
670
|
+
scrollToPage(p) {
|
|
671
|
+
const el = document.getElementById(`arv-pw-${p}`);
|
|
672
|
+
const scroll = this.scrollEl?.nativeElement;
|
|
673
|
+
if (el && scroll) {
|
|
674
|
+
scroll.scrollTo({ top: el.offsetTop - 8, behavior: 'smooth' });
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
onScroll() {
|
|
678
|
+
const scroll = this.scrollEl?.nativeElement;
|
|
679
|
+
if (!scroll)
|
|
680
|
+
return;
|
|
681
|
+
const list = scroll.querySelector('.arv-page-list');
|
|
682
|
+
if (!list)
|
|
683
|
+
return;
|
|
684
|
+
const mid = scroll.scrollTop + scroll.clientHeight / 2;
|
|
685
|
+
let best = 1, bestDist = Infinity;
|
|
686
|
+
for (const child of Array.from(list.children)) {
|
|
687
|
+
if (!child.id?.startsWith('arv-pw-'))
|
|
688
|
+
continue;
|
|
689
|
+
const p = parseInt(child.id.replace('arv-pw-', ''), 10);
|
|
690
|
+
const dist = Math.abs(child.offsetTop + child.offsetHeight / 2 - mid);
|
|
691
|
+
if (dist < bestDist) {
|
|
692
|
+
bestDist = dist;
|
|
693
|
+
best = p;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (best !== this.currentPage) {
|
|
697
|
+
this.currentPage = best;
|
|
698
|
+
this.preloadWindow();
|
|
699
|
+
this.cdr.markForCheck();
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
// ── Zoom ───────────────────────────────────────────────────────────────────
|
|
703
|
+
zoomIn() { this.scale = Math.min(this.scale * 1.2, PT_TO_PX * 4); this.cdr.markForCheck(); }
|
|
704
|
+
zoomOut() { this.scale = Math.max(this.scale / 1.2, PT_TO_PX * 0.25); this.cdr.markForCheck(); }
|
|
705
|
+
zoomReset() { this.scale = PT_TO_PX; this.cdr.markForCheck(); }
|
|
706
|
+
// ── Refresh ────────────────────────────────────────────────────────────────
|
|
707
|
+
refresh() {
|
|
708
|
+
this.pageCache.clear();
|
|
709
|
+
this.api.clearCache();
|
|
710
|
+
this.preloadWindow();
|
|
711
|
+
this.cdr.markForCheck();
|
|
712
|
+
}
|
|
713
|
+
// ── Print ──────────────────────────────────────────────────────────────────
|
|
714
|
+
printAll() {
|
|
715
|
+
this.ensureAllPagesLoaded(() => window.print());
|
|
716
|
+
}
|
|
717
|
+
printCurrent() {
|
|
718
|
+
// Add temporary class to hide all other pages
|
|
719
|
+
const list = document.querySelector('.arv-page-list');
|
|
720
|
+
if (!list) {
|
|
721
|
+
window.print();
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
Array.from(list.querySelectorAll('.arv-page-wrap')).forEach((el) => {
|
|
725
|
+
const pageEl = el;
|
|
726
|
+
const id = pageEl.id?.replace('arv-pw-', '');
|
|
727
|
+
pageEl.style.display = id === String(this.currentPage) ? '' : 'none';
|
|
728
|
+
});
|
|
729
|
+
window.print();
|
|
730
|
+
// Restore
|
|
731
|
+
Array.from(list.querySelectorAll('.arv-page-wrap'))
|
|
732
|
+
.forEach((el) => (el.style.display = ''));
|
|
733
|
+
}
|
|
734
|
+
ensureAllPagesLoaded(callback) {
|
|
735
|
+
// Pre-load remaining pages then print
|
|
736
|
+
const missing = [];
|
|
737
|
+
for (let p = 1; p <= this.api.pageCount; p++) {
|
|
738
|
+
if (!this.pageCache.has(p))
|
|
739
|
+
missing.push(p);
|
|
740
|
+
}
|
|
741
|
+
if (missing.length === 0) {
|
|
742
|
+
callback();
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
let loaded = 0;
|
|
746
|
+
for (const p of missing) {
|
|
747
|
+
this.api.getPage(p).pipe(takeUntil(this.destroy$)).subscribe(page => {
|
|
748
|
+
this.pageCache.set(p, page);
|
|
749
|
+
loaded++;
|
|
750
|
+
this.cdr.markForCheck();
|
|
751
|
+
if (loaded === missing.length)
|
|
752
|
+
callback();
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// ── Download page as image ─────────────────────────────────────────────────
|
|
757
|
+
async downloadPageImage() {
|
|
758
|
+
// Find the current page's renderer
|
|
759
|
+
const renderer = this.pageRenderers.find(r => {
|
|
760
|
+
const el = r.getPageElement();
|
|
761
|
+
return el?.getAttribute('data-page') === String(this.currentPage);
|
|
762
|
+
});
|
|
763
|
+
const pageEl = renderer?.getPageElement();
|
|
764
|
+
if (!pageEl) {
|
|
765
|
+
alert('Page not rendered yet. Please wait.');
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
// Use html2canvas if available, otherwise canvas screenshot
|
|
770
|
+
const h2c = window.html2canvas;
|
|
771
|
+
if (h2c) {
|
|
772
|
+
const canvas = await h2c(pageEl, { scale: 2, useCORS: true });
|
|
773
|
+
this.downloadCanvas(canvas, `page-${this.currentPage}.png`);
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
// Fallback: DOM-to-canvas via browser print screenshot prompt
|
|
777
|
+
alert('html2canvas not loaded. Add <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> to your index.html for image download.');
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
catch (e) {
|
|
781
|
+
console.error('Screenshot failed', e);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
downloadCanvas(canvas, filename) {
|
|
785
|
+
const a = document.createElement('a');
|
|
786
|
+
a.href = canvas.toDataURL('image/png');
|
|
787
|
+
a.download = filename;
|
|
788
|
+
document.body.appendChild(a);
|
|
789
|
+
a.click();
|
|
790
|
+
document.body.removeChild(a);
|
|
791
|
+
}
|
|
792
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ReportViewerComponent, deps: [{ token: ReportViewerApiService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
793
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ReportViewerComponent, isStandalone: true, selector: "arv-report-viewer", inputs: { apiBaseUrl: "apiBaseUrl", theme: "theme", renderRequest: "renderRequest" }, viewQueries: [{ propertyName: "scrollEl", first: true, predicate: ["scrollEl"], descendants: true }, { propertyName: "pageRenderers", predicate: ["pageRenderers"], descendants: true }], ngImport: i0, template: `
|
|
794
|
+
<div class="arv-shell" [attr.data-theme]="theme">
|
|
795
|
+
|
|
796
|
+
<!-- ═══ TOOLBAR ════════════════════════════════════════════════ -->
|
|
797
|
+
<div class="arv-toolbar arv-no-print">
|
|
798
|
+
|
|
799
|
+
<span class="arv-toolbar__brand">📄 Report Viewer</span>
|
|
800
|
+
|
|
801
|
+
<!-- Navigation -->
|
|
802
|
+
<div class="arv-nav">
|
|
803
|
+
<button class="arv-btn arv-btn--icon" title="First" [disabled]="currentPage <= 1"
|
|
804
|
+
(click)="navigateTo(1)">⏮</button>
|
|
805
|
+
<button class="arv-btn arv-btn--icon" title="Previous" [disabled]="currentPage <= 1"
|
|
806
|
+
(click)="navigateTo(currentPage - 1)">◀</button>
|
|
807
|
+
|
|
808
|
+
<input class="arv-page-input" type="number"
|
|
809
|
+
[min]="1" [max]="api.pageCount"
|
|
810
|
+
[ngModel]="currentPage"
|
|
811
|
+
(change)="onPageInputChange($event)" />
|
|
812
|
+
<span class="arv-nav__of">/ {{ api.pageCount }}</span>
|
|
813
|
+
|
|
814
|
+
<button class="arv-btn arv-btn--icon" title="Next"
|
|
815
|
+
[disabled]="currentPage >= api.pageCount"
|
|
816
|
+
(click)="navigateTo(currentPage + 1)">▶</button>
|
|
817
|
+
<button class="arv-btn arv-btn--icon" title="Last"
|
|
818
|
+
[disabled]="currentPage >= api.pageCount"
|
|
819
|
+
(click)="navigateTo(api.pageCount)">⏭</button>
|
|
820
|
+
</div>
|
|
821
|
+
|
|
822
|
+
<!-- Zoom -->
|
|
823
|
+
<div class="arv-zoom">
|
|
824
|
+
<button class="arv-btn arv-btn--icon" title="Zoom out" (click)="zoomOut()">−</button>
|
|
825
|
+
<span class="arv-zoom__val">{{ zoomPercent }}%</span>
|
|
826
|
+
<button class="arv-btn arv-btn--icon" title="Zoom in" (click)="zoomIn()">+</button>
|
|
827
|
+
<button class="arv-btn arv-btn--sm" (click)="zoomReset()">1:1</button>
|
|
828
|
+
</div>
|
|
829
|
+
|
|
830
|
+
<!-- Refresh -->
|
|
831
|
+
<button class="arv-btn arv-btn--sm" title="Refresh" (click)="refresh()">🔄</button>
|
|
832
|
+
|
|
833
|
+
<!-- Print -->
|
|
834
|
+
<div class="arv-btn-group">
|
|
835
|
+
<button class="arv-btn arv-btn--sm" title="Print all pages" (click)="printAll()">🖨 Print All</button>
|
|
836
|
+
<button class="arv-btn arv-btn--sm" title="Print current page" (click)="printCurrent()">🖨 Page</button>
|
|
837
|
+
</div>
|
|
838
|
+
|
|
839
|
+
<!-- Download page as image -->
|
|
840
|
+
<button class="arv-btn arv-btn--sm" title="Download current page as image"
|
|
841
|
+
(click)="downloadPageImage()">⬇ Image</button>
|
|
842
|
+
|
|
843
|
+
<!-- Status -->
|
|
844
|
+
<div class="arv-status">
|
|
845
|
+
@if (api.loading$ | async) {
|
|
846
|
+
<span class="arv-spinner-sm"></span> Rendering…
|
|
847
|
+
} @else if (api.pageCount > 0) {
|
|
848
|
+
<span class="arv-status__ok">✓ {{ api.pageCount }} pages</span>
|
|
849
|
+
}
|
|
850
|
+
</div>
|
|
851
|
+
</div>
|
|
852
|
+
|
|
853
|
+
<!-- ═══ BODY ════════════════════════════════════════════════════ -->
|
|
854
|
+
<div class="arv-body">
|
|
855
|
+
|
|
856
|
+
<!-- Left panel -->
|
|
857
|
+
<arv-left-panel
|
|
858
|
+
class="arv-no-print"
|
|
859
|
+
[currentPage]="currentPage"
|
|
860
|
+
(pageSelected)="navigateTo($event)">
|
|
861
|
+
</arv-left-panel>
|
|
862
|
+
|
|
863
|
+
<!-- Virtual page list -->
|
|
864
|
+
<div class="arv-canvas-scroll"
|
|
865
|
+
#scrollEl
|
|
866
|
+
(scroll)="onScroll()">
|
|
867
|
+
|
|
868
|
+
@if (api.loading$ | async) {
|
|
869
|
+
<div class="arv-loading">
|
|
870
|
+
<div class="arv-spinner"></div>
|
|
871
|
+
<div>Rendering report…</div>
|
|
872
|
+
</div>
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
@else if ((api.session$ | async) === null) {
|
|
876
|
+
<div class="arv-empty">
|
|
877
|
+
No report loaded.
|
|
878
|
+
</div>
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
@else {
|
|
882
|
+
<div class="arv-page-list"
|
|
883
|
+
[style.width.px]="pageListWidth">
|
|
884
|
+
|
|
885
|
+
@for (p of pageNumbers; track p) {
|
|
886
|
+
<div class="arv-page-wrap"
|
|
887
|
+
[id]="'arv-pw-' + p"
|
|
888
|
+
[style.height.px]="pageWrapHeight">
|
|
889
|
+
|
|
890
|
+
<div class="arv-page-num-label arv-no-print">{{ p }}</div>
|
|
891
|
+
|
|
892
|
+
@if (isInWindow(p)) {
|
|
893
|
+
<!-- Rendered page -->
|
|
894
|
+
<arv-page-renderer
|
|
895
|
+
#pageRenderers
|
|
896
|
+
[page]="getPage(p)"
|
|
897
|
+
[scale]="scale"
|
|
898
|
+
[attr.data-page]="p">
|
|
899
|
+
</arv-page-renderer>
|
|
900
|
+
} @else {
|
|
901
|
+
<!-- Placeholder (correct height to keep scrollbar accurate) -->
|
|
902
|
+
<div class="arv-placeholder"
|
|
903
|
+
[style.width.px]="pageWidthPx"
|
|
904
|
+
[style.height.px]="pageHeightPx">
|
|
905
|
+
Page {{ p }}
|
|
906
|
+
</div>
|
|
907
|
+
}
|
|
908
|
+
</div>
|
|
909
|
+
}
|
|
910
|
+
</div>
|
|
911
|
+
}
|
|
912
|
+
</div>
|
|
913
|
+
</div>
|
|
914
|
+
</div>
|
|
915
|
+
|
|
916
|
+
<!-- ═══ PRINT STYLES ═══════════════════════════════════════════════ -->
|
|
917
|
+
<style id="arv-print-css">
|
|
918
|
+
@media print {
|
|
919
|
+
.arv-no-print { display: none !important; }
|
|
920
|
+
.arv-shell { height: auto !important; }
|
|
921
|
+
.arv-body { overflow: visible !important; }
|
|
922
|
+
.arv-canvas-scroll { overflow: visible !important; height: auto !important; }
|
|
923
|
+
.arv-page-wrap { page-break-after: always; }
|
|
924
|
+
.arv-page-wrap:last-child { page-break-after: avoid; }
|
|
925
|
+
}
|
|
926
|
+
</style>
|
|
927
|
+
`, isInline: true, styles: [":host{display:block;height:100%}.arv-shell{display:flex;flex-direction:column;height:100%;background:var(--arv-bg, #1e1e2e);color:var(--arv-text, #abb2bf);font-family:system-ui,-apple-system,sans-serif;font-size:12px}[data-theme=light]{--arv-bg: #f0f0f5;--arv-surface: #ffffff;--arv-surface2: #f5f5fa;--arv-border: #d0d0e0;--arv-accent: #3b6fd4;--arv-text: #333;--arv-text-muted: #888}.arv-toolbar{display:flex;align-items:center;gap:8px;padding:0 12px;height:44px;flex-shrink:0;background:var(--arv-surface, #2a2a3e);border-bottom:1px solid var(--arv-border, #3d3d5c);flex-wrap:wrap}.arv-toolbar__brand{font-weight:700;font-size:13px;color:var(--arv-text-strong, #eee);margin-right:8px}.arv-nav{display:flex;align-items:center;gap:4px}.arv-nav__of{font-size:11px;color:var(--arv-text-muted, #888)}.arv-page-input{width:52px;text-align:center;padding:2px 4px;background:var(--arv-surface, #2a2a3e);border:1px solid var(--arv-border, #3d3d5c);border-radius:4px;color:var(--arv-text, #ccc);font-size:12px}.arv-zoom{display:flex;align-items:center;gap:4px}.arv-zoom__val{min-width:42px;text-align:center;font-size:11px;color:var(--arv-text-muted, #888)}.arv-btn{display:inline-flex;align-items:center;gap:3px;padding:3px 8px;background:var(--arv-surface2, #313148);border:1px solid var(--arv-border, #3d3d5c);border-radius:4px;color:var(--arv-text, #ccc);font-size:11px;cursor:pointer;white-space:nowrap}.arv-btn:hover{background:var(--arv-border, #3d3d5c)}.arv-btn:disabled{opacity:.4;pointer-events:none}.arv-btn--icon{padding:3px 6px;font-size:13px}.arv-btn--sm{padding:2px 7px}.arv-btn-group{display:flex;gap:1px}.arv-btn-group .arv-btn:first-child{border-radius:4px 0 0 4px}.arv-btn-group .arv-btn:last-child{border-radius:0 4px 4px 0}.arv-status{margin-left:auto;display:flex;align-items:center;gap:6px;font-size:11px;color:var(--arv-text-muted, #888)}.arv-status__ok{color:#4caf50}.arv-body{display:flex;flex:1;overflow:hidden;min-height:0}.arv-canvas-scroll{flex:1;overflow:auto;background:var(--arv-bg, #1e1e2e);padding:20px;display:flex;flex-direction:column;align-items:center}.arv-page-list{display:flex;flex-direction:column;align-items:center}.arv-page-wrap{position:relative;display:flex;flex-direction:column;align-items:center;padding-bottom:16px}.arv-page-num-label{font-size:10px;color:var(--arv-text-muted, #888);margin-bottom:4px;align-self:flex-start}.arv-placeholder{background:var(--arv-surface, #2a2a3e);border:1px dashed var(--arv-border, #3d3d5c);display:flex;align-items:center;justify-content:center;color:var(--arv-text-muted, #888);font-size:11px}.arv-loading{display:flex;flex-direction:column;align-items:center;gap:12px;padding:60px;color:var(--arv-text-muted, #888)}.arv-spinner{width:32px;height:32px;border:3px solid var(--arv-border, #3d3d5c);border-top-color:var(--arv-accent, #5b8ef0);border-radius:50%;animation:arv-spin .8s linear infinite}.arv-spinner-sm{display:inline-block;width:14px;height:14px;border:2px solid var(--arv-border, #3d3d5c);border-top-color:var(--arv-accent, #5b8ef0);border-radius:50%;animation:arv-spin .7s linear infinite}@keyframes arv-spin{to{transform:rotate(360deg)}}.arv-empty{padding:60px;text-align:center;color:var(--arv-text-muted, #888)}\n", "@media print{.arv-no-print{display:none!important}.arv-shell{height:auto!important}.arv-body{overflow:visible!important}.arv-canvas-scroll{overflow:visible!important;height:auto!important}.arv-page-wrap{page-break-after:always}.arv-page-wrap:last-child{page-break-after:avoid}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: PageRendererComponent, selector: "arv-page-renderer", inputs: ["page", "scale"] }, { kind: "component", type: LeftPanelComponent, selector: "arv-left-panel", inputs: ["currentPage"], outputs: ["pageSelected"] }, { kind: "pipe", type: i3.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
928
|
+
}
|
|
929
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ReportViewerComponent, decorators: [{
|
|
930
|
+
type: Component,
|
|
931
|
+
args: [{ selector: 'arv-report-viewer', standalone: true, imports: [CommonModule, FormsModule, PageRendererComponent, LeftPanelComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
932
|
+
<div class="arv-shell" [attr.data-theme]="theme">
|
|
933
|
+
|
|
934
|
+
<!-- ═══ TOOLBAR ════════════════════════════════════════════════ -->
|
|
935
|
+
<div class="arv-toolbar arv-no-print">
|
|
936
|
+
|
|
937
|
+
<span class="arv-toolbar__brand">📄 Report Viewer</span>
|
|
938
|
+
|
|
939
|
+
<!-- Navigation -->
|
|
940
|
+
<div class="arv-nav">
|
|
941
|
+
<button class="arv-btn arv-btn--icon" title="First" [disabled]="currentPage <= 1"
|
|
942
|
+
(click)="navigateTo(1)">⏮</button>
|
|
943
|
+
<button class="arv-btn arv-btn--icon" title="Previous" [disabled]="currentPage <= 1"
|
|
944
|
+
(click)="navigateTo(currentPage - 1)">◀</button>
|
|
945
|
+
|
|
946
|
+
<input class="arv-page-input" type="number"
|
|
947
|
+
[min]="1" [max]="api.pageCount"
|
|
948
|
+
[ngModel]="currentPage"
|
|
949
|
+
(change)="onPageInputChange($event)" />
|
|
950
|
+
<span class="arv-nav__of">/ {{ api.pageCount }}</span>
|
|
951
|
+
|
|
952
|
+
<button class="arv-btn arv-btn--icon" title="Next"
|
|
953
|
+
[disabled]="currentPage >= api.pageCount"
|
|
954
|
+
(click)="navigateTo(currentPage + 1)">▶</button>
|
|
955
|
+
<button class="arv-btn arv-btn--icon" title="Last"
|
|
956
|
+
[disabled]="currentPage >= api.pageCount"
|
|
957
|
+
(click)="navigateTo(api.pageCount)">⏭</button>
|
|
958
|
+
</div>
|
|
959
|
+
|
|
960
|
+
<!-- Zoom -->
|
|
961
|
+
<div class="arv-zoom">
|
|
962
|
+
<button class="arv-btn arv-btn--icon" title="Zoom out" (click)="zoomOut()">−</button>
|
|
963
|
+
<span class="arv-zoom__val">{{ zoomPercent }}%</span>
|
|
964
|
+
<button class="arv-btn arv-btn--icon" title="Zoom in" (click)="zoomIn()">+</button>
|
|
965
|
+
<button class="arv-btn arv-btn--sm" (click)="zoomReset()">1:1</button>
|
|
966
|
+
</div>
|
|
967
|
+
|
|
968
|
+
<!-- Refresh -->
|
|
969
|
+
<button class="arv-btn arv-btn--sm" title="Refresh" (click)="refresh()">🔄</button>
|
|
970
|
+
|
|
971
|
+
<!-- Print -->
|
|
972
|
+
<div class="arv-btn-group">
|
|
973
|
+
<button class="arv-btn arv-btn--sm" title="Print all pages" (click)="printAll()">🖨 Print All</button>
|
|
974
|
+
<button class="arv-btn arv-btn--sm" title="Print current page" (click)="printCurrent()">🖨 Page</button>
|
|
975
|
+
</div>
|
|
976
|
+
|
|
977
|
+
<!-- Download page as image -->
|
|
978
|
+
<button class="arv-btn arv-btn--sm" title="Download current page as image"
|
|
979
|
+
(click)="downloadPageImage()">⬇ Image</button>
|
|
980
|
+
|
|
981
|
+
<!-- Status -->
|
|
982
|
+
<div class="arv-status">
|
|
983
|
+
@if (api.loading$ | async) {
|
|
984
|
+
<span class="arv-spinner-sm"></span> Rendering…
|
|
985
|
+
} @else if (api.pageCount > 0) {
|
|
986
|
+
<span class="arv-status__ok">✓ {{ api.pageCount }} pages</span>
|
|
987
|
+
}
|
|
988
|
+
</div>
|
|
989
|
+
</div>
|
|
990
|
+
|
|
991
|
+
<!-- ═══ BODY ════════════════════════════════════════════════════ -->
|
|
992
|
+
<div class="arv-body">
|
|
993
|
+
|
|
994
|
+
<!-- Left panel -->
|
|
995
|
+
<arv-left-panel
|
|
996
|
+
class="arv-no-print"
|
|
997
|
+
[currentPage]="currentPage"
|
|
998
|
+
(pageSelected)="navigateTo($event)">
|
|
999
|
+
</arv-left-panel>
|
|
1000
|
+
|
|
1001
|
+
<!-- Virtual page list -->
|
|
1002
|
+
<div class="arv-canvas-scroll"
|
|
1003
|
+
#scrollEl
|
|
1004
|
+
(scroll)="onScroll()">
|
|
1005
|
+
|
|
1006
|
+
@if (api.loading$ | async) {
|
|
1007
|
+
<div class="arv-loading">
|
|
1008
|
+
<div class="arv-spinner"></div>
|
|
1009
|
+
<div>Rendering report…</div>
|
|
1010
|
+
</div>
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
@else if ((api.session$ | async) === null) {
|
|
1014
|
+
<div class="arv-empty">
|
|
1015
|
+
No report loaded.
|
|
1016
|
+
</div>
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
@else {
|
|
1020
|
+
<div class="arv-page-list"
|
|
1021
|
+
[style.width.px]="pageListWidth">
|
|
1022
|
+
|
|
1023
|
+
@for (p of pageNumbers; track p) {
|
|
1024
|
+
<div class="arv-page-wrap"
|
|
1025
|
+
[id]="'arv-pw-' + p"
|
|
1026
|
+
[style.height.px]="pageWrapHeight">
|
|
1027
|
+
|
|
1028
|
+
<div class="arv-page-num-label arv-no-print">{{ p }}</div>
|
|
1029
|
+
|
|
1030
|
+
@if (isInWindow(p)) {
|
|
1031
|
+
<!-- Rendered page -->
|
|
1032
|
+
<arv-page-renderer
|
|
1033
|
+
#pageRenderers
|
|
1034
|
+
[page]="getPage(p)"
|
|
1035
|
+
[scale]="scale"
|
|
1036
|
+
[attr.data-page]="p">
|
|
1037
|
+
</arv-page-renderer>
|
|
1038
|
+
} @else {
|
|
1039
|
+
<!-- Placeholder (correct height to keep scrollbar accurate) -->
|
|
1040
|
+
<div class="arv-placeholder"
|
|
1041
|
+
[style.width.px]="pageWidthPx"
|
|
1042
|
+
[style.height.px]="pageHeightPx">
|
|
1043
|
+
Page {{ p }}
|
|
1044
|
+
</div>
|
|
1045
|
+
}
|
|
1046
|
+
</div>
|
|
1047
|
+
}
|
|
1048
|
+
</div>
|
|
1049
|
+
}
|
|
1050
|
+
</div>
|
|
1051
|
+
</div>
|
|
1052
|
+
</div>
|
|
1053
|
+
|
|
1054
|
+
<!-- ═══ PRINT STYLES ═══════════════════════════════════════════════ -->
|
|
1055
|
+
<style id="arv-print-css">
|
|
1056
|
+
@media print {
|
|
1057
|
+
.arv-no-print { display: none !important; }
|
|
1058
|
+
.arv-shell { height: auto !important; }
|
|
1059
|
+
.arv-body { overflow: visible !important; }
|
|
1060
|
+
.arv-canvas-scroll { overflow: visible !important; height: auto !important; }
|
|
1061
|
+
.arv-page-wrap { page-break-after: always; }
|
|
1062
|
+
.arv-page-wrap:last-child { page-break-after: avoid; }
|
|
1063
|
+
}
|
|
1064
|
+
</style>
|
|
1065
|
+
`, styles: [":host{display:block;height:100%}.arv-shell{display:flex;flex-direction:column;height:100%;background:var(--arv-bg, #1e1e2e);color:var(--arv-text, #abb2bf);font-family:system-ui,-apple-system,sans-serif;font-size:12px}[data-theme=light]{--arv-bg: #f0f0f5;--arv-surface: #ffffff;--arv-surface2: #f5f5fa;--arv-border: #d0d0e0;--arv-accent: #3b6fd4;--arv-text: #333;--arv-text-muted: #888}.arv-toolbar{display:flex;align-items:center;gap:8px;padding:0 12px;height:44px;flex-shrink:0;background:var(--arv-surface, #2a2a3e);border-bottom:1px solid var(--arv-border, #3d3d5c);flex-wrap:wrap}.arv-toolbar__brand{font-weight:700;font-size:13px;color:var(--arv-text-strong, #eee);margin-right:8px}.arv-nav{display:flex;align-items:center;gap:4px}.arv-nav__of{font-size:11px;color:var(--arv-text-muted, #888)}.arv-page-input{width:52px;text-align:center;padding:2px 4px;background:var(--arv-surface, #2a2a3e);border:1px solid var(--arv-border, #3d3d5c);border-radius:4px;color:var(--arv-text, #ccc);font-size:12px}.arv-zoom{display:flex;align-items:center;gap:4px}.arv-zoom__val{min-width:42px;text-align:center;font-size:11px;color:var(--arv-text-muted, #888)}.arv-btn{display:inline-flex;align-items:center;gap:3px;padding:3px 8px;background:var(--arv-surface2, #313148);border:1px solid var(--arv-border, #3d3d5c);border-radius:4px;color:var(--arv-text, #ccc);font-size:11px;cursor:pointer;white-space:nowrap}.arv-btn:hover{background:var(--arv-border, #3d3d5c)}.arv-btn:disabled{opacity:.4;pointer-events:none}.arv-btn--icon{padding:3px 6px;font-size:13px}.arv-btn--sm{padding:2px 7px}.arv-btn-group{display:flex;gap:1px}.arv-btn-group .arv-btn:first-child{border-radius:4px 0 0 4px}.arv-btn-group .arv-btn:last-child{border-radius:0 4px 4px 0}.arv-status{margin-left:auto;display:flex;align-items:center;gap:6px;font-size:11px;color:var(--arv-text-muted, #888)}.arv-status__ok{color:#4caf50}.arv-body{display:flex;flex:1;overflow:hidden;min-height:0}.arv-canvas-scroll{flex:1;overflow:auto;background:var(--arv-bg, #1e1e2e);padding:20px;display:flex;flex-direction:column;align-items:center}.arv-page-list{display:flex;flex-direction:column;align-items:center}.arv-page-wrap{position:relative;display:flex;flex-direction:column;align-items:center;padding-bottom:16px}.arv-page-num-label{font-size:10px;color:var(--arv-text-muted, #888);margin-bottom:4px;align-self:flex-start}.arv-placeholder{background:var(--arv-surface, #2a2a3e);border:1px dashed var(--arv-border, #3d3d5c);display:flex;align-items:center;justify-content:center;color:var(--arv-text-muted, #888);font-size:11px}.arv-loading{display:flex;flex-direction:column;align-items:center;gap:12px;padding:60px;color:var(--arv-text-muted, #888)}.arv-spinner{width:32px;height:32px;border:3px solid var(--arv-border, #3d3d5c);border-top-color:var(--arv-accent, #5b8ef0);border-radius:50%;animation:arv-spin .8s linear infinite}.arv-spinner-sm{display:inline-block;width:14px;height:14px;border:2px solid var(--arv-border, #3d3d5c);border-top-color:var(--arv-accent, #5b8ef0);border-radius:50%;animation:arv-spin .7s linear infinite}@keyframes arv-spin{to{transform:rotate(360deg)}}.arv-empty{padding:60px;text-align:center;color:var(--arv-text-muted, #888)}\n", "@media print{.arv-no-print{display:none!important}.arv-shell{height:auto!important}.arv-body{overflow:visible!important}.arv-canvas-scroll{overflow:visible!important;height:auto!important}.arv-page-wrap{page-break-after:always}.arv-page-wrap:last-child{page-break-after:avoid}}\n"] }]
|
|
1066
|
+
}], ctorParameters: () => [{ type: ReportViewerApiService }, { type: i0.ChangeDetectorRef }], propDecorators: { apiBaseUrl: [{
|
|
1067
|
+
type: Input
|
|
1068
|
+
}], theme: [{
|
|
1069
|
+
type: Input
|
|
1070
|
+
}], renderRequest: [{
|
|
1071
|
+
type: Input
|
|
1072
|
+
}], scrollEl: [{
|
|
1073
|
+
type: ViewChild,
|
|
1074
|
+
args: ['scrollEl']
|
|
1075
|
+
}], pageRenderers: [{
|
|
1076
|
+
type: ViewChildren,
|
|
1077
|
+
args: ['pageRenderers']
|
|
1078
|
+
}] } });
|
|
1079
|
+
|
|
1080
|
+
// Public API for the report-viewer Angular library
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Generated bundle index. Do not edit.
|
|
1084
|
+
*/
|
|
1085
|
+
|
|
1086
|
+
export { LeftPanelComponent, PageRendererComponent, ReportViewerApiService, ReportViewerComponent };
|
|
1087
|
+
//# sourceMappingURL=arilekh-report-viewer.mjs.map
|