@nexterajs/report-viewer 0.0.1 → 0.1.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.
@@ -1,6 +1,6 @@
1
1
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
2
2
  import * as i0 from '@angular/core';
3
- import { input, inject, DOCUMENT, ElementRef, ChangeDetectionStrategy, Component, Injectable, signal, computed, DestroyRef, output } from '@angular/core';
3
+ import { input, computed, inject, DOCUMENT, ElementRef, ChangeDetectionStrategy, Component, Injectable, signal, DestroyRef, output, viewChild, linkedSignal } from '@angular/core';
4
4
  import { HttpErrorResponse, HttpClient } from '@angular/common/http';
5
5
  import { defer, map, catchError, from, switchMap, throwError, timeout, Subject, Subscription, finalize, interval, exhaustMap, EMPTY } from 'rxjs';
6
6
 
@@ -8,7 +8,7 @@ import { defer, map, catchError, from, switchMap, throwError, timeout, Subject,
8
8
  * The version of the viewer protocol this package speaks: the `protocol` member of every
9
9
  * `AngularViewerData` answer from `Nextera.Report.AspNetCore`.
10
10
  */
11
- const NXR_VIEWER_PROTOCOL = 1;
11
+ const NXE_VIEWER_PROTOCOL = 1;
12
12
 
13
13
  /**
14
14
  * Page HTML from the server, made safe to place in the application's document.
@@ -137,8 +137,19 @@ function adoptedSheet(cssText) {
137
137
  return sheet;
138
138
  }
139
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;}';
140
+ /**
141
+ * The viewer draws each page's frame and the space between pages, so the page CSS's own are cancelled.
142
+ *
143
+ * Zoom scales the page's own box from its top left corner. Stimulsoft re-renders the page on the server
144
+ * at the asked-for zoom and never scales on the client; this engine's page HTML is absolutely positioned
145
+ * in points, so scaling it here puts the same marks in the same places without a round trip, and the text
146
+ * is re-rasterised at the new size rather than resampled. The host element is sized to the scaled box so
147
+ * the pages around it lay out against what is on screen.
148
+ */
149
+ function pageFrameCss(zoom) {
150
+ const transform = zoom === 1 ? '' : `transform:scale(${zoom});transform-origin:top left;`;
151
+ return `.nextera-page{margin:0;box-shadow:none;${transform}}`;
152
+ }
142
153
  /**
143
154
  * One report page, in its own shadow root.
144
155
  *
@@ -147,7 +158,7 @@ const PAGE_FRAME_CSS = '.nextera-page{margin:0;box-shadow:none;}';
147
158
  * report's styles and the application's never meet. `contain: paint` keeps even a fixed-position
148
159
  * element inside the page's box.
149
160
  */
150
- class NxrReportPageComponent {
161
+ class NxeReportPageComponent {
151
162
  /** The page's number and size. */
152
163
  page = input.required(/* @ts-ignore */
153
164
  ...(ngDevMode ? [{ debugName: "page" }] : /* istanbul ignore next */ []));
@@ -157,13 +168,20 @@ class NxrReportPageComponent {
157
168
  /** The stylesheet every page of the report needs. */
158
169
  css = input.required(/* @ts-ignore */
159
170
  ...(ngDevMode ? [{ debugName: "css" }] : /* istanbul ignore next */ []));
171
+ /** The zoom to draw the page at, as a factor: 1 is 100 %. */
172
+ zoom = input(1, /* @ts-ignore */
173
+ ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
174
+ scaledWidth = computed(() => this.page().widthPoints * this.zoom(), /* @ts-ignore */
175
+ ...(ngDevMode ? [{ debugName: "scaledWidth" }] : /* istanbul ignore next */ []));
176
+ scaledHeight = computed(() => this.page().heightPoints * this.zoom(), /* @ts-ignore */
177
+ ...(ngDevMode ? [{ debugName: "scaledHeight" }] : /* istanbul ignore next */ []));
160
178
  document = inject(DOCUMENT);
161
179
  root = inject(ElementRef).nativeElement.attachShadow({
162
180
  mode: 'open',
163
181
  });
164
182
  ngOnChanges(changes) {
165
- if (changes['css']) {
166
- this.root.adoptedStyleSheets = [adoptedSheet(this.css()), adoptedSheet(PAGE_FRAME_CSS)];
183
+ if (changes['css'] || changes['zoom']) {
184
+ this.root.adoptedStyleSheets = [adoptedSheet(this.css()), adoptedSheet(pageFrameCss(this.zoom()))];
167
185
  }
168
186
  if (changes['html']) {
169
187
  const html = this.html();
@@ -175,26 +193,26 @@ class NxrReportPageComponent {
175
193
  }
176
194
  }
177
195
  }
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 });
196
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeReportPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
197
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxeReportPageComponent, isStandalone: true, selector: "nxe-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 }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-page": "page().pageNumber", "style.width.pt": "scaledWidth()", "style.height.pt": "scaledHeight()" }, classAttribute: "nxe-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
198
  }
181
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrReportPageComponent, decorators: [{
199
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeReportPageComponent, decorators: [{
182
200
  type: Component,
183
- args: [{ selector: 'nxr-report-page', standalone: true, template: '', changeDetection: ChangeDetectionStrategy.OnPush, host: {
184
- class: 'nxr-report-page',
201
+ args: [{ selector: 'nxe-report-page', standalone: true, template: '', changeDetection: ChangeDetectionStrategy.OnPush, host: {
202
+ class: 'nxe-report-page',
185
203
  '[attr.data-page]': 'page().pageNumber',
186
- '[style.width.pt]': 'page().widthPoints',
187
- '[style.height.pt]': 'page().heightPoints',
204
+ '[style.width.pt]': 'scaledWidth()',
205
+ '[style.height.pt]': 'scaledHeight()',
188
206
  }, 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 }] }] } });
207
+ }], 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 }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }] } });
190
208
 
191
209
  /**
192
210
  * Every caption and message the viewer shows, in its built-in English. A key in PascalCase is the name
193
211
  * Stimulsoft's viewer gives the same text, such as `PageOf`; a key in camelCase names a message only this
194
212
  * 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.
213
+ * of them, so code reads them from an `NxeViewerStrings` value wherever one is at hand.
196
214
  */
197
- const NXR_ENGLISH = {
215
+ const NXE_ENGLISH = {
198
216
  /** The label before the page-number box. */
199
217
  Page: 'Page',
200
218
  /** The label between the page-number box and the page count. */
@@ -203,6 +221,18 @@ const NXR_ENGLISH = {
203
221
  PrevPage: 'Previous page',
204
222
  NextPage: 'Next page',
205
223
  LastPage: 'Last page',
224
+ /** The save button's caption and tooltip; its menu items are captioned by the format they write. */
225
+ Save: 'Save',
226
+ SaveToolTip: 'Save',
227
+ /** The view-mode button's tooltip, and its menu items, whose captions are the mode names. */
228
+ ViewModeToolTip: 'View mode',
229
+ SinglePage: 'Single Page',
230
+ Continuous: 'Continuous',
231
+ MultiplePages: 'Multiple Pages',
232
+ /** The zoom button's tooltip; its caption is the current percentage, so it has no key. */
233
+ ZoomToolTip: 'Zoom',
234
+ ZoomOnePage: 'One Page',
235
+ ZoomPageWidth: 'Page Width',
206
236
  errorUnreachable: 'The report server could not be reached.',
207
237
  errorStatus: 'The report server answered with HTTP status {0}.',
208
238
  errorTimeout: 'The report server did not answer within {0} seconds.',
@@ -218,25 +248,25 @@ function formatString(template, ...values) {
218
248
  * A success answer that is not what the protocol says its action answers: the wrong shape, or an
219
249
  * AngularViewerData answer from another version of the protocol.
220
250
  */
221
- class NxrMalformedAnswerError extends Error {
251
+ class NxeMalformedAnswerError extends Error {
222
252
  action;
223
253
  protocol;
224
254
  constructor(action,
225
255
  /** 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.`);
256
+ protocol = NXE_VIEWER_PROTOCOL) {
257
+ super(`The ${action} answer is not what viewer protocol ${NXE_VIEWER_PROTOCOL} answers.`);
228
258
  this.action = action;
229
259
  this.protocol = protocol;
230
- this.name = 'NxrMalformedAnswerError';
260
+ this.name = 'NxeMalformedAnswerError';
231
261
  }
232
262
  }
233
263
  /** A request that got no answer within the server options' `requestTimeout`. */
234
- class NxrTimeoutError extends Error {
264
+ class NxeTimeoutError extends Error {
235
265
  seconds;
236
266
  constructor(seconds) {
237
267
  super(`No answer within ${seconds} seconds.`);
238
268
  this.seconds = seconds;
239
- this.name = 'NxrTimeoutError';
269
+ this.name = 'NxeTimeoutError';
240
270
  }
241
271
  }
242
272
  const OPTION_GROUPS = ['actions', 'appearance', 'toolbar', 'exports', 'server'];
@@ -279,25 +309,25 @@ function isLocated(value, textKey) {
279
309
  /** Reads the answer to AngularViewerData: the token, the viewer options and the host's translation. */
280
310
  function readViewerData(body) {
281
311
  if (!isObject(body))
282
- throw new NxrMalformedAnswerError('AngularViewerData');
312
+ throw new NxeMalformedAnswerError('AngularViewerData');
283
313
  const { protocol, token, options, localization } = body;
284
- if (isNumber(protocol) && protocol !== NXR_VIEWER_PROTOCOL) {
285
- throw new NxrMalformedAnswerError('AngularViewerData', protocol);
314
+ if (isNumber(protocol) && protocol !== NXE_VIEWER_PROTOCOL) {
315
+ throw new NxeMalformedAnswerError('AngularViewerData', protocol);
286
316
  }
287
- const valid = protocol === NXR_VIEWER_PROTOCOL &&
317
+ const valid = protocol === NXE_VIEWER_PROTOCOL &&
288
318
  typeof token === 'string' &&
289
319
  token !== '' &&
290
320
  isObject(options) &&
291
321
  OPTION_GROUPS.every((group) => isObject(options[group])) &&
292
322
  (localization === null || isObject(localization));
293
323
  if (!valid)
294
- throw new NxrMalformedAnswerError('AngularViewerData');
324
+ throw new NxeMalformedAnswerError('AngularViewerData');
295
325
  return body;
296
326
  }
297
327
  /** Reads the answer to GetReport: the report's name, page count, page sizes, bookmarks and page stylesheet. */
298
328
  function readReport(body) {
299
329
  if (!isObject(body))
300
- throw new NxrMalformedAnswerError('GetReport');
330
+ throw new NxeMalformedAnswerError('GetReport');
301
331
  const { reportName, pageCount, pages, bookmarks, pageCss, pagesHtml } = body;
302
332
  const valid = typeof reportName === 'string' &&
303
333
  isIndex(pageCount) &&
@@ -309,7 +339,7 @@ function readReport(body) {
309
339
  typeof pageCss === 'string' &&
310
340
  (pagesHtml === undefined || isPages(pagesHtml));
311
341
  if (!valid)
312
- throw new NxrMalformedAnswerError('GetReport');
342
+ throw new NxeMalformedAnswerError('GetReport');
313
343
  return {
314
344
  ...body,
315
345
  pagesHtml: pagesHtml ?? null,
@@ -318,20 +348,20 @@ function readReport(body) {
318
348
  /** Reads the answer to GetPages. */
319
349
  function readPages(body) {
320
350
  if (!isPages(body))
321
- throw new NxrMalformedAnswerError('GetPages');
351
+ throw new NxeMalformedAnswerError('GetPages');
322
352
  return body;
323
353
  }
324
354
  /** Reads the answer to Find. */
325
355
  function readFind(body) {
326
356
  const hits = isObject(body) ? body['hits'] : undefined;
327
357
  if (!Array.isArray(hits) || !hits.every((hit) => isLocated(hit, 'text')))
328
- throw new NxrMalformedAnswerError('Find');
358
+ throw new NxeMalformedAnswerError('Find');
329
359
  return body;
330
360
  }
331
361
  /** Reads the answer to UpdateCache. */
332
362
  function readUpdateCache(body) {
333
363
  if (!isObject(body) || typeof body['updated'] !== 'boolean')
334
- throw new NxrMalformedAnswerError('UpdateCache');
364
+ throw new NxeMalformedAnswerError('UpdateCache');
335
365
  return body;
336
366
  }
337
367
  function described(status, message) {
@@ -345,21 +375,21 @@ function described(status, message) {
345
375
  * - a timeout;
346
376
  * - a success answer the viewer cannot read.
347
377
  */
348
- function readFailure(failure, action, strings = NXR_ENGLISH) {
378
+ function readFailure(failure, action, strings = NXE_ENGLISH) {
349
379
  if (failure instanceof HttpErrorResponse) {
350
380
  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;
381
+ // status and message are all that is asked for. The rest of the protocol's error answer is taken when the server
382
+ // sent it and defaulted when it did not, because a host that writes its own error -- bright answers a failed
383
+ // GetReport with {status, message} from its controller's catch -- says the one thing the reader cannot work out
384
+ // for itself, and demanding type and retryable alongside it threw that sentence away and left the viewer showing
385
+ // the status code on its own.
386
+ if (isObject(body) && body['status'] === 'error' && typeof body['message'] === 'string') {
357
387
  return {
358
388
  status: failure.status,
359
- type: body['type'],
389
+ type: typeof body['type'] === 'string' ? body['type'] : 'Error',
360
390
  message: body['message'],
361
- retryable: body['retryable'],
362
- traceId,
391
+ retryable: typeof body['retryable'] === 'boolean' ? body['retryable'] : false,
392
+ traceId: typeof body['traceId'] === 'string' ? body['traceId'] : null,
363
393
  };
364
394
  }
365
395
  if (failure.status === 0)
@@ -368,12 +398,12 @@ function readFailure(failure, action, strings = NXR_ENGLISH) {
368
398
  return described(0, formatString(strings.errorMalformed, action));
369
399
  return described(failure.status, formatString(strings.errorStatus, failure.status));
370
400
  }
371
- if (failure instanceof NxrTimeoutError) {
401
+ if (failure instanceof NxeTimeoutError) {
372
402
  return described(0, formatString(strings.errorTimeout, failure.seconds));
373
403
  }
374
- if (failure instanceof NxrMalformedAnswerError) {
375
- return failure.protocol !== NXR_VIEWER_PROTOCOL
376
- ? described(0, formatString(strings.errorProtocol, failure.protocol, NXR_VIEWER_PROTOCOL))
404
+ if (failure instanceof NxeMalformedAnswerError) {
405
+ return failure.protocol !== NXE_VIEWER_PROTOCOL
406
+ ? described(0, formatString(strings.errorProtocol, failure.protocol, NXE_VIEWER_PROTOCOL))
377
407
  : described(0, formatString(strings.errorMalformed, failure.action));
378
408
  }
379
409
  return described(0, failure instanceof Error ? failure.message : String(failure));
@@ -383,7 +413,7 @@ function readFailure(failure, action, strings = NXR_ENGLISH) {
383
413
  * The seconds a request may take when no positive `requestTimeout` applies. That includes the first
384
414
  * request, which is sent before the server's options are known. Stimulsoft's viewer uses the same value.
385
415
  */
386
- const NXR_UNSET_REQUEST_TIMEOUT_SECONDS = 1000;
416
+ const NXE_UNSET_REQUEST_TIMEOUT_SECONDS = 1000;
387
417
  /**
388
418
  * The URL of a host action. The action's path takes the place of `{action}` in `requestUrl`, and a query
389
419
  * string on the action is appended to the URL's own.
@@ -429,7 +459,7 @@ function fileNameOf(disposition) {
429
459
  }
430
460
  function timeoutOf(session) {
431
461
  const seconds = session?.options.server.requestTimeout ?? 0;
432
- return seconds > 0 ? seconds : NXR_UNSET_REQUEST_TIMEOUT_SECONDS;
462
+ return seconds > 0 ? seconds : NXE_UNSET_REQUEST_TIMEOUT_SECONDS;
433
463
  }
434
464
  function formOf(context, action, session, parameters) {
435
465
  const form = new FormData();
@@ -473,10 +503,10 @@ async function readableFailure(failure) {
473
503
  }
474
504
  /**
475
505
  * 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
506
+ * interceptors apply, and reads each answer. Every observable fails with an `NxeViewerFailure`. A request's
477
507
  * form is built when the observable is subscribed.
478
508
  */
479
- class NxrViewerClientService {
509
+ class NxeViewerClientService {
480
510
  http = inject(HttpClient);
481
511
  /** AngularViewerData, sent to the component's `action`: the token and the options. */
482
512
  openViewer(context, action) {
@@ -516,30 +546,46 @@ class NxrViewerClientService {
516
546
  const seconds = timeoutOf(session);
517
547
  return defer(() => this.http.post(actionUrl(context.requestUrl, hostAction), formOf(context, action, session, parameters))).pipe(timeout({
518
548
  first: seconds * 1000,
519
- with: () => throwError(() => new NxrTimeoutError(seconds)),
549
+ with: () => throwError(() => new NxeTimeoutError(seconds)),
520
550
  }), map((body) => read(body)), catchError((failure) => throwError(() => readFailure(failure, action, context.strings))));
521
551
  }
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 });
552
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerClientService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
553
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerClientService });
524
554
  }
525
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrViewerClientService, decorators: [{
555
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerClientService, decorators: [{
526
556
  type: Injectable
527
557
  }] });
528
558
 
529
559
  /** 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;
560
+ const NXE_KEEP_ALIVE_MS = 180_000;
531
561
  function withPages(pages, answer) {
532
562
  answer?.pages.forEach((html, offset) => pages.set(answer.startIndex + offset, html));
533
563
  return pages;
534
564
  }
565
+ /**
566
+ * Hands a file to the browser to save, under the name the server gave it. A blob URL is used rather
567
+ * than a data one because an exported report is far larger than a URL may be, and it is revoked on
568
+ * the next turn of the event loop, once the click has been taken.
569
+ */
570
+ function saveBlob(blob, fileName) {
571
+ const url = URL.createObjectURL(blob);
572
+ const link = document.createElement('a');
573
+ link.href = url;
574
+ link.download = fileName;
575
+ link.style.display = 'none';
576
+ document.body.append(link);
577
+ link.click();
578
+ link.remove();
579
+ setTimeout(() => URL.revokeObjectURL(url), 0);
580
+ }
535
581
  /**
536
582
  * One viewer's conversation with the report server, and the state the viewer shows.
537
583
  *
538
584
  * Every signal changes only in this service's methods and in the answers they handle; nothing reacts to
539
585
  * a signal changing. Each viewer provides its own session, so viewers on one page never share state.
540
586
  */
541
- class NxrViewerSessionService {
542
- client = inject(NxrViewerClientService);
587
+ class NxeViewerSessionService {
588
+ client = inject(NxeViewerClientService);
543
589
  optionsState = signal(null, /* @ts-ignore */
544
590
  ...(ngDevMode ? [{ debugName: "optionsState" }] : /* istanbul ignore next */ []));
545
591
  reportState = signal(null, /* @ts-ignore */
@@ -575,6 +621,19 @@ class NxrViewerSessionService {
575
621
  return { index, size: report.pages[index], html: this.htmlState().get(index) ?? null };
576
622
  }, /* @ts-ignore */
577
623
  ...(ngDevMode ? [{ debugName: "shownPage" }] : /* istanbul ignore next */ []));
624
+ /**
625
+ * Every page of the report, in order, each with its HTML when it has arrived. The page modes that show
626
+ * more than one page at a time lay these out and ask for the ones that come into view; a page holds its
627
+ * size, and so its place, before its HTML is here.
628
+ */
629
+ allPages = computed(() => {
630
+ const report = this.reportState();
631
+ if (!report)
632
+ return [];
633
+ const html = this.htmlState();
634
+ return report.pages.map((size, index) => ({ index, size, html: html.get(index) ?? null }));
635
+ }, /* @ts-ignore */
636
+ ...(ngDevMode ? [{ debugName: "allPages" }] : /* istanbul ignore next */ []));
578
637
  /** Emits after each successful GetReport answer. */
579
638
  reportLoaded = new Subject();
580
639
  /** Emits each failure put on show. */
@@ -629,6 +688,34 @@ class NxrViewerSessionService {
629
688
  this.pageIndexState.set(index);
630
689
  this.loadPage(index);
631
690
  }
691
+ /**
692
+ * Asks for a page's HTML without moving the page on screen, for the modes that show several pages at
693
+ * once and fetch the ones that come into view. A page already here, or already on its way, is left alone.
694
+ */
695
+ requestPage(index) {
696
+ if (this.destroyed || !Number.isInteger(index) || index < 0)
697
+ return;
698
+ this.loadPage(index);
699
+ }
700
+ /** Moves the page on screen without asking for it, for scroll tracking, which follows what is drawn. */
701
+ markCurrentPage(index) {
702
+ if (this.destroyed || !Number.isInteger(index) || index < 0 || index >= this.pageCount())
703
+ return;
704
+ this.pageIndexState.set(index);
705
+ }
706
+ /**
707
+ * ExportReport: the report as a file, handed to the browser to save. The export never times out, so
708
+ * it is tracked as a request the user waits for and the toolbar stays disabled until the file is here.
709
+ */
710
+ exportTo(format) {
711
+ if (this.destroyed || this.session === null || this.context === null)
712
+ return;
713
+ const { context, session } = this.connection();
714
+ this.track(this.client.exportReport(context, session, format), false, (answer) => {
715
+ const name = answer.fileName || `${this.reportState()?.reportName ?? 'report'}.${format.toLowerCase()}`;
716
+ saveBlob(answer.blob, name);
717
+ });
718
+ }
632
719
  /** Takes the failure off show. */
633
720
  dismissFailure() {
634
721
  this.failureState.set(null);
@@ -700,7 +787,7 @@ class NxrViewerSessionService {
700
787
  const { allowAutoUpdateCache, cacheMode } = session.options.server;
701
788
  if (!allowAutoUpdateCache || cacheMode === 'None')
702
789
  return;
703
- this.keepAlive = interval(NXR_KEEP_ALIVE_MS)
790
+ this.keepAlive = interval(NXE_KEEP_ALIVE_MS)
704
791
  .pipe(exhaustMap(() => this.client.updateCache(context, session).pipe(catchError(() => EMPTY))))
705
792
  .subscribe();
706
793
  }
@@ -719,15 +806,19 @@ class NxrViewerSessionService {
719
806
  throw new Error('The viewer has no report open.');
720
807
  return { context: this.context, session: this.session };
721
808
  }
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 });
809
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerSessionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
810
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerSessionService });
724
811
  }
725
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrViewerSessionService, decorators: [{
812
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerSessionService, decorators: [{
726
813
  type: Injectable
727
814
  }], ctorParameters: () => [] });
728
815
 
729
816
  /** The width of the page-number box, as Stimulsoft's page control sizes it. */
730
817
  const PAGE_BOX_WIDTH_PX = 45;
818
+ /** The zoom menu's fixed percentages, in Stimulsoft's order, above the two fit items. */
819
+ const ZOOM_PRESETS = [25, 50, 75, 100, 150, 200];
820
+ /** The view-mode menu's items, in Stimulsoft's order. */
821
+ const VIEW_MODES = ['SinglePage', 'Continuous', 'MultiplePages'];
731
822
  /**
732
823
  * The toolbar's page navigation.
733
824
  *
@@ -742,7 +833,7 @@ const PAGE_BOX_WIDTH_PX = 45;
742
833
  * report it would move past, and everything is disabled while a request the user waits for is on
743
834
  * its way.
744
835
  */
745
- class NxrViewerToolbarComponent {
836
+ class NxeViewerToolbarComponent {
746
837
  /** The options from the server, or null before the first answer, when the toolbar shows nothing. */
747
838
  options = input(null, /* @ts-ignore */
748
839
  ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
@@ -756,11 +847,62 @@ class NxrViewerToolbarComponent {
756
847
  busy = input(false, /* @ts-ignore */
757
848
  ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
758
849
  /** The captions to show. */
759
- strings = input(NXR_ENGLISH, /* @ts-ignore */
850
+ strings = input(NXE_ENGLISH, /* @ts-ignore */
760
851
  ...(ngDevMode ? [{ debugName: "strings" }] : /* istanbul ignore next */ []));
852
+ /** The mode the pages are shown in. */
853
+ viewMode = input('SinglePage', /* @ts-ignore */
854
+ ...(ngDevMode ? [{ debugName: "viewMode" }] : /* istanbul ignore next */ []));
855
+ /** The zoom the pages are drawn at, as a factor: 1 is 100 %. */
856
+ zoom = input(1, /* @ts-ignore */
857
+ ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
761
858
  /** The 0-based page the user asked for. */
762
859
  goToPage = output();
860
+ /** The mode the user chose. */
861
+ viewModeChange = output();
862
+ /** The zoom the user chose, as a factor. */
863
+ zoomChange = output();
864
+ /** The user asked to fit a whole page, or the page's width, in the panel. */
865
+ fitRequest = output();
866
+ /** The format the user asked the report to be saved as. */
867
+ saveRequest = output();
763
868
  pageBoxWidth = PAGE_BOX_WIDTH_PX;
869
+ zoomPresets = ZOOM_PRESETS;
870
+ viewModes = VIEW_MODES;
871
+ /** Which menu is open, or null. Stimulsoft opens one at a time and closes it on the next click. */
872
+ openMenuState = signal(null, /* @ts-ignore */
873
+ ...(ngDevMode ? [{ debugName: "openMenuState" }] : /* istanbul ignore next */ []));
874
+ openMenu = this.openMenuState.asReadonly();
875
+ /**
876
+ * The save menu's formats, in the order Stimulsoft lists them, and only those the server turned on.
877
+ * The button itself is left out when no format is on, as Stimulsoft forces it off then.
878
+ */
879
+ saveFormats = computed(() => {
880
+ const exports = this.options()?.exports;
881
+ if (!exports)
882
+ return [];
883
+ const formats = [];
884
+ if (exports.showExportToPdf)
885
+ formats.push('Pdf');
886
+ if (exports.showExportToHtml)
887
+ formats.push('Html');
888
+ return formats;
889
+ }, /* @ts-ignore */
890
+ ...(ngDevMode ? [{ debugName: "saveFormats" }] : /* istanbul ignore next */ []));
891
+ showSave = computed(() => this.toolbar()?.showSaveButton === true && this.saveFormats().length > 0, /* @ts-ignore */
892
+ ...(ngDevMode ? [{ debugName: "showSave" }] : /* istanbul ignore next */ []));
893
+ save(format) {
894
+ this.openMenuState.set(null);
895
+ this.saveRequest.emit(format);
896
+ }
897
+ /**
898
+ * The zoom as a rounded percentage, as Stimulsoft captions the button, and empty when the zoom is
899
+ * unknown or zero.
900
+ */
901
+ zoomCaption = computed(() => {
902
+ const zoom = this.zoom();
903
+ return Number.isFinite(zoom) && zoom > 0 ? `${Math.round(zoom * 100)}%` : '';
904
+ }, /* @ts-ignore */
905
+ ...(ngDevMode ? [{ debugName: "zoomCaption" }] : /* istanbul ignore next */ []));
764
906
  toolbar = computed(() => this.options()?.toolbar ?? null, /* @ts-ignore */
765
907
  ...(ngDevMode ? [{ debugName: "toolbar" }] : /* istanbul ignore next */ []));
766
908
  appearance = computed(() => this.options()?.appearance ?? null, /* @ts-ignore */
@@ -791,6 +933,30 @@ class NxrViewerToolbarComponent {
791
933
  ...(ngDevMode ? [{ debugName: "showNextPage" }] : /* istanbul ignore next */ []));
792
934
  showLastPage = computed(() => this.toolbar()?.showLastPageButton === true, /* @ts-ignore */
793
935
  ...(ngDevMode ? [{ debugName: "showLastPage" }] : /* istanbul ignore next */ []));
936
+ showZoom = computed(() => this.toolbar()?.showZoomButton === true, /* @ts-ignore */
937
+ ...(ngDevMode ? [{ debugName: "showZoom" }] : /* istanbul ignore next */ []));
938
+ showViewMode = computed(() => this.toolbar()?.showViewModeButton === true, /* @ts-ignore */
939
+ ...(ngDevMode ? [{ debugName: "showViewMode" }] : /* istanbul ignore next */ []));
940
+ /** Whether a preset is the zoom on show, so its menu item reads as chosen. */
941
+ isZoom(percent) {
942
+ return Math.round(this.zoom() * 100) === percent;
943
+ }
944
+ /** Opens a menu, or closes it when it is the one already open. */
945
+ toggleMenu(menu) {
946
+ this.openMenuState.update((open) => (open === menu ? null : menu));
947
+ }
948
+ chooseZoom(percent) {
949
+ this.openMenuState.set(null);
950
+ this.zoomChange.emit(percent / 100);
951
+ }
952
+ fit(what) {
953
+ this.openMenuState.set(null);
954
+ this.fitRequest.emit(what);
955
+ }
956
+ chooseViewMode(mode) {
957
+ this.openMenuState.set(null);
958
+ this.viewModeChange.emit(mode);
959
+ }
794
960
  atStart = computed(() => this.busy() || this.navigationDisabled() || this.pageIndex() <= 0, /* @ts-ignore */
795
961
  ...(ngDevMode ? [{ debugName: "atStart" }] : /* istanbul ignore next */ []));
796
962
  atEnd = computed(() => this.busy() || this.navigationDisabled() || this.pageIndex() >= this.pageCount() - 1, /* @ts-ignore */
@@ -838,12 +1004,53 @@ class NxrViewerToolbarComponent {
838
1004
  return;
839
1005
  this.goToPage.emit(index);
840
1006
  }
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: `
1007
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1008
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeViewerToolbarComponent, isStandalone: true, selector: "nxe-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 }, viewMode: { classPropertyName: "viewMode", publicName: "viewMode", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { goToPage: "goToPage", viewModeChange: "viewModeChange", zoomChange: "zoomChange", fitRequest: "fitRequest", saveRequest: "saveRequest" }, host: { attributes: { "role": "toolbar" }, properties: { "style.background-color": "backgroundColor()", "style.border": "border()", "style.color": "fontColor()", "style.font-family": "fontFamily()", "style.justify-content": "justifyContent()", "class.nxe-busy": "busy()" }, classAttribute: "nxe-viewer-toolbar" }, ngImport: i0, template: `
1009
+ @if (showSave()) {
1010
+ <div class="nxe-toolbar-menu">
1011
+ <button
1012
+ type="button"
1013
+ class="nxe-toolbar-button nxe-toolbar-captioned"
1014
+ [attr.aria-label]="strings().SaveToolTip"
1015
+ [attr.aria-expanded]="openMenu() === 'save'"
1016
+ (click)="toggleMenu('save')"
1017
+ >
1018
+ <svg viewBox="0 0 16 16" aria-hidden="true">
1019
+ <path
1020
+ d="M2 2h9.5L14 4.5V14H2zm2.5 0v4.5h6V2zm-1 7.5h9V14h-9zM8.5 3h1.5v2.5H8.5z"
1021
+ />
1022
+ </svg>
1023
+ <span class="nxe-toolbar-caption">{{ strings().Save }}</span>
1024
+ <svg class="nxe-toolbar-arrow" viewBox="0 0 8 8" aria-hidden="true">
1025
+ <path d="M1 3h6L4 6.2z" />
1026
+ </svg>
1027
+ </button>
1028
+ @if (openMenu() === 'save') {
1029
+ <div class="nxe-toolbar-popup" role="menu">
1030
+ @for (format of saveFormats(); track format) {
1031
+ <button
1032
+ type="button"
1033
+ class="nxe-toolbar-item"
1034
+ role="menuitem"
1035
+ (click)="save(format)"
1036
+ >
1037
+ <span class="nxe-toolbar-item-icon">
1038
+ <svg viewBox="0 0 16 16" aria-hidden="true">
1039
+ <path d="M3 1h7l3 3v11H3zm6.6 1.2v2.2h2.2z" />
1040
+ </svg>
1041
+ </span>
1042
+ {{ format }}
1043
+ </button>
1044
+ }
1045
+ </div>
1046
+ }
1047
+ </div>
1048
+ <span class="nxe-toolbar-separator"></span>
1049
+ }
843
1050
  @if (showFirstPage()) {
844
1051
  <button
845
1052
  type="button"
846
- class="nxr-toolbar-button"
1053
+ class="nxe-toolbar-button"
847
1054
  [disabled]="atStart()"
848
1055
  [attr.aria-label]="strings().FirstPage"
849
1056
  (click)="goTo(0)"
@@ -856,7 +1063,7 @@ class NxrViewerToolbarComponent {
856
1063
  @if (showPreviousPage()) {
857
1064
  <button
858
1065
  type="button"
859
- class="nxr-toolbar-button"
1066
+ class="nxe-toolbar-button"
860
1067
  [disabled]="atStart()"
861
1068
  [attr.aria-label]="strings().PrevPage"
862
1069
  (click)="goTo(pageIndex() - 1)"
@@ -865,25 +1072,25 @@ class NxrViewerToolbarComponent {
865
1072
  </button>
866
1073
  }
867
1074
  @if (showCurrentPage()) {
868
- <span class="nxr-page-control">
869
- <span class="nxr-page-label">{{ strings().Page }}</span>
1075
+ <span class="nxe-page-control">
1076
+ <span class="nxe-page-label">{{ strings().Page }}</span>
870
1077
  <input
871
1078
  type="text"
872
1079
  inputmode="numeric"
873
- class="nxr-page-box"
1080
+ class="nxe-page-box"
874
1081
  [style.width.px]="pageBoxWidth"
875
1082
  [disabled]="pageControlDisabled()"
876
1083
  [value]="pageIndex() + 1"
877
1084
  [attr.aria-label]="strings().Page"
878
1085
  (keydown.enter)="enterPage($event)"
879
1086
  />
880
- <span class="nxr-page-label">{{ strings().PageOf }} {{ pageCount() }}</span>
1087
+ <span class="nxe-page-label">{{ strings().PageOf }} {{ pageCount() }}</span>
881
1088
  </span>
882
1089
  }
883
1090
  @if (showNextPage()) {
884
1091
  <button
885
1092
  type="button"
886
- class="nxr-toolbar-button"
1093
+ class="nxe-toolbar-button"
887
1094
  [disabled]="atEnd()"
888
1095
  [attr.aria-label]="strings().NextPage"
889
1096
  (click)="goTo(pageIndex() + 1)"
@@ -894,7 +1101,7 @@ class NxrViewerToolbarComponent {
894
1101
  @if (showLastPage()) {
895
1102
  <button
896
1103
  type="button"
897
- class="nxr-toolbar-button"
1104
+ class="nxe-toolbar-button"
898
1105
  [disabled]="atEnd()"
899
1106
  [attr.aria-label]="strings().LastPage"
900
1107
  (click)="goTo(pageCount() - 1)"
@@ -904,24 +1111,166 @@ class NxrViewerToolbarComponent {
904
1111
  </svg>
905
1112
  </button>
906
1113
  }
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 });
1114
+ @if (showZoom() || showViewMode()) {
1115
+ <span class="nxe-toolbar-separator"></span>
1116
+ }
1117
+ @if (showZoom()) {
1118
+ <div class="nxe-toolbar-menu">
1119
+ <button
1120
+ type="button"
1121
+ class="nxe-toolbar-button nxe-toolbar-captioned"
1122
+ [attr.aria-label]="strings().ZoomToolTip"
1123
+ [attr.aria-expanded]="openMenu() === 'zoom'"
1124
+ (click)="toggleMenu('zoom')"
1125
+ >
1126
+ <svg viewBox="0 0 16 16" aria-hidden="true">
1127
+ <path
1128
+ d="M3 2h7l3 3v9H3zm6.6 1.2v2.2h2.2zM5 8h6v1.1H5zm0 2.4h6v1.1H5z"
1129
+ />
1130
+ </svg>
1131
+ <span class="nxe-toolbar-caption">{{ zoomCaption() }}</span>
1132
+ <svg class="nxe-toolbar-arrow" viewBox="0 0 8 8" aria-hidden="true">
1133
+ <path d="M1 3h6L4 6.2z" />
1134
+ </svg>
1135
+ </button>
1136
+ @if (openMenu() === 'zoom') {
1137
+ <div class="nxe-toolbar-popup" role="menu">
1138
+ @for (percent of zoomPresets; track percent) {
1139
+ <button
1140
+ type="button"
1141
+ class="nxe-toolbar-item"
1142
+ role="menuitem"
1143
+ [class.nxe-toolbar-item-on]="isZoom(percent)"
1144
+ (click)="chooseZoom(percent)"
1145
+ >
1146
+ <span class="nxe-toolbar-item-icon"></span>{{ percent }}%
1147
+ </button>
1148
+ }
1149
+ <span class="nxe-toolbar-item-separator"></span>
1150
+ <button type="button" class="nxe-toolbar-item" role="menuitem" (click)="fit('page')">
1151
+ <span class="nxe-toolbar-item-icon">
1152
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 2h10v12H3zm1.2 1.2v9.6h7.6V3.2z" /></svg>
1153
+ </span>
1154
+ {{ strings().ZoomOnePage }}
1155
+ </button>
1156
+ <button type="button" class="nxe-toolbar-item" role="menuitem" (click)="fit('width')">
1157
+ <span class="nxe-toolbar-item-icon">
1158
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M1 6h14v4H1zm1.2 1.2v1.6h11.6V7.2z" /></svg>
1159
+ </span>
1160
+ {{ strings().ZoomPageWidth }}
1161
+ </button>
1162
+ </div>
1163
+ }
1164
+ </div>
1165
+ }
1166
+ @if (showViewMode()) {
1167
+ <div class="nxe-toolbar-menu">
1168
+ <button
1169
+ type="button"
1170
+ class="nxe-toolbar-button nxe-toolbar-captioned"
1171
+ [attr.aria-label]="strings().ViewModeToolTip"
1172
+ [attr.aria-expanded]="openMenu() === 'viewMode'"
1173
+ (click)="toggleMenu('viewMode')"
1174
+ >
1175
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 2h10v12H3zm1.2 1.2v9.6h7.6V3.2z" /></svg>
1176
+ <span class="nxe-toolbar-caption">{{ strings()[viewMode()] }}</span>
1177
+ <svg class="nxe-toolbar-arrow" viewBox="0 0 8 8" aria-hidden="true">
1178
+ <path d="M1 3h6L4 6.2z" />
1179
+ </svg>
1180
+ </button>
1181
+ @if (openMenu() === 'viewMode') {
1182
+ <div class="nxe-toolbar-popup" role="menu">
1183
+ @for (mode of viewModes; track mode) {
1184
+ <button
1185
+ type="button"
1186
+ class="nxe-toolbar-item"
1187
+ role="menuitem"
1188
+ [class.nxe-toolbar-item-on]="viewMode() === mode"
1189
+ (click)="chooseViewMode(mode)"
1190
+ >
1191
+ <span class="nxe-toolbar-item-icon">
1192
+ @switch (mode) {
1193
+ @case ('SinglePage') {
1194
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 2h10v12H3zm1.2 1.2v9.6h7.6V3.2z" /></svg>
1195
+ }
1196
+ @case ('Continuous') {
1197
+ <svg viewBox="0 0 16 16" aria-hidden="true">
1198
+ <path d="M3 1h10v6H3zm1.2 1.2v3.6h7.6V2.2zM3 9h10v6H3zm1.2 1.2v3.6h7.6v-3.6z" />
1199
+ </svg>
1200
+ }
1201
+ @case ('MultiplePages') {
1202
+ <svg viewBox="0 0 16 16" aria-hidden="true">
1203
+ <path d="M2 2h5v5H2zm7 0h5v5H9zM2 9h5v5H2zm7 0h5v5H9z" />
1204
+ </svg>
1205
+ }
1206
+ }
1207
+ </span>
1208
+ {{ strings()[mode] }}
1209
+ </button>
1210
+ }
1211
+ </div>
1212
+ }
1213
+ </div>
1214
+ }
1215
+ `, 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(.nxe-busy):after{content:\"\";position:absolute;inset:0;cursor:default}.nxe-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}.nxe-toolbar-button:hover:not(:disabled){background:#00000014}.nxe-toolbar-button:disabled{opacity:.5;cursor:default}.nxe-toolbar-button svg{width:16px;height:16px;fill:currentColor}.nxe-page-control{display:inline-flex;align-items:center;gap:4px;padding:0 4px;white-space:nowrap}.nxe-page-box{height:21px;box-sizing:border-box;text-align:center;font:inherit;color:inherit}.nxe-page-box:disabled{color:inherit;-webkit-text-fill-color:currentColor;opacity:1}.nxe-toolbar-separator{width:1px;height:18px;margin:0 4px;background:currentColor;opacity:.25}.nxe-toolbar-menu{position:relative;display:flex;align-items:center}.nxe-toolbar-captioned{width:auto;gap:4px;padding:0 6px}.nxe-toolbar-caption{font-size:12px;white-space:nowrap}.nxe-toolbar-arrow{width:8px;height:8px;fill:currentColor;flex:none}.nxe-toolbar-popup{position:absolute;top:100%;left:0;z-index:10;min-width:150px;margin-top:2px;padding:3px 0;background:#fff;color:#000;border:1px solid #c8c8c8;box-shadow:0 2px 8px #00000040;display:flex;flex-direction:column}.nxe-toolbar-item{display:flex;align-items:center;gap:8px;border:0;background:transparent;color:inherit;font:inherit;font-size:12px;text-align:left;white-space:nowrap;padding:5px 14px 5px 6px;cursor:pointer}.nxe-toolbar-item:hover{background:#00000012}.nxe-toolbar-item-on{background:#0000001c}.nxe-toolbar-item-icon{width:16px;height:16px;flex:none;display:inline-flex;align-items:center;justify-content:center}.nxe-toolbar-item-icon svg{width:16px;height:16px;fill:currentColor}.nxe-toolbar-item-separator{height:1px;margin:3px 0;background:#c8c8c8}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
908
1216
  }
909
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrViewerToolbarComponent, decorators: [{
1217
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerToolbarComponent, decorators: [{
910
1218
  type: Component,
911
- args: [{ selector: 'nxr-viewer-toolbar', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
912
- class: 'nxr-viewer-toolbar',
1219
+ args: [{ selector: 'nxe-viewer-toolbar', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
1220
+ class: 'nxe-viewer-toolbar',
913
1221
  role: 'toolbar',
914
1222
  '[style.background-color]': 'backgroundColor()',
915
1223
  '[style.border]': 'border()',
916
1224
  '[style.color]': 'fontColor()',
917
1225
  '[style.font-family]': 'fontFamily()',
918
1226
  '[style.justify-content]': 'justifyContent()',
919
- '[class.nxr-busy]': 'busy()',
1227
+ '[class.nxe-busy]': 'busy()',
920
1228
  }, template: `
1229
+ @if (showSave()) {
1230
+ <div class="nxe-toolbar-menu">
1231
+ <button
1232
+ type="button"
1233
+ class="nxe-toolbar-button nxe-toolbar-captioned"
1234
+ [attr.aria-label]="strings().SaveToolTip"
1235
+ [attr.aria-expanded]="openMenu() === 'save'"
1236
+ (click)="toggleMenu('save')"
1237
+ >
1238
+ <svg viewBox="0 0 16 16" aria-hidden="true">
1239
+ <path
1240
+ d="M2 2h9.5L14 4.5V14H2zm2.5 0v4.5h6V2zm-1 7.5h9V14h-9zM8.5 3h1.5v2.5H8.5z"
1241
+ />
1242
+ </svg>
1243
+ <span class="nxe-toolbar-caption">{{ strings().Save }}</span>
1244
+ <svg class="nxe-toolbar-arrow" viewBox="0 0 8 8" aria-hidden="true">
1245
+ <path d="M1 3h6L4 6.2z" />
1246
+ </svg>
1247
+ </button>
1248
+ @if (openMenu() === 'save') {
1249
+ <div class="nxe-toolbar-popup" role="menu">
1250
+ @for (format of saveFormats(); track format) {
1251
+ <button
1252
+ type="button"
1253
+ class="nxe-toolbar-item"
1254
+ role="menuitem"
1255
+ (click)="save(format)"
1256
+ >
1257
+ <span class="nxe-toolbar-item-icon">
1258
+ <svg viewBox="0 0 16 16" aria-hidden="true">
1259
+ <path d="M3 1h7l3 3v11H3zm6.6 1.2v2.2h2.2z" />
1260
+ </svg>
1261
+ </span>
1262
+ {{ format }}
1263
+ </button>
1264
+ }
1265
+ </div>
1266
+ }
1267
+ </div>
1268
+ <span class="nxe-toolbar-separator"></span>
1269
+ }
921
1270
  @if (showFirstPage()) {
922
1271
  <button
923
1272
  type="button"
924
- class="nxr-toolbar-button"
1273
+ class="nxe-toolbar-button"
925
1274
  [disabled]="atStart()"
926
1275
  [attr.aria-label]="strings().FirstPage"
927
1276
  (click)="goTo(0)"
@@ -934,7 +1283,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
934
1283
  @if (showPreviousPage()) {
935
1284
  <button
936
1285
  type="button"
937
- class="nxr-toolbar-button"
1286
+ class="nxe-toolbar-button"
938
1287
  [disabled]="atStart()"
939
1288
  [attr.aria-label]="strings().PrevPage"
940
1289
  (click)="goTo(pageIndex() - 1)"
@@ -943,25 +1292,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
943
1292
  </button>
944
1293
  }
945
1294
  @if (showCurrentPage()) {
946
- <span class="nxr-page-control">
947
- <span class="nxr-page-label">{{ strings().Page }}</span>
1295
+ <span class="nxe-page-control">
1296
+ <span class="nxe-page-label">{{ strings().Page }}</span>
948
1297
  <input
949
1298
  type="text"
950
1299
  inputmode="numeric"
951
- class="nxr-page-box"
1300
+ class="nxe-page-box"
952
1301
  [style.width.px]="pageBoxWidth"
953
1302
  [disabled]="pageControlDisabled()"
954
1303
  [value]="pageIndex() + 1"
955
1304
  [attr.aria-label]="strings().Page"
956
1305
  (keydown.enter)="enterPage($event)"
957
1306
  />
958
- <span class="nxr-page-label">{{ strings().PageOf }} {{ pageCount() }}</span>
1307
+ <span class="nxe-page-label">{{ strings().PageOf }} {{ pageCount() }}</span>
959
1308
  </span>
960
1309
  }
961
1310
  @if (showNextPage()) {
962
1311
  <button
963
1312
  type="button"
964
- class="nxr-toolbar-button"
1313
+ class="nxe-toolbar-button"
965
1314
  [disabled]="atEnd()"
966
1315
  [attr.aria-label]="strings().NextPage"
967
1316
  (click)="goTo(pageIndex() + 1)"
@@ -972,7 +1321,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
972
1321
  @if (showLastPage()) {
973
1322
  <button
974
1323
  type="button"
975
- class="nxr-toolbar-button"
1324
+ class="nxe-toolbar-button"
976
1325
  [disabled]="atEnd()"
977
1326
  [attr.aria-label]="strings().LastPage"
978
1327
  (click)="goTo(pageCount() - 1)"
@@ -982,9 +1331,118 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
982
1331
  </svg>
983
1332
  </button>
984
1333
  }
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"] }] } });
1334
+ @if (showZoom() || showViewMode()) {
1335
+ <span class="nxe-toolbar-separator"></span>
1336
+ }
1337
+ @if (showZoom()) {
1338
+ <div class="nxe-toolbar-menu">
1339
+ <button
1340
+ type="button"
1341
+ class="nxe-toolbar-button nxe-toolbar-captioned"
1342
+ [attr.aria-label]="strings().ZoomToolTip"
1343
+ [attr.aria-expanded]="openMenu() === 'zoom'"
1344
+ (click)="toggleMenu('zoom')"
1345
+ >
1346
+ <svg viewBox="0 0 16 16" aria-hidden="true">
1347
+ <path
1348
+ d="M3 2h7l3 3v9H3zm6.6 1.2v2.2h2.2zM5 8h6v1.1H5zm0 2.4h6v1.1H5z"
1349
+ />
1350
+ </svg>
1351
+ <span class="nxe-toolbar-caption">{{ zoomCaption() }}</span>
1352
+ <svg class="nxe-toolbar-arrow" viewBox="0 0 8 8" aria-hidden="true">
1353
+ <path d="M1 3h6L4 6.2z" />
1354
+ </svg>
1355
+ </button>
1356
+ @if (openMenu() === 'zoom') {
1357
+ <div class="nxe-toolbar-popup" role="menu">
1358
+ @for (percent of zoomPresets; track percent) {
1359
+ <button
1360
+ type="button"
1361
+ class="nxe-toolbar-item"
1362
+ role="menuitem"
1363
+ [class.nxe-toolbar-item-on]="isZoom(percent)"
1364
+ (click)="chooseZoom(percent)"
1365
+ >
1366
+ <span class="nxe-toolbar-item-icon"></span>{{ percent }}%
1367
+ </button>
1368
+ }
1369
+ <span class="nxe-toolbar-item-separator"></span>
1370
+ <button type="button" class="nxe-toolbar-item" role="menuitem" (click)="fit('page')">
1371
+ <span class="nxe-toolbar-item-icon">
1372
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 2h10v12H3zm1.2 1.2v9.6h7.6V3.2z" /></svg>
1373
+ </span>
1374
+ {{ strings().ZoomOnePage }}
1375
+ </button>
1376
+ <button type="button" class="nxe-toolbar-item" role="menuitem" (click)="fit('width')">
1377
+ <span class="nxe-toolbar-item-icon">
1378
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M1 6h14v4H1zm1.2 1.2v1.6h11.6V7.2z" /></svg>
1379
+ </span>
1380
+ {{ strings().ZoomPageWidth }}
1381
+ </button>
1382
+ </div>
1383
+ }
1384
+ </div>
1385
+ }
1386
+ @if (showViewMode()) {
1387
+ <div class="nxe-toolbar-menu">
1388
+ <button
1389
+ type="button"
1390
+ class="nxe-toolbar-button nxe-toolbar-captioned"
1391
+ [attr.aria-label]="strings().ViewModeToolTip"
1392
+ [attr.aria-expanded]="openMenu() === 'viewMode'"
1393
+ (click)="toggleMenu('viewMode')"
1394
+ >
1395
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 2h10v12H3zm1.2 1.2v9.6h7.6V3.2z" /></svg>
1396
+ <span class="nxe-toolbar-caption">{{ strings()[viewMode()] }}</span>
1397
+ <svg class="nxe-toolbar-arrow" viewBox="0 0 8 8" aria-hidden="true">
1398
+ <path d="M1 3h6L4 6.2z" />
1399
+ </svg>
1400
+ </button>
1401
+ @if (openMenu() === 'viewMode') {
1402
+ <div class="nxe-toolbar-popup" role="menu">
1403
+ @for (mode of viewModes; track mode) {
1404
+ <button
1405
+ type="button"
1406
+ class="nxe-toolbar-item"
1407
+ role="menuitem"
1408
+ [class.nxe-toolbar-item-on]="viewMode() === mode"
1409
+ (click)="chooseViewMode(mode)"
1410
+ >
1411
+ <span class="nxe-toolbar-item-icon">
1412
+ @switch (mode) {
1413
+ @case ('SinglePage') {
1414
+ <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 2h10v12H3zm1.2 1.2v9.6h7.6V3.2z" /></svg>
1415
+ }
1416
+ @case ('Continuous') {
1417
+ <svg viewBox="0 0 16 16" aria-hidden="true">
1418
+ <path d="M3 1h10v6H3zm1.2 1.2v3.6h7.6V2.2zM3 9h10v6H3zm1.2 1.2v3.6h7.6v-3.6z" />
1419
+ </svg>
1420
+ }
1421
+ @case ('MultiplePages') {
1422
+ <svg viewBox="0 0 16 16" aria-hidden="true">
1423
+ <path d="M2 2h5v5H2zm7 0h5v5H9zM2 9h5v5H2zm7 0h5v5H9z" />
1424
+ </svg>
1425
+ }
1426
+ }
1427
+ </span>
1428
+ {{ strings()[mode] }}
1429
+ </button>
1430
+ }
1431
+ </div>
1432
+ }
1433
+ </div>
1434
+ }
1435
+ `, 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(.nxe-busy):after{content:\"\";position:absolute;inset:0;cursor:default}.nxe-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}.nxe-toolbar-button:hover:not(:disabled){background:#00000014}.nxe-toolbar-button:disabled{opacity:.5;cursor:default}.nxe-toolbar-button svg{width:16px;height:16px;fill:currentColor}.nxe-page-control{display:inline-flex;align-items:center;gap:4px;padding:0 4px;white-space:nowrap}.nxe-page-box{height:21px;box-sizing:border-box;text-align:center;font:inherit;color:inherit}.nxe-page-box:disabled{color:inherit;-webkit-text-fill-color:currentColor;opacity:1}.nxe-toolbar-separator{width:1px;height:18px;margin:0 4px;background:currentColor;opacity:.25}.nxe-toolbar-menu{position:relative;display:flex;align-items:center}.nxe-toolbar-captioned{width:auto;gap:4px;padding:0 6px}.nxe-toolbar-caption{font-size:12px;white-space:nowrap}.nxe-toolbar-arrow{width:8px;height:8px;fill:currentColor;flex:none}.nxe-toolbar-popup{position:absolute;top:100%;left:0;z-index:10;min-width:150px;margin-top:2px;padding:3px 0;background:#fff;color:#000;border:1px solid #c8c8c8;box-shadow:0 2px 8px #00000040;display:flex;flex-direction:column}.nxe-toolbar-item{display:flex;align-items:center;gap:8px;border:0;background:transparent;color:inherit;font:inherit;font-size:12px;text-align:left;white-space:nowrap;padding:5px 14px 5px 6px;cursor:pointer}.nxe-toolbar-item:hover{background:#00000012}.nxe-toolbar-item-on{background:#0000001c}.nxe-toolbar-item-icon{width:16px;height:16px;flex:none;display:inline-flex;align-items:center;justify-content:center}.nxe-toolbar-item-icon svg{width:16px;height:16px;fill:currentColor}.nxe-toolbar-item-separator{height:1px;margin:3px 0;background:#c8c8c8}\n"] }]
1436
+ }], 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 }] }], viewMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewMode", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }], goToPage: [{ type: i0.Output, args: ["goToPage"] }], viewModeChange: [{ type: i0.Output, args: ["viewModeChange"] }], zoomChange: [{ type: i0.Output, args: ["zoomChange"] }], fitRequest: [{ type: i0.Output, args: ["fitRequest"] }], saveRequest: [{ type: i0.Output, args: ["saveRequest"] }] } });
987
1437
 
1438
+ /** Stimulsoft's scroll tracker waits this long after scrolling stops before it moves the page. */
1439
+ const SCROLL_SETTLE_MS = 300;
1440
+ /** And animates a navigation scroll over this long, in steps of about this many milliseconds. */
1441
+ const SCROLL_ANIMATION_MS = 350;
1442
+ const SCROLL_STEP_MS = 30;
1443
+ /** The viewer never measures itself smaller than this, nor closer than this to the window's bottom. */
1444
+ const MIN_PANEL_HEIGHT = 200;
1445
+ const VIEWER_BOTTOM_GAP = 16;
988
1446
  /**
989
1447
  * A report served by `Nextera.Reports.Angular.NetCore`, shown page by page.
990
1448
  *
@@ -994,7 +1452,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
994
1452
  *
995
1453
  * Each viewer provides its own session, so several on a page never share a report or a token.
996
1454
  */
997
- class NxrReportViewerComponent {
1455
+ class NxeReportViewerComponent {
998
1456
  /** Each request's URL, with `{action}` in place of the host action it goes to. */
999
1457
  requestUrl = input('', /* @ts-ignore */
1000
1458
  ...(ngDevMode ? [{ debugName: "requestUrl" }] : /* istanbul ignore next */ []));
@@ -1023,11 +1481,43 @@ class NxrReportViewerComponent {
1023
1481
  loaded = output();
1024
1482
  /** Emits every failure the viewer puts on show. */
1025
1483
  error = output();
1026
- session = inject(NxrViewerSessionService);
1484
+ session = inject(NxeViewerSessionService);
1027
1485
  /** Stimulsoft's `api` object: what a host calls to drive the viewer from outside. */
1028
1486
  api = this.buildApi();
1029
1487
  appearance = computed(() => this.session.options()?.appearance ?? null, /* @ts-ignore */
1030
1488
  ...(ngDevMode ? [{ debugName: "appearance" }] : /* istanbul ignore next */ []));
1489
+ pagesBox = viewChild('pagesBox', /* @ts-ignore */
1490
+ ...(ngDevMode ? [{ debugName: "pagesBox" }] : /* istanbul ignore next */ []));
1491
+ rootBox = viewChild('rootBox', /* @ts-ignore */
1492
+ ...(ngDevMode ? [{ debugName: "rootBox" }] : /* istanbul ignore next */ []));
1493
+ /**
1494
+ * The mode the pages are shown in, and the zoom they are drawn at. Both start from the server's
1495
+ * options and are then the user's, until a new report's options arrive and reset them — which is
1496
+ * what `linkedSignal` says and an effect would only imply.
1497
+ */
1498
+ viewMode = linkedSignal(() => this.session.options()?.toolbar.viewMode ?? 'SinglePage', /* @ts-ignore */
1499
+ ...(ngDevMode ? [{ debugName: "viewMode" }] : /* istanbul ignore next */ []));
1500
+ zoom = linkedSignal(() => {
1501
+ const percent = this.session.options()?.toolbar.zoom ?? 100;
1502
+ return percent > 0 ? percent / 100 : 1;
1503
+ }, /* @ts-ignore */
1504
+ ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
1505
+ /**
1506
+ * The pages on screen: one in `SinglePage`, the whole report in the modes that scroll through it.
1507
+ * A page carries its size before its HTML arrives, so the list holds its shape while the pages that
1508
+ * come into view are fetched.
1509
+ */
1510
+ shownPages = computed(() => {
1511
+ if (this.viewMode() === 'SinglePage') {
1512
+ const page = this.session.shownPage();
1513
+ return page ? [page] : [];
1514
+ }
1515
+ return this.session.allPages();
1516
+ }, /* @ts-ignore */
1517
+ ...(ngDevMode ? [{ debugName: "shownPages" }] : /* istanbul ignore next */ []));
1518
+ /** Set while a page-navigation scroll is running, so scroll tracking does not fight it. */
1519
+ scrollingToPage = false;
1520
+ scrollSettled = null;
1031
1521
  toolbarVisible = computed(() => this.session.options()?.toolbar.visible !== false, /* @ts-ignore */
1032
1522
  ...(ngDevMode ? [{ debugName: "toolbarVisible" }] : /* istanbul ignore next */ []));
1033
1523
  pageCss = computed(() => this.session.report()?.pageCss ?? '', /* @ts-ignore */
@@ -1058,7 +1548,22 @@ class NxrReportViewerComponent {
1058
1548
  ...(ngDevMode ? [{ debugName: "rootWidth" }] : /* istanbul ignore next */ []));
1059
1549
  rootBackgroundColor = computed(() => this.backgroundColor() ?? 'White', /* @ts-ignore */
1060
1550
  ...(ngDevMode ? [{ debugName: "rootBackgroundColor" }] : /* istanbul ignore next */ []));
1551
+ /**
1552
+ * The height the viewer was measured to fill, once it is known.
1553
+ *
1554
+ * The report panel is what scrolls, as Stimulsoft's does — its panel measures 836 px against a
1555
+ * 16 513 px report while the document itself does not scroll at all. That needs the viewer to have a
1556
+ * definite height, and a host that asks for `100%` inside a container whose own height is automatic
1557
+ * does not give it one: the viewer would then grow to the whole report, 29 pages of it, and the
1558
+ * page would scroll instead of the panel. So when the height the host asked for cannot resolve, the
1559
+ * viewer measures the room between its top and the bottom of the window and takes that.
1560
+ */
1561
+ measuredHeight = signal(null, /* @ts-ignore */
1562
+ ...(ngDevMode ? [{ debugName: "measuredHeight" }] : /* istanbul ignore next */ []));
1061
1563
  rootHeight = computed(() => {
1564
+ const measured = this.measuredHeight();
1565
+ if (measured !== null)
1566
+ return measured;
1062
1567
  const given = this.height();
1063
1568
  if (given !== null)
1064
1569
  return given;
@@ -1066,9 +1571,12 @@ class NxrReportViewerComponent {
1066
1571
  }, /* @ts-ignore */
1067
1572
  ...(ngDevMode ? [{ debugName: "rootHeight" }] : /* istanbul ignore next */ []));
1068
1573
  constructor() {
1069
- this.session.reportLoaded
1070
- .pipe(takeUntilDestroyed())
1071
- .subscribe(() => this.loaded.emit());
1574
+ this.session.reportLoaded.pipe(takeUntilDestroyed()).subscribe(() => {
1575
+ // The modes that show the whole report start with every page laid out at its size and none of
1576
+ // them fetched, so the first screenful has to be asked for here; scrolling asks for the rest.
1577
+ this.afterPagesSettle();
1578
+ this.loaded.emit();
1579
+ });
1072
1580
  this.session.failed
1073
1581
  .pipe(takeUntilDestroyed())
1074
1582
  .subscribe((failure) => this.error.emit(failure));
@@ -1081,6 +1589,142 @@ class NxrReportViewerComponent {
1081
1589
  ngOnChanges() {
1082
1590
  this.session.open(this.context(), this.action());
1083
1591
  }
1592
+ /**
1593
+ * Page navigation. In a mode that scrolls through the whole report the page is reached by scrolling
1594
+ * to it, over 350 ms, which is what Stimulsoft's navigation actions do; in `SinglePage` the page is
1595
+ * simply the one shown.
1596
+ */
1597
+ navigateTo(index) {
1598
+ this.session.goToPage(index);
1599
+ if (this.viewMode() === 'SinglePage')
1600
+ return;
1601
+ this.scrollToPage(index);
1602
+ }
1603
+ /** The page number is kept and nothing scrolls, as Stimulsoft's view-mode actions leave it. */
1604
+ setViewMode(mode) {
1605
+ this.viewMode.set(mode);
1606
+ this.afterPagesSettle();
1607
+ }
1608
+ /** Nothing preserves the scroll offset across a zoom change, so the visible region shifts. */
1609
+ setZoom(zoom) {
1610
+ this.zoom.set(zoom);
1611
+ this.afterPagesSettle();
1612
+ }
1613
+ /**
1614
+ * Fit a whole page, or a page's width, in the panel. Stimulsoft computes this once from what is on
1615
+ * screen and stores the number as the zoom; no fit mode is remembered, so a later resize does not
1616
+ * re-fit. The 40 px it subtracts is the panel's padding and the page's margins.
1617
+ */
1618
+ fitTo(what) {
1619
+ const box = this.pagesBox()?.nativeElement;
1620
+ const page = box?.querySelector('nxe-report-page');
1621
+ if (!box || !(page instanceof HTMLElement))
1622
+ return;
1623
+ const available = what === 'width' ? box.clientWidth - 40 : box.clientHeight - 40;
1624
+ const drawn = what === 'width' ? page.offsetWidth : page.offsetHeight;
1625
+ if (drawn <= 0 || available <= 0)
1626
+ return;
1627
+ this.zoom.set((available * this.zoom()) / drawn);
1628
+ this.afterPagesSettle();
1629
+ }
1630
+ /**
1631
+ * Scrolling asks for the pages that have come into view, and, once it has stopped for 300 ms, moves
1632
+ * the page the toolbar reports to the first one still on screen — Stimulsoft's scroll tracking.
1633
+ */
1634
+ onScroll() {
1635
+ this.requestVisiblePages();
1636
+ if (this.scrollSettled !== null)
1637
+ clearTimeout(this.scrollSettled);
1638
+ this.scrollSettled = setTimeout(() => {
1639
+ this.scrollSettled = null;
1640
+ if (!this.scrollingToPage)
1641
+ this.trackCurrentPage();
1642
+ }, SCROLL_SETTLE_MS);
1643
+ }
1644
+ /** The room on screen changed, so the viewer measures itself against it again. */
1645
+ onWindowResize() {
1646
+ this.fitToAvailableRoom();
1647
+ }
1648
+ /** Lets the new layout reach the DOM, then fits the viewer to it and asks for what is on screen. */
1649
+ afterPagesSettle() {
1650
+ requestAnimationFrame(() => {
1651
+ this.fitToAvailableRoom();
1652
+ this.requestVisiblePages();
1653
+ });
1654
+ }
1655
+ /**
1656
+ * Takes the room between the viewer's top and the bottom of the window when the height the host
1657
+ * asked for did not resolve, so the panel inside scrolls rather than the page. Measured before the
1658
+ * height is applied, and again only when the window changes, so it never chases its own answer.
1659
+ */
1660
+ fitToAvailableRoom() {
1661
+ const root = this.rootBox()?.nativeElement;
1662
+ if (!root || !this.appearance()?.scrollbarsMode)
1663
+ return;
1664
+ const rect = root.getBoundingClientRect();
1665
+ const room = Math.max(MIN_PANEL_HEIGHT, Math.round(window.innerHeight - rect.top - VIEWER_BOTTOM_GAP));
1666
+ // A height that resolved is left alone; one that grew past the room on screen is taken over.
1667
+ if (this.measuredHeight() === null && rect.height <= room + 1)
1668
+ return;
1669
+ this.measuredHeight.set(`${room}px`);
1670
+ }
1671
+ /** Every page whose box meets the panel, plus one either side, so scrolling stays ahead of the eye. */
1672
+ requestVisiblePages() {
1673
+ if (this.viewMode() === 'SinglePage')
1674
+ return;
1675
+ const box = this.pagesBox()?.nativeElement;
1676
+ if (!box)
1677
+ return;
1678
+ const pages = box.querySelectorAll('nxe-report-page');
1679
+ const top = box.scrollTop - box.clientHeight;
1680
+ const bottom = box.scrollTop + box.clientHeight * 2;
1681
+ pages.forEach((element, index) => {
1682
+ if (!(element instanceof HTMLElement))
1683
+ return;
1684
+ const pageTop = element.offsetTop - box.offsetTop;
1685
+ if (pageTop + element.offsetHeight >= top && pageTop <= bottom)
1686
+ this.session.requestPage(index);
1687
+ });
1688
+ }
1689
+ /** The first page whose bottom is still below the panel's scroll offset. */
1690
+ trackCurrentPage() {
1691
+ const box = this.pagesBox()?.nativeElement;
1692
+ if (!box)
1693
+ return;
1694
+ const pages = box.querySelectorAll('nxe-report-page');
1695
+ for (let index = 0; index < pages.length; index++) {
1696
+ const element = pages[index];
1697
+ if (!(element instanceof HTMLElement))
1698
+ continue;
1699
+ const pageTop = element.offsetTop - box.offsetTop;
1700
+ if (pageTop + element.offsetHeight > box.scrollTop) {
1701
+ this.session.markCurrentPage(index);
1702
+ return;
1703
+ }
1704
+ }
1705
+ }
1706
+ /** Stimulsoft animates the panel to the page's top over 350 ms, in steps of about 30 ms. */
1707
+ scrollToPage(index) {
1708
+ const box = this.pagesBox()?.nativeElement;
1709
+ const element = box?.querySelectorAll('nxe-report-page')[index];
1710
+ if (!box || !(element instanceof HTMLElement))
1711
+ return;
1712
+ const from = box.scrollTop;
1713
+ const to = element.offsetTop - box.offsetTop;
1714
+ const started = performance.now();
1715
+ this.scrollingToPage = true;
1716
+ const step = () => {
1717
+ const done = Math.min(1, (performance.now() - started) / SCROLL_ANIMATION_MS);
1718
+ box.scrollTop = from + (to - from) * done;
1719
+ if (done < 1) {
1720
+ setTimeout(step, SCROLL_STEP_MS);
1721
+ return;
1722
+ }
1723
+ this.scrollingToPage = false;
1724
+ this.requestVisiblePages();
1725
+ };
1726
+ step();
1727
+ }
1084
1728
  /**
1085
1729
  * The getters read the session's signals, so a host that asks between renders gets what is on
1086
1730
  * screen now. Built from a captured session rather than `this`, because `this` inside an object
@@ -1108,104 +1752,126 @@ class NxrReportViewerComponent {
1108
1752
  requestUrl: this.requestUrl(),
1109
1753
  properties: this.properties(),
1110
1754
  postParametersFunction: this.postParametersFunction(),
1111
- strings: NXR_ENGLISH,
1755
+ strings: NXE_ENGLISH,
1112
1756
  };
1113
1757
  }
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: `
1758
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeReportViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1759
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeReportViewerComponent, isStandalone: true, selector: "nxe-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" }, host: { listeners: { "window:resize": "onWindowResize()" } }, providers: [NxeViewerClientService, NxeViewerSessionService], viewQueries: [{ propertyName: "pagesBox", first: true, predicate: ["pagesBox"], descendants: true, isSignal: true }, { propertyName: "rootBox", first: true, predicate: ["rootBox"], descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: `
1116
1760
  <div
1117
- class="nxr-viewer-root"
1761
+ #rootBox
1762
+ class="nxe-viewer-root"
1118
1763
  [style]="style()"
1119
1764
  [style.background-color]="rootBackgroundColor()"
1120
1765
  [style.width]="rootWidth()"
1121
1766
  [style.height]="rootHeight()"
1122
1767
  >
1123
1768
  @if (toolbarVisible()) {
1124
- <nxr-viewer-toolbar
1769
+ <nxe-viewer-toolbar
1125
1770
  [options]="session.options()"
1126
1771
  [pageIndex]="session.pageIndex()"
1127
1772
  [pageCount]="session.pageCount()"
1128
1773
  [busy]="session.busy()"
1129
- (goToPage)="session.goToPage($event)"
1774
+ [viewMode]="viewMode()"
1775
+ [zoom]="zoom()"
1776
+ (goToPage)="navigateTo($event)"
1777
+ (viewModeChange)="setViewMode($event)"
1778
+ (zoomChange)="setZoom($event)"
1779
+ (fitRequest)="fitTo($event)"
1780
+ (saveRequest)="session.exportTo($event)"
1130
1781
  />
1131
1782
  }
1132
1783
 
1133
1784
  <div
1134
- class="nxr-viewer-pages"
1785
+ #pagesBox
1786
+ class="nxe-viewer-pages"
1787
+ [class.nxe-viewer-continuous]="viewMode() === 'Continuous'"
1135
1788
  [style.text-align]="pageAlignment()"
1136
1789
  [style.overflow]="pagesOverflow()"
1790
+ (scroll)="onScroll()"
1137
1791
  >
1138
- @if (session.shownPage(); as page) {
1139
- <nxr-report-page
1140
- class="nxr-viewer-page"
1141
- [class.nxr-viewer-page-shadow]="showPageShadow()"
1792
+ @for (page of shownPages(); track page.index) {
1793
+ <nxe-report-page
1794
+ class="nxe-viewer-page"
1795
+ [class.nxe-viewer-page-shadow]="showPageShadow()"
1142
1796
  [style.border]="pageBorder()"
1143
1797
  [page]="page.size"
1144
1798
  [html]="page.html"
1145
1799
  [css]="pageCss()"
1800
+ [zoom]="zoom()"
1146
1801
  />
1147
1802
  }
1148
1803
  </div>
1149
1804
 
1150
1805
  @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()">
1806
+ <div class="nxe-viewer-failure" role="alert">
1807
+ <span class="nxe-viewer-failure-text">{{ failure.message }}</span>
1808
+ <button type="button" class="nxe-viewer-failure-close" (click)="session.dismissFailure()">
1154
1809
  &times;
1155
1810
  </button>
1156
1811
  </div>
1157
1812
  }
1158
1813
  </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 });
1814
+ `, isInline: true, styles: [":host{display:inline-block;position:relative}.nxe-viewer-root{display:flex;flex-direction:column;box-sizing:border-box;position:relative}.nxe-viewer-pages{flex:1 1 auto;min-height:0;box-sizing:border-box}.nxe-viewer-page{display:inline-block;vertical-align:top;margin:10px}.nxe-viewer-continuous .nxe-viewer-page{display:block;margin:10px auto}.nxe-viewer-page-shadow{box-shadow:2px 2px 6px #0000004d}.nxe-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}.nxe-viewer-failure-text{flex:1 1 auto}.nxe-viewer-failure-close{border:0;background:transparent;color:inherit;font-size:18px;line-height:1;cursor:pointer}\n"], dependencies: [{ kind: "component", type: NxeReportPageComponent, selector: "nxe-report-page", inputs: ["page", "html", "css", "zoom"] }, { kind: "component", type: NxeViewerToolbarComponent, selector: "nxe-viewer-toolbar", inputs: ["options", "pageIndex", "pageCount", "busy", "strings", "viewMode", "zoom"], outputs: ["goToPage", "viewModeChange", "zoomChange", "fitRequest", "saveRequest"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1160
1815
  }
1161
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrReportViewerComponent, decorators: [{
1816
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeReportViewerComponent, decorators: [{
1162
1817
  type: Component,
1163
- args: [{ selector: 'nxr-report-viewer', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NxrReportPageComponent, NxrViewerToolbarComponent], providers: [NxrViewerClientService, NxrViewerSessionService], template: `
1818
+ args: [{ selector: 'nxe-report-viewer', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NxeReportPageComponent, NxeViewerToolbarComponent], providers: [NxeViewerClientService, NxeViewerSessionService], host: { '(window:resize)': 'onWindowResize()' }, template: `
1164
1819
  <div
1165
- class="nxr-viewer-root"
1820
+ #rootBox
1821
+ class="nxe-viewer-root"
1166
1822
  [style]="style()"
1167
1823
  [style.background-color]="rootBackgroundColor()"
1168
1824
  [style.width]="rootWidth()"
1169
1825
  [style.height]="rootHeight()"
1170
1826
  >
1171
1827
  @if (toolbarVisible()) {
1172
- <nxr-viewer-toolbar
1828
+ <nxe-viewer-toolbar
1173
1829
  [options]="session.options()"
1174
1830
  [pageIndex]="session.pageIndex()"
1175
1831
  [pageCount]="session.pageCount()"
1176
1832
  [busy]="session.busy()"
1177
- (goToPage)="session.goToPage($event)"
1833
+ [viewMode]="viewMode()"
1834
+ [zoom]="zoom()"
1835
+ (goToPage)="navigateTo($event)"
1836
+ (viewModeChange)="setViewMode($event)"
1837
+ (zoomChange)="setZoom($event)"
1838
+ (fitRequest)="fitTo($event)"
1839
+ (saveRequest)="session.exportTo($event)"
1178
1840
  />
1179
1841
  }
1180
1842
 
1181
1843
  <div
1182
- class="nxr-viewer-pages"
1844
+ #pagesBox
1845
+ class="nxe-viewer-pages"
1846
+ [class.nxe-viewer-continuous]="viewMode() === 'Continuous'"
1183
1847
  [style.text-align]="pageAlignment()"
1184
1848
  [style.overflow]="pagesOverflow()"
1849
+ (scroll)="onScroll()"
1185
1850
  >
1186
- @if (session.shownPage(); as page) {
1187
- <nxr-report-page
1188
- class="nxr-viewer-page"
1189
- [class.nxr-viewer-page-shadow]="showPageShadow()"
1851
+ @for (page of shownPages(); track page.index) {
1852
+ <nxe-report-page
1853
+ class="nxe-viewer-page"
1854
+ [class.nxe-viewer-page-shadow]="showPageShadow()"
1190
1855
  [style.border]="pageBorder()"
1191
1856
  [page]="page.size"
1192
1857
  [html]="page.html"
1193
1858
  [css]="pageCss()"
1859
+ [zoom]="zoom()"
1194
1860
  />
1195
1861
  }
1196
1862
  </div>
1197
1863
 
1198
1864
  @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()">
1865
+ <div class="nxe-viewer-failure" role="alert">
1866
+ <span class="nxe-viewer-failure-text">{{ failure.message }}</span>
1867
+ <button type="button" class="nxe-viewer-failure-close" (click)="session.dismissFailure()">
1202
1868
  &times;
1203
1869
  </button>
1204
1870
  </div>
1205
1871
  }
1206
1872
  </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"] }] } });
1873
+ `, styles: [":host{display:inline-block;position:relative}.nxe-viewer-root{display:flex;flex-direction:column;box-sizing:border-box;position:relative}.nxe-viewer-pages{flex:1 1 auto;min-height:0;box-sizing:border-box}.nxe-viewer-page{display:inline-block;vertical-align:top;margin:10px}.nxe-viewer-continuous .nxe-viewer-page{display:block;margin:10px auto}.nxe-viewer-page-shadow{box-shadow:2px 2px 6px #0000004d}.nxe-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}.nxe-viewer-failure-text{flex:1 1 auto}.nxe-viewer-failure-close{border:0;background:transparent;color:inherit;font-size:18px;line-height:1;cursor:pointer}\n"] }]
1874
+ }], 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"] }], pagesBox: [{ type: i0.ViewChild, args: ['pagesBox', { isSignal: true }] }], rootBox: [{ type: i0.ViewChild, args: ['rootBox', { isSignal: true }] }] } });
1209
1875
 
1210
1876
  /*
1211
1877
  * Public API Surface of @nexterajs/report-viewer
@@ -1215,5 +1881,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
1215
1881
  * Generated bundle index. Do not edit.
1216
1882
  */
1217
1883
 
1218
- export { NXR_VIEWER_PROTOCOL, NxrReportViewerComponent };
1884
+ export { NXE_VIEWER_PROTOCOL, NxeReportViewerComponent };
1219
1885
  //# sourceMappingURL=nexterajs-report-viewer.mjs.map