@nexterajs/report-viewer 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1219 @@
1
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
2
+ import * as i0 from '@angular/core';
3
+ import { input, inject, DOCUMENT, ElementRef, ChangeDetectionStrategy, Component, Injectable, signal, computed, DestroyRef, output } from '@angular/core';
4
+ import { HttpErrorResponse, HttpClient } from '@angular/common/http';
5
+ import { defer, map, catchError, from, switchMap, throwError, timeout, Subject, Subscription, finalize, interval, exhaustMap, EMPTY } from 'rxjs';
6
+
7
+ /**
8
+ * The version of the viewer protocol this package speaks: the `protocol` member of every
9
+ * `AngularViewerData` answer from `Nextera.Report.AspNetCore`.
10
+ */
11
+ const NXR_VIEWER_PROTOCOL = 1;
12
+
13
+ /**
14
+ * Page HTML from the server, made safe to place in the application's document.
15
+ *
16
+ * The report repository's HTML renderer escapes every attribute value and writes links only with
17
+ * http, https, mailto, tel or no scheme, and its chart renderer strips event handlers from chart SVG.
18
+ * This is the second layer: whatever reaches the viewer, a page can neither run script nor load active
19
+ * content, and it cannot style the viewer around it.
20
+ */
21
+ /**
22
+ * Elements that run script, load documents, plugins or media, submit, animate other attributes, or
23
+ * carry CSS of their own. The renderer writes none of them into a page.
24
+ */
25
+ const REMOVED_ELEMENTS = [
26
+ 'script',
27
+ 'noscript',
28
+ 'template',
29
+ 'style',
30
+ 'link',
31
+ 'meta',
32
+ 'base',
33
+ 'iframe',
34
+ 'frame',
35
+ 'frameset',
36
+ 'object',
37
+ 'embed',
38
+ 'applet',
39
+ 'portal',
40
+ 'audio',
41
+ 'video',
42
+ 'source',
43
+ 'track',
44
+ 'picture',
45
+ 'form',
46
+ 'set',
47
+ 'animate',
48
+ ].join(',');
49
+ /** Attributes holding a URL that a click or a load would follow. */
50
+ const URL_ATTRIBUTES = new Set([
51
+ 'href',
52
+ 'xlink:href',
53
+ 'src',
54
+ 'action',
55
+ 'formaction',
56
+ 'poster',
57
+ 'background',
58
+ ]);
59
+ /** Attributes that make requests of their own and have no use in a page. */
60
+ const REMOVED_ATTRIBUTES = new Set(['srcset', 'ping']);
61
+ const LINK_SCHEMES = new Set(['http', 'https', 'mailto', 'tel']);
62
+ /**
63
+ * Whether a URL attribute's value can stay: no scheme (a relative or fragment URL), a link scheme, or,
64
+ * for `src`, an image as a data URL. Browsers ignore spaces and control characters around and inside a
65
+ * scheme, so the check drops them before reading it.
66
+ */
67
+ function isSafeUrl(attribute, value) {
68
+ let compact = '';
69
+ for (const character of value) {
70
+ if (character.charCodeAt(0) > 0x20)
71
+ compact += character;
72
+ }
73
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(compact)?.[1]?.toLowerCase();
74
+ if (scheme === undefined || LINK_SCHEMES.has(scheme))
75
+ return true;
76
+ return attribute === 'src' && scheme === 'data' && /^data:image\//i.test(compact);
77
+ }
78
+ /**
79
+ * Parses page HTML and returns it without anything active. A template's content is inert while it is
80
+ * examined: its scripts do not run, its images do not load and its handlers do not fire.
81
+ */
82
+ function scrubPageHtml(html, document) {
83
+ const template = document.createElement('template');
84
+ template.innerHTML = html;
85
+ const content = template.content;
86
+ for (const element of Array.from(content.querySelectorAll(REMOVED_ELEMENTS))) {
87
+ element.remove();
88
+ }
89
+ for (const element of Array.from(content.querySelectorAll('*'))) {
90
+ for (const attribute of Array.from(element.attributes)) {
91
+ const name = attribute.name.toLowerCase();
92
+ const unsafe = name.startsWith('on') ||
93
+ REMOVED_ATTRIBUTES.has(name) ||
94
+ (URL_ATTRIBUTES.has(name) && !isSafeUrl(name, attribute.value));
95
+ if (unsafe)
96
+ element.removeAttribute(attribute.name);
97
+ }
98
+ }
99
+ return content;
100
+ }
101
+ /**
102
+ * Deletes every style rule whose selector names `:host` or `:host-context()`, also inside grouping rules
103
+ * such as `@media` and inside nested rules. In a shadow root those rules style the shadow host, the
104
+ * viewer's own page element, so a page's CSS could otherwise move or hide it.
105
+ */
106
+ function deleteHostRules(container) {
107
+ for (let index = container.cssRules.length - 1; index >= 0; index--) {
108
+ const rule = container.cssRules[index];
109
+ if (rule instanceof CSSStyleRule && /:host/i.test(rule.selectorText)) {
110
+ container.deleteRule(index);
111
+ }
112
+ else if ('cssRules' in rule && !(rule instanceof CSSKeyframesRule)) {
113
+ deleteHostRules(rule);
114
+ }
115
+ }
116
+ }
117
+ const sheets = new Map();
118
+ /**
119
+ * One constructed stylesheet per distinct CSS text, shared by every shadow root that adopts it. A
120
+ * report's page CSS is the same for every page, so a long report parses it once. The sheet keeps no rule
121
+ * that could style the shadow host. CSS that a constructed stylesheet refuses gives an empty sheet, so the
122
+ * page shows unstyled instead of not at all.
123
+ */
124
+ function adoptedSheet(cssText) {
125
+ let sheet = sheets.get(cssText);
126
+ if (!sheet) {
127
+ sheet = new CSSStyleSheet();
128
+ try {
129
+ sheet.replaceSync(cssText);
130
+ deleteHostRules(sheet);
131
+ }
132
+ catch {
133
+ sheet = new CSSStyleSheet();
134
+ }
135
+ sheets.set(cssText, sheet);
136
+ }
137
+ return sheet;
138
+ }
139
+
140
+ /** The viewer draws each page's frame and the space between pages, so the page CSS's own are cancelled. */
141
+ const PAGE_FRAME_CSS = '.nextera-page{margin:0;box-shadow:none;}';
142
+ /**
143
+ * One report page, in its own shadow root.
144
+ *
145
+ * The page's size comes from the report's page list, so the page holds its place before its HTML
146
+ * arrives. The HTML is scrubbed by `scrubPageHtml` and placed beside the adopted page stylesheet, so the
147
+ * report's styles and the application's never meet. `contain: paint` keeps even a fixed-position
148
+ * element inside the page's box.
149
+ */
150
+ class NxrReportPageComponent {
151
+ /** The page's number and size. */
152
+ page = input.required(/* @ts-ignore */
153
+ ...(ngDevMode ? [{ debugName: "page" }] : /* istanbul ignore next */ []));
154
+ /** The page's HTML from the server, or null while it has not arrived. */
155
+ html = input(null, /* @ts-ignore */
156
+ ...(ngDevMode ? [{ debugName: "html" }] : /* istanbul ignore next */ []));
157
+ /** The stylesheet every page of the report needs. */
158
+ css = input.required(/* @ts-ignore */
159
+ ...(ngDevMode ? [{ debugName: "css" }] : /* istanbul ignore next */ []));
160
+ document = inject(DOCUMENT);
161
+ root = inject(ElementRef).nativeElement.attachShadow({
162
+ mode: 'open',
163
+ });
164
+ ngOnChanges(changes) {
165
+ if (changes['css']) {
166
+ this.root.adoptedStyleSheets = [adoptedSheet(this.css()), adoptedSheet(PAGE_FRAME_CSS)];
167
+ }
168
+ if (changes['html']) {
169
+ const html = this.html();
170
+ if (html === null) {
171
+ this.root.replaceChildren();
172
+ }
173
+ else {
174
+ this.root.replaceChildren(scrubPageHtml(html, this.document));
175
+ }
176
+ }
177
+ }
178
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrReportPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
179
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxrReportPageComponent, isStandalone: true, selector: "nxr-report-page", inputs: { page: { classPropertyName: "page", publicName: "page", isSignal: true, isRequired: true, transformFunction: null }, html: { classPropertyName: "html", publicName: "html", isSignal: true, isRequired: false, transformFunction: null }, css: { classPropertyName: "css", publicName: "css", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "attr.data-page": "page().pageNumber", "style.width.pt": "page().widthPoints", "style.height.pt": "page().heightPoints" }, classAttribute: "nxr-report-page" }, usesOnChanges: true, ngImport: i0, template: '', isInline: true, styles: [":host{display:block;position:relative;box-sizing:content-box;contain:paint;background:#fff}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
180
+ }
181
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrReportPageComponent, decorators: [{
182
+ type: Component,
183
+ args: [{ selector: 'nxr-report-page', standalone: true, template: '', changeDetection: ChangeDetectionStrategy.OnPush, host: {
184
+ class: 'nxr-report-page',
185
+ '[attr.data-page]': 'page().pageNumber',
186
+ '[style.width.pt]': 'page().widthPoints',
187
+ '[style.height.pt]': 'page().heightPoints',
188
+ }, styles: [":host{display:block;position:relative;box-sizing:content-box;contain:paint;background:#fff}\n"] }]
189
+ }], propDecorators: { page: [{ type: i0.Input, args: [{ isSignal: true, alias: "page", required: true }] }], html: [{ type: i0.Input, args: [{ isSignal: true, alias: "html", required: false }] }], css: [{ type: i0.Input, args: [{ isSignal: true, alias: "css", required: true }] }] } });
190
+
191
+ /**
192
+ * Every caption and message the viewer shows, in its built-in English. A key in PascalCase is the name
193
+ * Stimulsoft's viewer gives the same text, such as `PageOf`; a key in camelCase names a message only this
194
+ * viewer has. The English is this package's own. A later plan lets a host's translation file replace any
195
+ * of them, so code reads them from an `NxrViewerStrings` value wherever one is at hand.
196
+ */
197
+ const NXR_ENGLISH = {
198
+ /** The label before the page-number box. */
199
+ Page: 'Page',
200
+ /** The label between the page-number box and the page count. */
201
+ PageOf: 'of',
202
+ FirstPage: 'First page',
203
+ PrevPage: 'Previous page',
204
+ NextPage: 'Next page',
205
+ LastPage: 'Last page',
206
+ errorUnreachable: 'The report server could not be reached.',
207
+ errorStatus: 'The report server answered with HTTP status {0}.',
208
+ errorTimeout: 'The report server did not answer within {0} seconds.',
209
+ errorMalformed: "The report server's answer to {0} is not one this viewer can read.",
210
+ errorProtocol: 'The report server speaks viewer protocol {0}, and this viewer speaks protocol {1}.',
211
+ };
212
+ /** Puts each value in place of its numbered placeholder: `{0}` is the first value. */
213
+ function formatString(template, ...values) {
214
+ return template.replace(/\{(\d+)\}/g, (placeholder, index) => Number(index) < values.length ? String(values[Number(index)]) : placeholder);
215
+ }
216
+
217
+ /**
218
+ * A success answer that is not what the protocol says its action answers: the wrong shape, or an
219
+ * AngularViewerData answer from another version of the protocol.
220
+ */
221
+ class NxrMalformedAnswerError extends Error {
222
+ action;
223
+ protocol;
224
+ constructor(action,
225
+ /** The version an AngularViewerData answer declared, when it declared another one. */
226
+ protocol = NXR_VIEWER_PROTOCOL) {
227
+ super(`The ${action} answer is not what viewer protocol ${NXR_VIEWER_PROTOCOL} answers.`);
228
+ this.action = action;
229
+ this.protocol = protocol;
230
+ this.name = 'NxrMalformedAnswerError';
231
+ }
232
+ }
233
+ /** A request that got no answer within the server options' `requestTimeout`. */
234
+ class NxrTimeoutError extends Error {
235
+ seconds;
236
+ constructor(seconds) {
237
+ super(`No answer within ${seconds} seconds.`);
238
+ this.seconds = seconds;
239
+ this.name = 'NxrTimeoutError';
240
+ }
241
+ }
242
+ const OPTION_GROUPS = ['actions', 'appearance', 'toolbar', 'exports', 'server'];
243
+ function isObject(value) {
244
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
245
+ }
246
+ function isIndex(value) {
247
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0;
248
+ }
249
+ function isNumber(value) {
250
+ return typeof value === 'number' && Number.isFinite(value);
251
+ }
252
+ function isBounds(value) {
253
+ return (isObject(value) &&
254
+ isNumber(value['x']) &&
255
+ isNumber(value['y']) &&
256
+ isNumber(value['width']) &&
257
+ isNumber(value['height']));
258
+ }
259
+ function isPages(value) {
260
+ if (!isObject(value))
261
+ return false;
262
+ const pages = value['pages'];
263
+ return (isIndex(value['startIndex']) &&
264
+ Array.isArray(pages) &&
265
+ pages.every((page) => typeof page === 'string'));
266
+ }
267
+ function isPageSize(value) {
268
+ return (isObject(value) &&
269
+ isIndex(value['pageNumber']) &&
270
+ isNumber(value['widthPoints']) &&
271
+ isNumber(value['heightPoints']));
272
+ }
273
+ function isLocated(value, textKey) {
274
+ return (isObject(value) &&
275
+ typeof value[textKey] === 'string' &&
276
+ isIndex(value['pageIndex']) &&
277
+ isBounds(value['bounds']));
278
+ }
279
+ /** Reads the answer to AngularViewerData: the token, the viewer options and the host's translation. */
280
+ function readViewerData(body) {
281
+ if (!isObject(body))
282
+ throw new NxrMalformedAnswerError('AngularViewerData');
283
+ const { protocol, token, options, localization } = body;
284
+ if (isNumber(protocol) && protocol !== NXR_VIEWER_PROTOCOL) {
285
+ throw new NxrMalformedAnswerError('AngularViewerData', protocol);
286
+ }
287
+ const valid = protocol === NXR_VIEWER_PROTOCOL &&
288
+ typeof token === 'string' &&
289
+ token !== '' &&
290
+ isObject(options) &&
291
+ OPTION_GROUPS.every((group) => isObject(options[group])) &&
292
+ (localization === null || isObject(localization));
293
+ if (!valid)
294
+ throw new NxrMalformedAnswerError('AngularViewerData');
295
+ return body;
296
+ }
297
+ /** Reads the answer to GetReport: the report's name, page count, page sizes, bookmarks and page stylesheet. */
298
+ function readReport(body) {
299
+ if (!isObject(body))
300
+ throw new NxrMalformedAnswerError('GetReport');
301
+ const { reportName, pageCount, pages, bookmarks, pageCss, pagesHtml } = body;
302
+ const valid = typeof reportName === 'string' &&
303
+ isIndex(pageCount) &&
304
+ Array.isArray(pages) &&
305
+ pages.length === pageCount &&
306
+ pages.every(isPageSize) &&
307
+ Array.isArray(bookmarks) &&
308
+ bookmarks.every((bookmark) => isLocated(bookmark, 'title')) &&
309
+ typeof pageCss === 'string' &&
310
+ (pagesHtml === undefined || isPages(pagesHtml));
311
+ if (!valid)
312
+ throw new NxrMalformedAnswerError('GetReport');
313
+ return {
314
+ ...body,
315
+ pagesHtml: pagesHtml ?? null,
316
+ };
317
+ }
318
+ /** Reads the answer to GetPages. */
319
+ function readPages(body) {
320
+ if (!isPages(body))
321
+ throw new NxrMalformedAnswerError('GetPages');
322
+ return body;
323
+ }
324
+ /** Reads the answer to Find. */
325
+ function readFind(body) {
326
+ const hits = isObject(body) ? body['hits'] : undefined;
327
+ if (!Array.isArray(hits) || !hits.every((hit) => isLocated(hit, 'text')))
328
+ throw new NxrMalformedAnswerError('Find');
329
+ return body;
330
+ }
331
+ /** Reads the answer to UpdateCache. */
332
+ function readUpdateCache(body) {
333
+ if (!isObject(body) || typeof body['updated'] !== 'boolean')
334
+ throw new NxrMalformedAnswerError('UpdateCache');
335
+ return body;
336
+ }
337
+ function described(status, message) {
338
+ return { status, type: 'Error', message, retryable: false, traceId: null };
339
+ }
340
+ /**
341
+ * What went wrong with a request, from whatever it failed with:
342
+ * - the server's own error answer, kept as it is;
343
+ * - an HTTP error answer without the protocol's shape (a host's, a framework's or a proxy's);
344
+ * - no answer at all;
345
+ * - a timeout;
346
+ * - a success answer the viewer cannot read.
347
+ */
348
+ function readFailure(failure, action, strings = NXR_ENGLISH) {
349
+ if (failure instanceof HttpErrorResponse) {
350
+ const body = failure.error;
351
+ if (isObject(body) &&
352
+ body['status'] === 'error' &&
353
+ typeof body['type'] === 'string' &&
354
+ typeof body['message'] === 'string' &&
355
+ typeof body['retryable'] === 'boolean') {
356
+ const traceId = typeof body['traceId'] === 'string' ? body['traceId'] : null;
357
+ return {
358
+ status: failure.status,
359
+ type: body['type'],
360
+ message: body['message'],
361
+ retryable: body['retryable'],
362
+ traceId,
363
+ };
364
+ }
365
+ if (failure.status === 0)
366
+ return described(0, strings.errorUnreachable);
367
+ if (failure.status >= 200 && failure.status < 300)
368
+ return described(0, formatString(strings.errorMalformed, action));
369
+ return described(failure.status, formatString(strings.errorStatus, failure.status));
370
+ }
371
+ if (failure instanceof NxrTimeoutError) {
372
+ return described(0, formatString(strings.errorTimeout, failure.seconds));
373
+ }
374
+ if (failure instanceof NxrMalformedAnswerError) {
375
+ return failure.protocol !== NXR_VIEWER_PROTOCOL
376
+ ? described(0, formatString(strings.errorProtocol, failure.protocol, NXR_VIEWER_PROTOCOL))
377
+ : described(0, formatString(strings.errorMalformed, failure.action));
378
+ }
379
+ return described(0, failure instanceof Error ? failure.message : String(failure));
380
+ }
381
+
382
+ /**
383
+ * The seconds a request may take when no positive `requestTimeout` applies. That includes the first
384
+ * request, which is sent before the server's options are known. Stimulsoft's viewer uses the same value.
385
+ */
386
+ const NXR_UNSET_REQUEST_TIMEOUT_SECONDS = 1000;
387
+ /**
388
+ * The URL of a host action. The action's path takes the place of `{action}` in `requestUrl`, and a query
389
+ * string on the action is appended to the URL's own.
390
+ */
391
+ function actionUrl(requestUrl, action) {
392
+ const queryAt = action.indexOf('?');
393
+ const path = queryAt < 0 ? action : action.slice(0, queryAt);
394
+ const url = requestUrl.replace('{action}', () => path);
395
+ if (queryAt < 0)
396
+ return url;
397
+ return `${url}${url.includes('?') ? '&' : '?'}${action.slice(queryAt + 1)}`;
398
+ }
399
+ /**
400
+ * The host action that answers a viewer action. GetReport goes to `getReport` (or `viewerEvent` when that
401
+ * is empty). The other actions go to `viewerEvent`, or to `getReport` when the server keeps nothing.
402
+ * ExportReport goes to `exportReport` when that is set.
403
+ */
404
+ function hostActionFor(options, action) {
405
+ const { getReport, viewerEvent, exportReport } = options.actions;
406
+ const reportAction = getReport || viewerEvent;
407
+ const eventAction = options.server.cacheMode === 'None' ? reportAction : viewerEvent;
408
+ if (action === 'GetReport')
409
+ return reportAction;
410
+ if (action === 'ExportReport')
411
+ return exportReport || eventAction;
412
+ return eventAction;
413
+ }
414
+ /** The file name in a Content-Disposition header, preferring its UTF-8 form. */
415
+ function fileNameOf(disposition) {
416
+ if (!disposition)
417
+ return null;
418
+ const encoded = /filename\*\s*=\s*UTF-8''([^;]+)/i.exec(disposition);
419
+ if (encoded) {
420
+ try {
421
+ return decodeURIComponent(encoded[1].trim());
422
+ }
423
+ catch {
424
+ // A malformed escape: the plain file name follows.
425
+ }
426
+ }
427
+ const plain = /filename\s*=\s*(?:"([^"]*)"|([^;]+))/i.exec(disposition);
428
+ return plain ? (plain[1] ?? plain[2]).trim() : null;
429
+ }
430
+ function timeoutOf(session) {
431
+ const seconds = session?.options.server.requestTimeout ?? 0;
432
+ return seconds > 0 ? seconds : NXR_UNSET_REQUEST_TIMEOUT_SECONDS;
433
+ }
434
+ function formOf(context, action, session, parameters) {
435
+ const form = new FormData();
436
+ form.set('nextera_action', action);
437
+ if (session) {
438
+ const { cacheMode, cacheTimeout, cacheItemPriority } = session.options.server;
439
+ form.set('nextera_token', session.token);
440
+ form.set('nextera_cache', JSON.stringify({ mode: cacheMode, timeout: cacheTimeout, priority: cacheItemPriority }));
441
+ }
442
+ if (parameters)
443
+ form.set('nextera_parameters', JSON.stringify(parameters));
444
+ if (context.properties)
445
+ form.set('properties', JSON.stringify(context.properties));
446
+ const added = context.postParametersFunction?.({ ...parameters, action });
447
+ if (typeof added === 'object' && added !== null) {
448
+ for (const [name, value] of Object.entries(added)) {
449
+ form.set(name, value instanceof Blob ? value : String(value));
450
+ }
451
+ }
452
+ return form;
453
+ }
454
+ /** An error answer to a blob request carries its body as a blob. Read it, as JSON when it is JSON, so it can be classified. */
455
+ async function readableFailure(failure) {
456
+ if (!(failure instanceof HttpErrorResponse) || !(failure.error instanceof Blob))
457
+ return failure;
458
+ const text = await failure.error.text();
459
+ let body = text;
460
+ try {
461
+ body = JSON.parse(text);
462
+ }
463
+ catch {
464
+ // Not JSON: the failure is described by its status.
465
+ }
466
+ return new HttpErrorResponse({
467
+ error: body,
468
+ headers: failure.headers,
469
+ status: failure.status,
470
+ statusText: failure.statusText,
471
+ url: failure.url ?? undefined,
472
+ });
473
+ }
474
+ /**
475
+ * Sends one viewer's protocol requests through the application's `HttpClient`, so the application's
476
+ * interceptors apply, and reads each answer. Every observable fails with an `NxrViewerFailure`. A request's
477
+ * form is built when the observable is subscribed.
478
+ */
479
+ class NxrViewerClientService {
480
+ http = inject(HttpClient);
481
+ /** AngularViewerData, sent to the component's `action`: the token and the options. */
482
+ openViewer(context, action) {
483
+ return this.post(context, 'AngularViewerData', action, null, null, readViewerData);
484
+ }
485
+ /** GetReport: renders the report, with the HTML of the pages in `range` when one is given. */
486
+ getReport(context, session, range) {
487
+ const hostAction = hostActionFor(session.options, 'GetReport');
488
+ return this.post(context, 'GetReport', hostAction, session, range ?? null, readReport);
489
+ }
490
+ /** GetPages: the HTML of the pages in `range`. */
491
+ getPages(context, session, range) {
492
+ return this.post(context, 'GetPages', hostActionFor(session.options, 'GetPages'), session, range, readPages);
493
+ }
494
+ /** Find: the text components that match. */
495
+ find(context, session, parameters) {
496
+ return this.post(context, 'Find', hostActionFor(session.options, 'Find'), session, parameters, readFind);
497
+ }
498
+ /** UpdateCache: keeps the server's copy of the report. */
499
+ updateCache(context, session) {
500
+ const hostAction = hostActionFor(session.options, 'UpdateCache');
501
+ return this.post(context, 'UpdateCache', hostAction, session, null, readUpdateCache);
502
+ }
503
+ /** ExportReport: the report as a file, in a format such as `Pdf` or `Html`. It never times out. */
504
+ exportReport(context, session, format) {
505
+ const url = actionUrl(context.requestUrl, hostActionFor(session.options, 'ExportReport'));
506
+ return defer(() => this.http.post(url, formOf(context, 'ExportReport', session, { format }), {
507
+ observe: 'response',
508
+ responseType: 'blob',
509
+ })).pipe(map((response) => ({
510
+ blob: response.body ?? new Blob([]),
511
+ fileName: fileNameOf(response.headers.get('Content-Disposition')) ?? '',
512
+ contentType: response.headers.get('Content-Type') ?? '',
513
+ })), catchError((failure) => from(readableFailure(failure)).pipe(switchMap((readable) => throwError(() => readFailure(readable, 'ExportReport', context.strings))))));
514
+ }
515
+ post(context, action, hostAction, session, parameters, read) {
516
+ const seconds = timeoutOf(session);
517
+ return defer(() => this.http.post(actionUrl(context.requestUrl, hostAction), formOf(context, action, session, parameters))).pipe(timeout({
518
+ first: seconds * 1000,
519
+ with: () => throwError(() => new NxrTimeoutError(seconds)),
520
+ }), map((body) => read(body)), catchError((failure) => throwError(() => readFailure(failure, action, context.strings))));
521
+ }
522
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrViewerClientService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
523
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrViewerClientService });
524
+ }
525
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrViewerClientService, decorators: [{
526
+ type: Injectable
527
+ }] });
528
+
529
+ /** How often an open viewer asks the server to keep its report: every 3 minutes, as Stimulsoft's viewer does. */
530
+ const NXR_KEEP_ALIVE_MS = 180_000;
531
+ function withPages(pages, answer) {
532
+ answer?.pages.forEach((html, offset) => pages.set(answer.startIndex + offset, html));
533
+ return pages;
534
+ }
535
+ /**
536
+ * One viewer's conversation with the report server, and the state the viewer shows.
537
+ *
538
+ * Every signal changes only in this service's methods and in the answers they handle; nothing reacts to
539
+ * a signal changing. Each viewer provides its own session, so viewers on one page never share state.
540
+ */
541
+ class NxrViewerSessionService {
542
+ client = inject(NxrViewerClientService);
543
+ optionsState = signal(null, /* @ts-ignore */
544
+ ...(ngDevMode ? [{ debugName: "optionsState" }] : /* istanbul ignore next */ []));
545
+ reportState = signal(null, /* @ts-ignore */
546
+ ...(ngDevMode ? [{ debugName: "reportState" }] : /* istanbul ignore next */ []));
547
+ htmlState = signal(new Map(), /* @ts-ignore */
548
+ ...(ngDevMode ? [{ debugName: "htmlState" }] : /* istanbul ignore next */ []));
549
+ pageIndexState = signal(0, /* @ts-ignore */
550
+ ...(ngDevMode ? [{ debugName: "pageIndexState" }] : /* istanbul ignore next */ []));
551
+ pendingState = signal(0, /* @ts-ignore */
552
+ ...(ngDevMode ? [{ debugName: "pendingState" }] : /* istanbul ignore next */ []));
553
+ failureState = signal(null, /* @ts-ignore */
554
+ ...(ngDevMode ? [{ debugName: "failureState" }] : /* istanbul ignore next */ []));
555
+ /** The options from the last AngularViewerData answer. */
556
+ options = this.optionsState.asReadonly();
557
+ /** The report from the last GetReport answer, or null before one. */
558
+ report = this.reportState.asReadonly();
559
+ /** The 0-based index of the page on screen. */
560
+ pageIndex = this.pageIndexState.asReadonly();
561
+ /** The failure on show, until it is dismissed. */
562
+ failure = this.failureState.asReadonly();
563
+ /** How many pages the report has; 0 before one is loaded. */
564
+ pageCount = computed(() => this.reportState()?.pageCount ?? 0, /* @ts-ignore */
565
+ ...(ngDevMode ? [{ debugName: "pageCount" }] : /* istanbul ignore next */ []));
566
+ /** Whether a request the user waits for is on its way. Keep-alive requests do not count. */
567
+ busy = computed(() => this.pendingState() > 0, /* @ts-ignore */
568
+ ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
569
+ /** The page on screen, or null before a report. */
570
+ shownPage = computed(() => {
571
+ const report = this.reportState();
572
+ const index = this.pageIndexState();
573
+ if (!report || index >= report.pageCount)
574
+ return null;
575
+ return { index, size: report.pages[index], html: this.htmlState().get(index) ?? null };
576
+ }, /* @ts-ignore */
577
+ ...(ngDevMode ? [{ debugName: "shownPage" }] : /* istanbul ignore next */ []));
578
+ /** Emits after each successful GetReport answer. */
579
+ reportLoaded = new Subject();
580
+ /** Emits each failure put on show. */
581
+ failed = new Subject();
582
+ context = null;
583
+ session = null;
584
+ /** Every request the user waits for. */
585
+ requests = new Subscription();
586
+ /** The page requests among them, which a new rendering of the report makes obsolete. */
587
+ pageRequests = new Subscription();
588
+ keepAlive = Subscription.EMPTY;
589
+ pagesOnTheWay = new Set();
590
+ /** True from the moment an expired report is sent to be rendered again until that rendering settles. */
591
+ recovering = false;
592
+ destroyed = false;
593
+ constructor() {
594
+ inject(DestroyRef).onDestroy(() => {
595
+ this.destroyed = true;
596
+ this.close();
597
+ this.reportLoaded.complete();
598
+ this.failed.complete();
599
+ });
600
+ }
601
+ /** Opens the report the host action names, in place of whatever this viewer showed. */
602
+ open(context, action) {
603
+ if (this.destroyed)
604
+ return;
605
+ this.close();
606
+ this.context = context;
607
+ this.reportState.set(null);
608
+ this.htmlState.set(new Map());
609
+ this.pageIndexState.set(0);
610
+ this.failureState.set(null);
611
+ if (!context.requestUrl)
612
+ return;
613
+ this.track(this.client.openViewer(context, action), false, (answer) => {
614
+ const session = { token: answer.token, options: answer.options };
615
+ this.session = session;
616
+ this.optionsState.set(answer.options);
617
+ this.startKeepAlive(context, session);
618
+ this.render();
619
+ });
620
+ }
621
+ /**
622
+ * Shows the page at a 0-based index; an index outside the report is ignored. Asking for the page already
623
+ * on screen fetches it again when its HTML is missing, as it is after its request failed.
624
+ */
625
+ goToPage(index) {
626
+ const inReport = Number.isInteger(index) && index >= 0 && index < this.pageCount();
627
+ if (this.destroyed || !inReport)
628
+ return;
629
+ this.pageIndexState.set(index);
630
+ this.loadPage(index);
631
+ }
632
+ /** Takes the failure off show. */
633
+ dismissFailure() {
634
+ this.failureState.set(null);
635
+ }
636
+ /**
637
+ * GetReport, asking for the page on screen, which stays on screen when it is still in the report. The new
638
+ * rendering makes every page request still on its way obsolete. Its settling ends a recovery.
639
+ */
640
+ render() {
641
+ const { context, session } = this.connection();
642
+ const range = { startIndex: this.pageIndexState(), count: 1 };
643
+ const rendering = this.client
644
+ .getReport(context, session, range)
645
+ .pipe(finalize(() => (this.recovering = false)));
646
+ this.track(rendering, false, (report) => {
647
+ this.pageRequests.unsubscribe();
648
+ this.pageRequests = new Subscription();
649
+ this.htmlState.set(withPages(new Map(), report.pagesHtml));
650
+ this.reportState.set(report);
651
+ this.pageIndexState.update((index) => Math.max(0, Math.min(index, report.pageCount - 1)));
652
+ this.reportLoaded.next();
653
+ this.loadPage(this.pageIndexState());
654
+ });
655
+ }
656
+ /** GetPages for one page, unless the page is already here or on its way. */
657
+ loadPage(index) {
658
+ if (index >= this.pageCount() || this.htmlState().has(index) || this.pagesOnTheWay.has(index))
659
+ return;
660
+ const { context, session } = this.connection();
661
+ this.pagesOnTheWay.add(index);
662
+ const request = this.client
663
+ .getPages(context, session, { startIndex: index, count: 1 })
664
+ .pipe(finalize(() => this.pagesOnTheWay.delete(index)));
665
+ this.pageRequests.add(this.track(request, true, (answer) => this.htmlState.set(withPages(new Map(this.htmlState()), answer))));
666
+ }
667
+ /**
668
+ * Sends a request the user waits for, and returns its subscription. A `recoverable` request that finds the
669
+ * report gone renders it again; every other failure goes on show.
670
+ */
671
+ track(request, recoverable, next) {
672
+ this.pendingState.update((count) => count + 1);
673
+ const subscription = request
674
+ .pipe(finalize(() => this.pendingState.update((count) => count - 1)))
675
+ .subscribe({
676
+ next,
677
+ error: (failure) => this.fail(failure, recoverable),
678
+ });
679
+ this.requests.add(subscription);
680
+ return subscription;
681
+ }
682
+ /**
683
+ * An expired report found by a recoverable request is rendered again, unless a rendering is already on its
684
+ * way; every other failure goes on show. Only that rendering settling ends the recovery, so failures in the
685
+ * meantime cannot start a second one.
686
+ */
687
+ fail(failure, recoverable) {
688
+ if (failure.type === 'CacheExpired' && recoverable) {
689
+ if (!this.recovering) {
690
+ this.recovering = true;
691
+ this.render();
692
+ }
693
+ return;
694
+ }
695
+ this.failureState.set(failure);
696
+ this.failed.next(failure);
697
+ }
698
+ /** UpdateCache every three minutes while the server keeps the report. Its failures are not shown. */
699
+ startKeepAlive(context, session) {
700
+ const { allowAutoUpdateCache, cacheMode } = session.options.server;
701
+ if (!allowAutoUpdateCache || cacheMode === 'None')
702
+ return;
703
+ this.keepAlive = interval(NXR_KEEP_ALIVE_MS)
704
+ .pipe(exhaustMap(() => this.client.updateCache(context, session).pipe(catchError(() => EMPTY))))
705
+ .subscribe();
706
+ }
707
+ /** Stops every request and the keep-alive. */
708
+ close() {
709
+ this.requests.unsubscribe();
710
+ this.requests = new Subscription();
711
+ this.pageRequests = new Subscription();
712
+ this.keepAlive.unsubscribe();
713
+ this.session = null;
714
+ this.pagesOnTheWay.clear();
715
+ this.recovering = false;
716
+ }
717
+ connection() {
718
+ if (!this.context || !this.session)
719
+ throw new Error('The viewer has no report open.');
720
+ return { context: this.context, session: this.session };
721
+ }
722
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrViewerSessionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
723
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrViewerSessionService });
724
+ }
725
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrViewerSessionService, decorators: [{
726
+ type: Injectable
727
+ }], ctorParameters: () => [] });
728
+
729
+ /** The width of the page-number box, as Stimulsoft's page control sizes it. */
730
+ const PAGE_BOX_WIDTH_PX = 45;
731
+ /**
732
+ * The toolbar's page navigation.
733
+ *
734
+ * Stimulsoft's toolbar is a long list of groups; this is the navigation group of it, which is what
735
+ * a report viewer cannot do without: first, previous, the page control, next, last. Each button
736
+ * appears only when its `toolbar.show…Button` option is on, and the group is laid out where
737
+ * `toolbar.alignment` says. The other groups — save, print, find, bookmarks, zoom, view mode —
738
+ * arrive with the tasks that implement them.
739
+ *
740
+ * Navigation is disabled the way Stimulsoft disables it: in `MultiplePages`, and in `Continuous`
741
+ * unless the report panel or the whole viewer scrolls. A button is disabled at the end of the
742
+ * report it would move past, and everything is disabled while a request the user waits for is on
743
+ * its way.
744
+ */
745
+ class NxrViewerToolbarComponent {
746
+ /** The options from the server, or null before the first answer, when the toolbar shows nothing. */
747
+ options = input(null, /* @ts-ignore */
748
+ ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
749
+ /** The 0-based index of the page on screen. */
750
+ pageIndex = input(0, /* @ts-ignore */
751
+ ...(ngDevMode ? [{ debugName: "pageIndex" }] : /* istanbul ignore next */ []));
752
+ /** How many pages the report has. */
753
+ pageCount = input(0, /* @ts-ignore */
754
+ ...(ngDevMode ? [{ debugName: "pageCount" }] : /* istanbul ignore next */ []));
755
+ /** Whether a request the user waits for is on its way. */
756
+ busy = input(false, /* @ts-ignore */
757
+ ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
758
+ /** The captions to show. */
759
+ strings = input(NXR_ENGLISH, /* @ts-ignore */
760
+ ...(ngDevMode ? [{ debugName: "strings" }] : /* istanbul ignore next */ []));
761
+ /** The 0-based page the user asked for. */
762
+ goToPage = output();
763
+ pageBoxWidth = PAGE_BOX_WIDTH_PX;
764
+ toolbar = computed(() => this.options()?.toolbar ?? null, /* @ts-ignore */
765
+ ...(ngDevMode ? [{ debugName: "toolbar" }] : /* istanbul ignore next */ []));
766
+ appearance = computed(() => this.options()?.appearance ?? null, /* @ts-ignore */
767
+ ...(ngDevMode ? [{ debugName: "appearance" }] : /* istanbul ignore next */ []));
768
+ /**
769
+ * Stimulsoft's rule: navigation is off in `MultiplePages`, and in `Continuous` unless the report
770
+ * panel scrolls or the viewer is full screen — in those cases the page is reached by scrolling.
771
+ */
772
+ navigationDisabled = computed(() => {
773
+ const toolbar = this.toolbar();
774
+ const appearance = this.appearance();
775
+ if (!toolbar || !appearance)
776
+ return true;
777
+ if (toolbar.viewMode === 'MultiplePages')
778
+ return true;
779
+ return (toolbar.viewMode === 'Continuous' &&
780
+ !appearance.scrollbarsMode &&
781
+ !appearance.fullScreenMode);
782
+ }, /* @ts-ignore */
783
+ ...(ngDevMode ? [{ debugName: "navigationDisabled" }] : /* istanbul ignore next */ []));
784
+ showFirstPage = computed(() => this.toolbar()?.showFirstPageButton === true, /* @ts-ignore */
785
+ ...(ngDevMode ? [{ debugName: "showFirstPage" }] : /* istanbul ignore next */ []));
786
+ showPreviousPage = computed(() => this.toolbar()?.showPreviousPageButton === true, /* @ts-ignore */
787
+ ...(ngDevMode ? [{ debugName: "showPreviousPage" }] : /* istanbul ignore next */ []));
788
+ showCurrentPage = computed(() => this.toolbar()?.showCurrentPageControl === true, /* @ts-ignore */
789
+ ...(ngDevMode ? [{ debugName: "showCurrentPage" }] : /* istanbul ignore next */ []));
790
+ showNextPage = computed(() => this.toolbar()?.showNextPageButton === true, /* @ts-ignore */
791
+ ...(ngDevMode ? [{ debugName: "showNextPage" }] : /* istanbul ignore next */ []));
792
+ showLastPage = computed(() => this.toolbar()?.showLastPageButton === true, /* @ts-ignore */
793
+ ...(ngDevMode ? [{ debugName: "showLastPage" }] : /* istanbul ignore next */ []));
794
+ atStart = computed(() => this.busy() || this.navigationDisabled() || this.pageIndex() <= 0, /* @ts-ignore */
795
+ ...(ngDevMode ? [{ debugName: "atStart" }] : /* istanbul ignore next */ []));
796
+ atEnd = computed(() => this.busy() || this.navigationDisabled() || this.pageIndex() >= this.pageCount() - 1, /* @ts-ignore */
797
+ ...(ngDevMode ? [{ debugName: "atEnd" }] : /* istanbul ignore next */ []));
798
+ pageControlDisabled = computed(() => this.busy() || this.navigationDisabled() || this.pageCount() <= 1, /* @ts-ignore */
799
+ ...(ngDevMode ? [{ debugName: "pageControlDisabled" }] : /* istanbul ignore next */ []));
800
+ backgroundColor = computed(() => this.toolbar()?.backgroundColor || null, /* @ts-ignore */
801
+ ...(ngDevMode ? [{ debugName: "backgroundColor" }] : /* istanbul ignore next */ []));
802
+ fontColor = computed(() => this.toolbar()?.fontColor || null, /* @ts-ignore */
803
+ ...(ngDevMode ? [{ debugName: "fontColor" }] : /* istanbul ignore next */ []));
804
+ fontFamily = computed(() => this.toolbar()?.fontFamily || null, /* @ts-ignore */
805
+ ...(ngDevMode ? [{ debugName: "fontFamily" }] : /* istanbul ignore next */ []));
806
+ /** A border only in `Simple`; `Separated` has none, as Stimulsoft's toolbar does. */
807
+ border = computed(() => {
808
+ const toolbar = this.toolbar();
809
+ if (!toolbar || toolbar.displayMode === 'Separated' || !toolbar.borderColor)
810
+ return null;
811
+ return `1px solid ${toolbar.borderColor}`;
812
+ }, /* @ts-ignore */
813
+ ...(ngDevMode ? [{ debugName: "border" }] : /* istanbul ignore next */ []));
814
+ justifyContent = computed(() => {
815
+ switch (this.toolbar()?.alignment) {
816
+ case 'Center':
817
+ return 'center';
818
+ case 'Right':
819
+ return 'flex-end';
820
+ default:
821
+ return 'flex-start';
822
+ }
823
+ }, /* @ts-ignore */
824
+ ...(ngDevMode ? [{ debugName: "justifyContent" }] : /* istanbul ignore next */ []));
825
+ /**
826
+ * Enter in the page box goes to the page typed, reading anything that is not a number as page 1
827
+ * and clamping to the report, as Stimulsoft's page control does. Leaving the box does nothing.
828
+ */
829
+ enterPage(event) {
830
+ const box = event.target;
831
+ const typed = Number.parseInt(box.value, 10);
832
+ const page = Math.min(Math.max(Number.isNaN(typed) ? 1 : typed, 1), this.pageCount());
833
+ box.value = String(page);
834
+ this.goTo(page - 1);
835
+ }
836
+ goTo(index) {
837
+ if (this.busy() || index === this.pageIndex() || index < 0 || index >= this.pageCount())
838
+ return;
839
+ this.goToPage.emit(index);
840
+ }
841
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrViewerToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
842
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxrViewerToolbarComponent, isStandalone: true, selector: "nxr-viewer-toolbar", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, pageIndex: { classPropertyName: "pageIndex", publicName: "pageIndex", isSignal: true, isRequired: false, transformFunction: null }, pageCount: { classPropertyName: "pageCount", publicName: "pageCount", isSignal: true, isRequired: false, transformFunction: null }, busy: { classPropertyName: "busy", publicName: "busy", isSignal: true, isRequired: false, transformFunction: null }, strings: { classPropertyName: "strings", publicName: "strings", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { goToPage: "goToPage" }, host: { attributes: { "role": "toolbar" }, properties: { "style.background-color": "backgroundColor()", "style.border": "border()", "style.color": "fontColor()", "style.font-family": "fontFamily()", "style.justify-content": "justifyContent()", "class.nxr-busy": "busy()" }, classAttribute: "nxr-viewer-toolbar" }, ngImport: i0, template: `
843
+ @if (showFirstPage()) {
844
+ <button
845
+ type="button"
846
+ class="nxr-toolbar-button"
847
+ [disabled]="atStart()"
848
+ [attr.aria-label]="strings().FirstPage"
849
+ (click)="goTo(0)"
850
+ >
851
+ <svg viewBox="0 0 16 16" aria-hidden="true">
852
+ <path d="M12.5 3v10L6 8zM4.5 3h1.2v10H4.5z" />
853
+ </svg>
854
+ </button>
855
+ }
856
+ @if (showPreviousPage()) {
857
+ <button
858
+ type="button"
859
+ class="nxr-toolbar-button"
860
+ [disabled]="atStart()"
861
+ [attr.aria-label]="strings().PrevPage"
862
+ (click)="goTo(pageIndex() - 1)"
863
+ >
864
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M11 3v10L4.5 8z" /></svg>
865
+ </button>
866
+ }
867
+ @if (showCurrentPage()) {
868
+ <span class="nxr-page-control">
869
+ <span class="nxr-page-label">{{ strings().Page }}</span>
870
+ <input
871
+ type="text"
872
+ inputmode="numeric"
873
+ class="nxr-page-box"
874
+ [style.width.px]="pageBoxWidth"
875
+ [disabled]="pageControlDisabled()"
876
+ [value]="pageIndex() + 1"
877
+ [attr.aria-label]="strings().Page"
878
+ (keydown.enter)="enterPage($event)"
879
+ />
880
+ <span class="nxr-page-label">{{ strings().PageOf }} {{ pageCount() }}</span>
881
+ </span>
882
+ }
883
+ @if (showNextPage()) {
884
+ <button
885
+ type="button"
886
+ class="nxr-toolbar-button"
887
+ [disabled]="atEnd()"
888
+ [attr.aria-label]="strings().NextPage"
889
+ (click)="goTo(pageIndex() + 1)"
890
+ >
891
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3v10l6.5-5z" /></svg>
892
+ </button>
893
+ }
894
+ @if (showLastPage()) {
895
+ <button
896
+ type="button"
897
+ class="nxr-toolbar-button"
898
+ [disabled]="atEnd()"
899
+ [attr.aria-label]="strings().LastPage"
900
+ (click)="goTo(pageCount() - 1)"
901
+ >
902
+ <svg viewBox="0 0 16 16" aria-hidden="true">
903
+ <path d="M3.5 3v10L10 8zM10.3 3h1.2v10h-1.2z" />
904
+ </svg>
905
+ </button>
906
+ }
907
+ `, isInline: true, styles: [":host{display:flex;align-items:center;gap:2px;box-sizing:border-box;padding:3px 6px;min-height:30px;position:relative;-webkit-user-select:none;user-select:none}:host(.nxr-busy):after{content:\"\";position:absolute;inset:0;cursor:default}.nxr-toolbar-button{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;padding:0;border:0;border-radius:2px;background:transparent;color:inherit;font:inherit;cursor:pointer}.nxr-toolbar-button:hover:not(:disabled){background:#00000014}.nxr-toolbar-button:disabled{opacity:.5;cursor:default}.nxr-toolbar-button svg{width:16px;height:16px;fill:currentColor}.nxr-page-control{display:inline-flex;align-items:center;gap:4px;padding:0 4px;white-space:nowrap}.nxr-page-box{height:21px;box-sizing:border-box;text-align:center;font:inherit;color:inherit}.nxr-page-box:disabled{color:transparent}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
908
+ }
909
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrViewerToolbarComponent, decorators: [{
910
+ type: Component,
911
+ args: [{ selector: 'nxr-viewer-toolbar', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
912
+ class: 'nxr-viewer-toolbar',
913
+ role: 'toolbar',
914
+ '[style.background-color]': 'backgroundColor()',
915
+ '[style.border]': 'border()',
916
+ '[style.color]': 'fontColor()',
917
+ '[style.font-family]': 'fontFamily()',
918
+ '[style.justify-content]': 'justifyContent()',
919
+ '[class.nxr-busy]': 'busy()',
920
+ }, template: `
921
+ @if (showFirstPage()) {
922
+ <button
923
+ type="button"
924
+ class="nxr-toolbar-button"
925
+ [disabled]="atStart()"
926
+ [attr.aria-label]="strings().FirstPage"
927
+ (click)="goTo(0)"
928
+ >
929
+ <svg viewBox="0 0 16 16" aria-hidden="true">
930
+ <path d="M12.5 3v10L6 8zM4.5 3h1.2v10H4.5z" />
931
+ </svg>
932
+ </button>
933
+ }
934
+ @if (showPreviousPage()) {
935
+ <button
936
+ type="button"
937
+ class="nxr-toolbar-button"
938
+ [disabled]="atStart()"
939
+ [attr.aria-label]="strings().PrevPage"
940
+ (click)="goTo(pageIndex() - 1)"
941
+ >
942
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M11 3v10L4.5 8z" /></svg>
943
+ </button>
944
+ }
945
+ @if (showCurrentPage()) {
946
+ <span class="nxr-page-control">
947
+ <span class="nxr-page-label">{{ strings().Page }}</span>
948
+ <input
949
+ type="text"
950
+ inputmode="numeric"
951
+ class="nxr-page-box"
952
+ [style.width.px]="pageBoxWidth"
953
+ [disabled]="pageControlDisabled()"
954
+ [value]="pageIndex() + 1"
955
+ [attr.aria-label]="strings().Page"
956
+ (keydown.enter)="enterPage($event)"
957
+ />
958
+ <span class="nxr-page-label">{{ strings().PageOf }} {{ pageCount() }}</span>
959
+ </span>
960
+ }
961
+ @if (showNextPage()) {
962
+ <button
963
+ type="button"
964
+ class="nxr-toolbar-button"
965
+ [disabled]="atEnd()"
966
+ [attr.aria-label]="strings().NextPage"
967
+ (click)="goTo(pageIndex() + 1)"
968
+ >
969
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3v10l6.5-5z" /></svg>
970
+ </button>
971
+ }
972
+ @if (showLastPage()) {
973
+ <button
974
+ type="button"
975
+ class="nxr-toolbar-button"
976
+ [disabled]="atEnd()"
977
+ [attr.aria-label]="strings().LastPage"
978
+ (click)="goTo(pageCount() - 1)"
979
+ >
980
+ <svg viewBox="0 0 16 16" aria-hidden="true">
981
+ <path d="M3.5 3v10L10 8zM10.3 3h1.2v10h-1.2z" />
982
+ </svg>
983
+ </button>
984
+ }
985
+ `, styles: [":host{display:flex;align-items:center;gap:2px;box-sizing:border-box;padding:3px 6px;min-height:30px;position:relative;-webkit-user-select:none;user-select:none}:host(.nxr-busy):after{content:\"\";position:absolute;inset:0;cursor:default}.nxr-toolbar-button{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;padding:0;border:0;border-radius:2px;background:transparent;color:inherit;font:inherit;cursor:pointer}.nxr-toolbar-button:hover:not(:disabled){background:#00000014}.nxr-toolbar-button:disabled{opacity:.5;cursor:default}.nxr-toolbar-button svg{width:16px;height:16px;fill:currentColor}.nxr-page-control{display:inline-flex;align-items:center;gap:4px;padding:0 4px;white-space:nowrap}.nxr-page-box{height:21px;box-sizing:border-box;text-align:center;font:inherit;color:inherit}.nxr-page-box:disabled{color:transparent}\n"] }]
986
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], pageIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageIndex", required: false }] }], pageCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageCount", required: false }] }], busy: [{ type: i0.Input, args: [{ isSignal: true, alias: "busy", required: false }] }], strings: [{ type: i0.Input, args: [{ isSignal: true, alias: "strings", required: false }] }], goToPage: [{ type: i0.Output, args: ["goToPage"] }] } });
987
+
988
+ /**
989
+ * A report served by `Nextera.Reports.Angular.NetCore`, shown page by page.
990
+ *
991
+ * The inputs, outputs and `api` are Stimulsoft's `stimulsoft-viewer-angular`, so a host swaps the
992
+ * component and keeps its bindings. As there, a change to any input reloads the report: the
993
+ * component asks for the viewer's options again and opens the report the action names.
994
+ *
995
+ * Each viewer provides its own session, so several on a page never share a report or a token.
996
+ */
997
+ class NxrReportViewerComponent {
998
+ /** Each request's URL, with `{action}` in place of the host action it goes to. */
999
+ requestUrl = input('', /* @ts-ignore */
1000
+ ...(ngDevMode ? [{ debugName: "requestUrl" }] : /* istanbul ignore next */ []));
1001
+ /** The host action that answers the first request, and so names the report to open. */
1002
+ action = input('', /* @ts-ignore */
1003
+ ...(ngDevMode ? [{ debugName: "action" }] : /* istanbul ignore next */ []));
1004
+ /** Sent to the host with every request, as JSON, when it is truthy. */
1005
+ properties = input(null, /* @ts-ignore */
1006
+ ...(ngDevMode ? [{ debugName: "properties" }] : /* istanbul ignore next */ []));
1007
+ /** The viewer's width; `100%` when it is not given. */
1008
+ width = input(null, /* @ts-ignore */
1009
+ ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
1010
+ /** The viewer's height; 650px when the report panel scrolls, otherwise `100%`. */
1011
+ height = input(null, /* @ts-ignore */
1012
+ ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
1013
+ /** The viewer's background; `White` when it is not given. */
1014
+ backgroundColor = input('White', /* @ts-ignore */
1015
+ ...(ngDevMode ? [{ debugName: "backgroundColor" }] : /* istanbul ignore next */ []));
1016
+ /** Styles for the viewer's root element. Width, height and background take precedence over it. */
1017
+ style = input(null, /* @ts-ignore */
1018
+ ...(ngDevMode ? [{ debugName: "style" }] : /* istanbul ignore next */ []));
1019
+ /** Called for every request; each key of an object it returns becomes a form field. */
1020
+ postParametersFunction = input(null, /* @ts-ignore */
1021
+ ...(ngDevMode ? [{ debugName: "postParametersFunction" }] : /* istanbul ignore next */ []));
1022
+ /** Emits after each report is loaded. */
1023
+ loaded = output();
1024
+ /** Emits every failure the viewer puts on show. */
1025
+ error = output();
1026
+ session = inject(NxrViewerSessionService);
1027
+ /** Stimulsoft's `api` object: what a host calls to drive the viewer from outside. */
1028
+ api = this.buildApi();
1029
+ appearance = computed(() => this.session.options()?.appearance ?? null, /* @ts-ignore */
1030
+ ...(ngDevMode ? [{ debugName: "appearance" }] : /* istanbul ignore next */ []));
1031
+ toolbarVisible = computed(() => this.session.options()?.toolbar.visible !== false, /* @ts-ignore */
1032
+ ...(ngDevMode ? [{ debugName: "toolbarVisible" }] : /* istanbul ignore next */ []));
1033
+ pageCss = computed(() => this.session.report()?.pageCss ?? '', /* @ts-ignore */
1034
+ ...(ngDevMode ? [{ debugName: "pageCss" }] : /* istanbul ignore next */ []));
1035
+ showPageShadow = computed(() => this.appearance()?.showPageShadow === true, /* @ts-ignore */
1036
+ ...(ngDevMode ? [{ debugName: "showPageShadow" }] : /* istanbul ignore next */ []));
1037
+ pageBorder = computed(() => {
1038
+ const color = this.appearance()?.pageBorderColor;
1039
+ return color ? `1px solid ${color}` : null;
1040
+ }, /* @ts-ignore */
1041
+ ...(ngDevMode ? [{ debugName: "pageBorder" }] : /* istanbul ignore next */ []));
1042
+ /** Stimulsoft aligns the pages with the report panel's text alignment; `Default` means centred. */
1043
+ pageAlignment = computed(() => {
1044
+ switch (this.appearance()?.pageAlignment) {
1045
+ case 'Left':
1046
+ return 'left';
1047
+ case 'Right':
1048
+ return 'right';
1049
+ default:
1050
+ return 'center';
1051
+ }
1052
+ }, /* @ts-ignore */
1053
+ ...(ngDevMode ? [{ debugName: "pageAlignment" }] : /* istanbul ignore next */ []));
1054
+ /** The report panel scrolls inside the viewer only when the server asks for scrollbars. */
1055
+ pagesOverflow = computed(() => this.appearance()?.scrollbarsMode ? 'auto' : 'hidden', /* @ts-ignore */
1056
+ ...(ngDevMode ? [{ debugName: "pagesOverflow" }] : /* istanbul ignore next */ []));
1057
+ rootWidth = computed(() => this.width() ?? '100%', /* @ts-ignore */
1058
+ ...(ngDevMode ? [{ debugName: "rootWidth" }] : /* istanbul ignore next */ []));
1059
+ rootBackgroundColor = computed(() => this.backgroundColor() ?? 'White', /* @ts-ignore */
1060
+ ...(ngDevMode ? [{ debugName: "rootBackgroundColor" }] : /* istanbul ignore next */ []));
1061
+ rootHeight = computed(() => {
1062
+ const given = this.height();
1063
+ if (given !== null)
1064
+ return given;
1065
+ return this.appearance()?.scrollbarsMode ? '650px' : '100%';
1066
+ }, /* @ts-ignore */
1067
+ ...(ngDevMode ? [{ debugName: "rootHeight" }] : /* istanbul ignore next */ []));
1068
+ constructor() {
1069
+ this.session.reportLoaded
1070
+ .pipe(takeUntilDestroyed())
1071
+ .subscribe(() => this.loaded.emit());
1072
+ this.session.failed
1073
+ .pipe(takeUntilDestroyed())
1074
+ .subscribe((failure) => this.error.emit(failure));
1075
+ }
1076
+ /**
1077
+ * A change to any input reloads, as Stimulsoft's viewer does: its `ngOnChanges` never looks at
1078
+ * which input changed, so even a width change asks the server for the options and the report
1079
+ * again. Matching that keeps a host's reload behaviour the same across the swap.
1080
+ */
1081
+ ngOnChanges() {
1082
+ this.session.open(this.context(), this.action());
1083
+ }
1084
+ /**
1085
+ * The getters read the session's signals, so a host that asks between renders gets what is on
1086
+ * screen now. Built from a captured session rather than `this`, because `this` inside an object
1087
+ * literal's accessor is the literal.
1088
+ */
1089
+ buildApi() {
1090
+ const session = this.session;
1091
+ return {
1092
+ get currentPage() {
1093
+ return session.pageIndex();
1094
+ },
1095
+ set currentPage(value) {
1096
+ session.goToPage(value);
1097
+ },
1098
+ get pagesCount() {
1099
+ return session.pageCount();
1100
+ },
1101
+ get reportType() {
1102
+ return 'Report';
1103
+ },
1104
+ };
1105
+ }
1106
+ context() {
1107
+ return {
1108
+ requestUrl: this.requestUrl(),
1109
+ properties: this.properties(),
1110
+ postParametersFunction: this.postParametersFunction(),
1111
+ strings: NXR_ENGLISH,
1112
+ };
1113
+ }
1114
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrReportViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1115
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxrReportViewerComponent, isStandalone: true, selector: "nxr-report-viewer", inputs: { requestUrl: { classPropertyName: "requestUrl", publicName: "requestUrl", isSignal: true, isRequired: false, transformFunction: null }, action: { classPropertyName: "action", publicName: "action", isSignal: true, isRequired: false, transformFunction: null }, properties: { classPropertyName: "properties", publicName: "properties", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, backgroundColor: { classPropertyName: "backgroundColor", publicName: "backgroundColor", isSignal: true, isRequired: false, transformFunction: null }, style: { classPropertyName: "style", publicName: "style", isSignal: true, isRequired: false, transformFunction: null }, postParametersFunction: { classPropertyName: "postParametersFunction", publicName: "postParametersFunction", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loaded: "loaded", error: "error" }, providers: [NxrViewerClientService, NxrViewerSessionService], usesOnChanges: true, ngImport: i0, template: `
1116
+ <div
1117
+ class="nxr-viewer-root"
1118
+ [style]="style()"
1119
+ [style.background-color]="rootBackgroundColor()"
1120
+ [style.width]="rootWidth()"
1121
+ [style.height]="rootHeight()"
1122
+ >
1123
+ @if (toolbarVisible()) {
1124
+ <nxr-viewer-toolbar
1125
+ [options]="session.options()"
1126
+ [pageIndex]="session.pageIndex()"
1127
+ [pageCount]="session.pageCount()"
1128
+ [busy]="session.busy()"
1129
+ (goToPage)="session.goToPage($event)"
1130
+ />
1131
+ }
1132
+
1133
+ <div
1134
+ class="nxr-viewer-pages"
1135
+ [style.text-align]="pageAlignment()"
1136
+ [style.overflow]="pagesOverflow()"
1137
+ >
1138
+ @if (session.shownPage(); as page) {
1139
+ <nxr-report-page
1140
+ class="nxr-viewer-page"
1141
+ [class.nxr-viewer-page-shadow]="showPageShadow()"
1142
+ [style.border]="pageBorder()"
1143
+ [page]="page.size"
1144
+ [html]="page.html"
1145
+ [css]="pageCss()"
1146
+ />
1147
+ }
1148
+ </div>
1149
+
1150
+ @if (session.failure(); as failure) {
1151
+ <div class="nxr-viewer-failure" role="alert">
1152
+ <span class="nxr-viewer-failure-text">{{ failure.message }}</span>
1153
+ <button type="button" class="nxr-viewer-failure-close" (click)="session.dismissFailure()">
1154
+ &times;
1155
+ </button>
1156
+ </div>
1157
+ }
1158
+ </div>
1159
+ `, isInline: true, styles: [":host{display:inline-block;position:relative}.nxr-viewer-root{display:flex;flex-direction:column;box-sizing:border-box;position:relative}.nxr-viewer-pages{flex:1 1 auto;min-height:0;padding:10px;box-sizing:border-box}.nxr-viewer-page{display:inline-block;vertical-align:top}.nxr-viewer-page-shadow{box-shadow:2px 2px 6px #0000004d}.nxr-viewer-failure{position:absolute;left:0;right:0;bottom:0;display:flex;align-items:center;gap:8px;padding:8px 12px;background:#fdecea;color:#611a15;font-size:13px}.nxr-viewer-failure-text{flex:1 1 auto}.nxr-viewer-failure-close{border:0;background:transparent;color:inherit;font-size:18px;line-height:1;cursor:pointer}\n"], dependencies: [{ kind: "component", type: NxrReportPageComponent, selector: "nxr-report-page", inputs: ["page", "html", "css"] }, { kind: "component", type: NxrViewerToolbarComponent, selector: "nxr-viewer-toolbar", inputs: ["options", "pageIndex", "pageCount", "busy", "strings"], outputs: ["goToPage"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1160
+ }
1161
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrReportViewerComponent, decorators: [{
1162
+ type: Component,
1163
+ args: [{ selector: 'nxr-report-viewer', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NxrReportPageComponent, NxrViewerToolbarComponent], providers: [NxrViewerClientService, NxrViewerSessionService], template: `
1164
+ <div
1165
+ class="nxr-viewer-root"
1166
+ [style]="style()"
1167
+ [style.background-color]="rootBackgroundColor()"
1168
+ [style.width]="rootWidth()"
1169
+ [style.height]="rootHeight()"
1170
+ >
1171
+ @if (toolbarVisible()) {
1172
+ <nxr-viewer-toolbar
1173
+ [options]="session.options()"
1174
+ [pageIndex]="session.pageIndex()"
1175
+ [pageCount]="session.pageCount()"
1176
+ [busy]="session.busy()"
1177
+ (goToPage)="session.goToPage($event)"
1178
+ />
1179
+ }
1180
+
1181
+ <div
1182
+ class="nxr-viewer-pages"
1183
+ [style.text-align]="pageAlignment()"
1184
+ [style.overflow]="pagesOverflow()"
1185
+ >
1186
+ @if (session.shownPage(); as page) {
1187
+ <nxr-report-page
1188
+ class="nxr-viewer-page"
1189
+ [class.nxr-viewer-page-shadow]="showPageShadow()"
1190
+ [style.border]="pageBorder()"
1191
+ [page]="page.size"
1192
+ [html]="page.html"
1193
+ [css]="pageCss()"
1194
+ />
1195
+ }
1196
+ </div>
1197
+
1198
+ @if (session.failure(); as failure) {
1199
+ <div class="nxr-viewer-failure" role="alert">
1200
+ <span class="nxr-viewer-failure-text">{{ failure.message }}</span>
1201
+ <button type="button" class="nxr-viewer-failure-close" (click)="session.dismissFailure()">
1202
+ &times;
1203
+ </button>
1204
+ </div>
1205
+ }
1206
+ </div>
1207
+ `, styles: [":host{display:inline-block;position:relative}.nxr-viewer-root{display:flex;flex-direction:column;box-sizing:border-box;position:relative}.nxr-viewer-pages{flex:1 1 auto;min-height:0;padding:10px;box-sizing:border-box}.nxr-viewer-page{display:inline-block;vertical-align:top}.nxr-viewer-page-shadow{box-shadow:2px 2px 6px #0000004d}.nxr-viewer-failure{position:absolute;left:0;right:0;bottom:0;display:flex;align-items:center;gap:8px;padding:8px 12px;background:#fdecea;color:#611a15;font-size:13px}.nxr-viewer-failure-text{flex:1 1 auto}.nxr-viewer-failure-close{border:0;background:transparent;color:inherit;font-size:18px;line-height:1;cursor:pointer}\n"] }]
1208
+ }], ctorParameters: () => [], propDecorators: { requestUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "requestUrl", required: false }] }], action: [{ type: i0.Input, args: [{ isSignal: true, alias: "action", required: false }] }], properties: [{ type: i0.Input, args: [{ isSignal: true, alias: "properties", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], backgroundColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "backgroundColor", required: false }] }], style: [{ type: i0.Input, args: [{ isSignal: true, alias: "style", required: false }] }], postParametersFunction: [{ type: i0.Input, args: [{ isSignal: true, alias: "postParametersFunction", required: false }] }], loaded: [{ type: i0.Output, args: ["loaded"] }], error: [{ type: i0.Output, args: ["error"] }] } });
1209
+
1210
+ /*
1211
+ * Public API Surface of @nexterajs/report-viewer
1212
+ */
1213
+
1214
+ /**
1215
+ * Generated bundle index. Do not edit.
1216
+ */
1217
+
1218
+ export { NXR_VIEWER_PROTOCOL, NxrReportViewerComponent };
1219
+ //# sourceMappingURL=nexterajs-report-viewer.mjs.map