@nexterajs/report-viewer 0.0.1 → 0.1.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.
@@ -1,14 +1,334 @@
1
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
1
+ import { takeUntilDestroyed, toObservable } 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, output, signal, viewChild, afterNextRender, ChangeDetectionStrategy, Component, computed, inject, DOCUMENT, ElementRef, Injectable, DestroyRef, linkedSignal } from '@angular/core';
4
+ import { defer, map, catchError, from, switchMap, throwError, timeout, Subject, Subscription, finalize, interval, exhaustMap, EMPTY, distinctUntilChanged, skip } from 'rxjs';
4
5
  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
6
 
7
7
  /**
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
+
13
+ /**
14
+ * Finding text in the pages on screen, as Stimulsoft 2025.1.4's viewer does
15
+ * (angular-viewer-panels-chrome.md §1.4-1.7), over each page's shadow root.
16
+ *
17
+ * A candidate is an element whose only child is a single text node; text split across elements is
18
+ * never found. The element's HTML is compared, lower-cased unless the case must match, by a plain
19
+ * substring test; a whole word needs no ASCII letter or digit on either side of the first occurrence.
20
+ * Each hit gets one label, however often the text occurs in it.
21
+ */
22
+ /** The label every hit carries, and how the current one differs from the rest. */
23
+ const LABEL_CLASS = 'nxe-find-label';
24
+ const CURRENT_BORDER = 'red';
25
+ const OTHER_BORDER = 'rgb(138, 138, 138)';
26
+ /** The inline position a hit had before its label made it a positioning box, to put back afterwards. */
27
+ const PREVIOUS_POSITION = 'nxeFindPosition';
28
+ const WORD_CHARACTER = /[A-Za-z0-9]/;
29
+ /** Every element in the roots, in document order, that holds the text. */
30
+ function findHits(roots, text, match) {
31
+ const needle = match.matchCase ? text : text.toLowerCase();
32
+ const hits = [];
33
+ if (needle === '')
34
+ return hits;
35
+ for (const root of roots) {
36
+ for (const element of Array.from(root.querySelectorAll('*'))) {
37
+ if (element.childNodes.length !== 1 || element.firstChild?.nodeType !== 3)
38
+ continue;
39
+ const html = match.matchCase ? element.innerHTML : element.innerHTML.toLowerCase();
40
+ const at = html.indexOf(needle);
41
+ if (at < 0)
42
+ continue;
43
+ if (match.wholeWord && !standsAlone(html, at, needle.length))
44
+ continue;
45
+ hits.push(element);
46
+ }
47
+ }
48
+ return hits;
49
+ }
50
+ function standsAlone(html, at, length) {
51
+ const before = at > 0 ? html[at - 1] : '';
52
+ const after = html[at + length] ?? '';
53
+ return !WORD_CHARACTER.test(before) && !WORD_CHARACTER.test(after);
54
+ }
55
+ /**
56
+ * Labels each hit: a yellow box over the element, 60 % opaque so the text reads through it, with a
57
+ * 2 px border that is red on the current hit and grey on the rest, inserted as the element's first
58
+ * child and sized to it. The element becomes the label's positioning box; Stimulsoft makes it
59
+ * `relative` unless it is positioned already, and it is here unless its computed position is
60
+ * `static`, because this engine positions its page elements from the stylesheet.
61
+ */
62
+ function labelHits(hits, current) {
63
+ hits.forEach((element, index) => {
64
+ const label = element.ownerDocument.createElement('div');
65
+ label.className = LABEL_CLASS;
66
+ Object.assign(label.style, {
67
+ position: 'absolute',
68
+ left: '0px',
69
+ top: '0px',
70
+ width: `${Math.max(0, element.offsetWidth - 4)}px`,
71
+ height: `${Math.max(0, element.offsetHeight - 4)}px`,
72
+ zIndex: '150',
73
+ opacity: '0.6',
74
+ borderRadius: '3px',
75
+ background: 'rgb(255, 255, 0)',
76
+ border: `2px solid ${index === current ? CURRENT_BORDER : OTHER_BORDER}`,
77
+ pointerEvents: 'none',
78
+ });
79
+ if (getComputedStyle(element).position === 'static') {
80
+ element.dataset[PREVIOUS_POSITION] = element.style.position;
81
+ element.style.position = 'relative';
82
+ }
83
+ element.insertBefore(label, element.firstChild);
84
+ });
85
+ }
86
+ /** Moves the current mark from one hit to another. */
87
+ function markCurrent(hits, previous, current) {
88
+ const border = (index, color) => {
89
+ const label = hits[index]?.querySelector(`:scope > .${LABEL_CLASS}`);
90
+ if (label)
91
+ label.style.borderColor = color;
92
+ };
93
+ border(previous, OTHER_BORDER);
94
+ border(current, CURRENT_BORDER);
95
+ }
96
+ /** Removes every label from the roots, and gives back the position each hit had before it. */
97
+ function clearHits(roots) {
98
+ for (const root of roots) {
99
+ for (const label of Array.from(root.querySelectorAll(`.${LABEL_CLASS}`))) {
100
+ const element = label.parentElement;
101
+ label.remove();
102
+ if (element && PREVIOUS_POSITION in element.dataset) {
103
+ element.style.position = element.dataset[PREVIOUS_POSITION] ?? '';
104
+ delete element.dataset[PREVIOUS_POSITION];
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Every caption and message the viewer shows, in its built-in English. A key in PascalCase is the name
112
+ * Stimulsoft's viewer gives the same text, such as `PageOf`; a key in camelCase names a message only this
113
+ * viewer has. The English is this package's own. A later plan lets a host's translation file replace any
114
+ * of them, so code reads them from an `NxeViewerStrings` value wherever one is at hand.
115
+ */
116
+ const NXE_ENGLISH = {
117
+ /** The label before the page-number box. */
118
+ Page: 'Page',
119
+ /** The label between the page-number box and the page count. */
120
+ PageOf: 'of',
121
+ FirstPage: 'First page',
122
+ PrevPage: 'Previous page',
123
+ NextPage: 'Next page',
124
+ LastPage: 'Last page',
125
+ /** The save button's caption and tooltip, and its menu items, captioned by the format they write. */
126
+ Save: 'Save',
127
+ SaveToolTip: 'Save',
128
+ formatPdf: 'Adobe PDF',
129
+ formatHtml: 'HTML',
130
+ /** The resources button's tooltip. */
131
+ Resources: 'Resources',
132
+ /** The find button's tooltip, and the find panel's captions. */
133
+ FindToolTip: 'Find',
134
+ FindWhat: 'Find What',
135
+ FindPrevious: 'Find Previous',
136
+ FindNext: 'Find Next',
137
+ MatchCase: 'Match Case',
138
+ MatchWholeWord: 'Match Whole Word',
139
+ closeFind: 'Close',
140
+ /** The view-mode button's tooltip, and its menu items, whose captions are the mode names. */
141
+ ViewModeToolTip: 'View mode',
142
+ SinglePage: 'Single Page',
143
+ Continuous: 'Continuous',
144
+ MultiplePages: 'Multiple Pages',
145
+ /** The zoom button's tooltip; its caption is the current percentage, so it has no key. */
146
+ ZoomToolTip: 'Zoom',
147
+ /** Fitting a whole page, which Stimulsoft's menu captions by the page's height. */
148
+ ZoomOnePage: 'Page Height',
149
+ ZoomPageWidth: 'Page Width',
150
+ errorUnreachable: 'The report server could not be reached.',
151
+ errorStatus: 'The report server answered with HTTP status {0}.',
152
+ errorTimeout: 'The report server did not answer within {0} seconds.',
153
+ errorMalformed: "The report server's answer to {0} is not one this viewer can read.",
154
+ errorProtocol: 'The report server speaks viewer protocol {0}, and this viewer speaks protocol {1}.',
155
+ };
156
+ /** Puts each value in place of its numbered placeholder: `{0}` is the first value. */
157
+ function formatString(template, ...values) {
158
+ return template.replace(/\{(\d+)\}/g, (placeholder, index) => Number(index) < values.length ? String(values[Number(index)]) : placeholder);
159
+ }
160
+
161
+ /**
162
+ * The find panel, as Stimulsoft 2025.1.4 draws and runs it (angular-viewer-panels-chrome.md §1): a
163
+ * second bar below the toolbar with Close, the "Find What" box, Find Previous, Find Next, and the Match
164
+ * Case and Match Whole Word toggles, every item with a 2 px margin.
165
+ *
166
+ * It searches only on Enter in the box and on the two find buttons, never as the user types, and the
167
+ * toggles only change how the next search runs. It opens empty with the box focused, and has no
168
+ * tooltips. The viewer does the finding; this panel says what to find.
169
+ */
170
+ class NxeViewerFindPanelComponent {
171
+ /** The captions to show. */
172
+ strings = input(NXE_ENGLISH, /* @ts-ignore */
173
+ ...(ngDevMode ? [{ debugName: "strings" }] : /* istanbul ignore next */ []));
174
+ /** A search the user asked for. */
175
+ search = output();
176
+ /** The user closed the panel. */
177
+ closed = output();
178
+ matchCase = signal(false, /* @ts-ignore */
179
+ ...(ngDevMode ? [{ debugName: "matchCase" }] : /* istanbul ignore next */ []));
180
+ wholeWord = signal(false, /* @ts-ignore */
181
+ ...(ngDevMode ? [{ debugName: "wholeWord" }] : /* istanbul ignore next */ []));
182
+ box = viewChild.required('box', /* @ts-ignore */
183
+ ...(ngDevMode ? [{ debugName: "box" }] : /* istanbul ignore next */ []));
184
+ constructor() {
185
+ // Stimulsoft focuses the box on the next tick after the panel opens.
186
+ afterNextRender(() => this.box().nativeElement.focus());
187
+ }
188
+ find(text, forward) {
189
+ this.search.emit({ text, forward, matchCase: this.matchCase(), wholeWord: this.wholeWord() });
190
+ }
191
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerFindPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
192
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.5", type: NxeViewerFindPanelComponent, isStandalone: true, selector: "nxe-viewer-find-panel", inputs: { strings: { classPropertyName: "strings", publicName: "strings", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { search: "search", closed: "closed" }, host: { attributes: { "role": "search" }, classAttribute: "nxe-viewer-find-panel" }, viewQueries: [{ propertyName: "box", first: true, predicate: ["box"], descendants: true, isSignal: true }], ngImport: i0, template: `
193
+ <div class="nxe-find-bar">
194
+ <button
195
+ type="button"
196
+ class="nxe-find-button"
197
+ [attr.aria-label]="strings().closeFind"
198
+ (click)="closed.emit()"
199
+ >
200
+ <span class="nxe-find-icon">
201
+ <svg viewBox="0 0 16 16" aria-hidden="true">
202
+ <path d="M3.5 3.5l9 9m0-9-9 9" fill="none" stroke="#707070" stroke-width="1.2" />
203
+ </svg>
204
+ </span>
205
+ </button>
206
+ <span class="nxe-find-label">{{ strings().FindWhat }}</span>
207
+ <input
208
+ #box
209
+ type="text"
210
+ class="nxe-find-box"
211
+ [attr.aria-label]="strings().FindWhat"
212
+ (keydown.enter)="find(box.value, true)"
213
+ />
214
+ <button type="button" class="nxe-find-button" (click)="find(box.value, false)">
215
+ <span class="nxe-find-icon">
216
+ <svg viewBox="0 0 16 16" aria-hidden="true">
217
+ <path
218
+ d="M8 3.5 13 9h-3v3.5H6V9H3z"
219
+ fill="#81c1f1"
220
+ stroke="#0161b1"
221
+ stroke-linejoin="round"
222
+ />
223
+ </svg>
224
+ </span>
225
+ <span class="nxe-find-caption">{{ strings().FindPrevious }}</span>
226
+ </button>
227
+ <button type="button" class="nxe-find-button" (click)="find(box.value, true)">
228
+ <span class="nxe-find-icon">
229
+ <svg viewBox="0 0 16 16" aria-hidden="true">
230
+ <path
231
+ d="M8 12.5 13 7h-3V3.5H6V7H3z"
232
+ fill="#81c1f1"
233
+ stroke="#0161b1"
234
+ stroke-linejoin="round"
235
+ />
236
+ </svg>
237
+ </span>
238
+ <span class="nxe-find-caption">{{ strings().FindNext }}</span>
239
+ </button>
240
+ <button
241
+ type="button"
242
+ class="nxe-find-button"
243
+ [class.nxe-find-on]="matchCase()"
244
+ [attr.aria-pressed]="matchCase()"
245
+ (click)="matchCase.set(!matchCase())"
246
+ >
247
+ <span class="nxe-find-caption">{{ strings().MatchCase }}</span>
248
+ </button>
249
+ <button
250
+ type="button"
251
+ class="nxe-find-button"
252
+ [class.nxe-find-on]="wholeWord()"
253
+ [attr.aria-pressed]="wholeWord()"
254
+ (click)="wholeWord.set(!wholeWord())"
255
+ >
256
+ <span class="nxe-find-caption">{{ strings().MatchWholeWord }}</span>
257
+ </button>
258
+ </div>
259
+ `, isInline: true, styles: [":host{display:block;box-sizing:border-box;padding-top:2px;-webkit-user-select:none;user-select:none;font-family:Arial;font-size:12px;color:#202020}.nxe-find-bar{display:flex;align-items:center;box-sizing:border-box;height:31px;border:1px solid #c6c6c6;border-radius:3px;background:#fff;cursor:default}.nxe-find-button{display:inline-flex;align-items:center;box-sizing:border-box;height:25px;margin:2px;padding:0;border:1px solid transparent;border-radius:3px;background:transparent;color:inherit;font:inherit;white-space:nowrap;cursor:default}.nxe-find-button:hover{background:#dadada}.nxe-find-button.nxe-find-on{background:#e5e5e5}.nxe-find-icon{display:flex;flex:none;box-sizing:border-box;width:22px;padding-left:3px}.nxe-find-icon svg{width:16px;height:16px}.nxe-find-caption{padding:1px 5px 0 0}.nxe-find-label{margin:2px;padding:2px 0 0;white-space:nowrap}.nxe-find-box{box-sizing:content-box;width:170px;height:21px;margin:2px;padding:0 0 0 4px;border:1px solid #ababab;border-radius:3px;background:#fff;color:inherit;font:inherit;line-height:21px;outline:none}.nxe-find-box:hover{border-color:#6b6b6b}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
260
+ }
261
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerFindPanelComponent, decorators: [{
262
+ type: Component,
263
+ args: [{ selector: 'nxe-viewer-find-panel', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'nxe-viewer-find-panel', role: 'search' }, template: `
264
+ <div class="nxe-find-bar">
265
+ <button
266
+ type="button"
267
+ class="nxe-find-button"
268
+ [attr.aria-label]="strings().closeFind"
269
+ (click)="closed.emit()"
270
+ >
271
+ <span class="nxe-find-icon">
272
+ <svg viewBox="0 0 16 16" aria-hidden="true">
273
+ <path d="M3.5 3.5l9 9m0-9-9 9" fill="none" stroke="#707070" stroke-width="1.2" />
274
+ </svg>
275
+ </span>
276
+ </button>
277
+ <span class="nxe-find-label">{{ strings().FindWhat }}</span>
278
+ <input
279
+ #box
280
+ type="text"
281
+ class="nxe-find-box"
282
+ [attr.aria-label]="strings().FindWhat"
283
+ (keydown.enter)="find(box.value, true)"
284
+ />
285
+ <button type="button" class="nxe-find-button" (click)="find(box.value, false)">
286
+ <span class="nxe-find-icon">
287
+ <svg viewBox="0 0 16 16" aria-hidden="true">
288
+ <path
289
+ d="M8 3.5 13 9h-3v3.5H6V9H3z"
290
+ fill="#81c1f1"
291
+ stroke="#0161b1"
292
+ stroke-linejoin="round"
293
+ />
294
+ </svg>
295
+ </span>
296
+ <span class="nxe-find-caption">{{ strings().FindPrevious }}</span>
297
+ </button>
298
+ <button type="button" class="nxe-find-button" (click)="find(box.value, true)">
299
+ <span class="nxe-find-icon">
300
+ <svg viewBox="0 0 16 16" aria-hidden="true">
301
+ <path
302
+ d="M8 12.5 13 7h-3V3.5H6V7H3z"
303
+ fill="#81c1f1"
304
+ stroke="#0161b1"
305
+ stroke-linejoin="round"
306
+ />
307
+ </svg>
308
+ </span>
309
+ <span class="nxe-find-caption">{{ strings().FindNext }}</span>
310
+ </button>
311
+ <button
312
+ type="button"
313
+ class="nxe-find-button"
314
+ [class.nxe-find-on]="matchCase()"
315
+ [attr.aria-pressed]="matchCase()"
316
+ (click)="matchCase.set(!matchCase())"
317
+ >
318
+ <span class="nxe-find-caption">{{ strings().MatchCase }}</span>
319
+ </button>
320
+ <button
321
+ type="button"
322
+ class="nxe-find-button"
323
+ [class.nxe-find-on]="wholeWord()"
324
+ [attr.aria-pressed]="wholeWord()"
325
+ (click)="wholeWord.set(!wholeWord())"
326
+ >
327
+ <span class="nxe-find-caption">{{ strings().MatchWholeWord }}</span>
328
+ </button>
329
+ </div>
330
+ `, styles: [":host{display:block;box-sizing:border-box;padding-top:2px;-webkit-user-select:none;user-select:none;font-family:Arial;font-size:12px;color:#202020}.nxe-find-bar{display:flex;align-items:center;box-sizing:border-box;height:31px;border:1px solid #c6c6c6;border-radius:3px;background:#fff;cursor:default}.nxe-find-button{display:inline-flex;align-items:center;box-sizing:border-box;height:25px;margin:2px;padding:0;border:1px solid transparent;border-radius:3px;background:transparent;color:inherit;font:inherit;white-space:nowrap;cursor:default}.nxe-find-button:hover{background:#dadada}.nxe-find-button.nxe-find-on{background:#e5e5e5}.nxe-find-icon{display:flex;flex:none;box-sizing:border-box;width:22px;padding-left:3px}.nxe-find-icon svg{width:16px;height:16px}.nxe-find-caption{padding:1px 5px 0 0}.nxe-find-label{margin:2px;padding:2px 0 0;white-space:nowrap}.nxe-find-box{box-sizing:content-box;width:170px;height:21px;margin:2px;padding:0 0 0 4px;border:1px solid #ababab;border-radius:3px;background:#fff;color:inherit;font:inherit;line-height:21px;outline:none}.nxe-find-box:hover{border-color:#6b6b6b}\n"] }]
331
+ }], ctorParameters: () => [], propDecorators: { strings: [{ type: i0.Input, args: [{ isSignal: true, alias: "strings", required: false }] }], search: [{ type: i0.Output, args: ["search"] }], closed: [{ type: i0.Output, args: ["closed"] }], box: [{ type: i0.ViewChild, args: ['box', { isSignal: true }] }] } });
12
332
 
13
333
  /**
14
334
  * Page HTML from the server, made safe to place in the application's document.
@@ -137,8 +457,22 @@ function adoptedSheet(cssText) {
137
457
  return sheet;
138
458
  }
139
459
 
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;}';
460
+ /** CSS pixels per point, and the share of its height Stimulsoft's viewer gives a page. */
461
+ const PX_PER_PT = 96 / 72;
462
+ const VIEWER_HEIGHT = 239 / 240;
463
+ /**
464
+ * The viewer draws each page's frame and the space between pages, so the page CSS's own are cancelled.
465
+ *
466
+ * Zoom scales the page's own box from its top left corner. Stimulsoft re-renders the page on the server
467
+ * at the asked-for zoom and never scales on the client; this engine's page HTML is absolutely positioned
468
+ * in points, so scaling it here puts the same marks in the same places without a round trip, and the text
469
+ * is re-rasterised at the new size rather than resampled. The host element is sized to the scaled box so
470
+ * the pages around it lay out against what is on screen.
471
+ */
472
+ function pageFrameCss(zoom) {
473
+ const transform = zoom === 1 ? '' : `transform:scale(${zoom});transform-origin:top left;`;
474
+ return `.nextera-page{margin:0;box-shadow:none;${transform}}`;
475
+ }
142
476
  /**
143
477
  * One report page, in its own shadow root.
144
478
  *
@@ -147,7 +481,7 @@ const PAGE_FRAME_CSS = '.nextera-page{margin:0;box-shadow:none;}';
147
481
  * report's styles and the application's never meet. `contain: paint` keeps even a fixed-position
148
482
  * element inside the page's box.
149
483
  */
150
- class NxrReportPageComponent {
484
+ class NxeReportPageComponent {
151
485
  /** The page's number and size. */
152
486
  page = input.required(/* @ts-ignore */
153
487
  ...(ngDevMode ? [{ debugName: "page" }] : /* istanbul ignore next */ []));
@@ -157,13 +491,26 @@ class NxrReportPageComponent {
157
491
  /** The stylesheet every page of the report needs. */
158
492
  css = input.required(/* @ts-ignore */
159
493
  ...(ngDevMode ? [{ debugName: "css" }] : /* istanbul ignore next */ []));
494
+ /** The zoom to draw the page at, as a factor: 1 is 100 %. */
495
+ zoom = input(1, /* @ts-ignore */
496
+ ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
497
+ /**
498
+ * The page's box in CSS pixels, as the server's page HTML draws it and Stimulsoft's viewer sizes a
499
+ * page: floor(W) wide and floor(H x 239/240) tall, 796 x 1118 for A4 (renderer-html's
500
+ * viewer-page.ts). Known from the page's size alone, so the frame holds its place before the HTML
501
+ * arrives, and scaled by the zoom.
502
+ */
503
+ scaledWidth = computed(() => Math.floor(this.page().widthPoints * PX_PER_PT + 1e-9) * this.zoom(), /* @ts-ignore */
504
+ ...(ngDevMode ? [{ debugName: "scaledWidth" }] : /* istanbul ignore next */ []));
505
+ scaledHeight = computed(() => Math.floor(this.page().heightPoints * PX_PER_PT * VIEWER_HEIGHT + 1e-9) * this.zoom(), /* @ts-ignore */
506
+ ...(ngDevMode ? [{ debugName: "scaledHeight" }] : /* istanbul ignore next */ []));
160
507
  document = inject(DOCUMENT);
161
508
  root = inject(ElementRef).nativeElement.attachShadow({
162
509
  mode: 'open',
163
510
  });
164
511
  ngOnChanges(changes) {
165
- if (changes['css']) {
166
- this.root.adoptedStyleSheets = [adoptedSheet(this.css()), adoptedSheet(PAGE_FRAME_CSS)];
512
+ if (changes['css'] || changes['zoom']) {
513
+ this.root.adoptedStyleSheets = [adoptedSheet(this.css()), adoptedSheet(pageFrameCss(this.zoom()))];
167
514
  }
168
515
  if (changes['html']) {
169
516
  const html = this.html();
@@ -175,68 +522,42 @@ class NxrReportPageComponent {
175
522
  }
176
523
  }
177
524
  }
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 });
525
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeReportPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
526
+ 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.px": "scaledWidth()", "style.height.px": "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
527
  }
181
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrReportPageComponent, decorators: [{
528
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeReportPageComponent, decorators: [{
182
529
  type: Component,
183
- args: [{ selector: 'nxr-report-page', standalone: true, template: '', changeDetection: ChangeDetectionStrategy.OnPush, host: {
184
- class: 'nxr-report-page',
530
+ args: [{ selector: 'nxe-report-page', standalone: true, template: '', changeDetection: ChangeDetectionStrategy.OnPush, host: {
531
+ class: 'nxe-report-page',
185
532
  '[attr.data-page]': 'page().pageNumber',
186
- '[style.width.pt]': 'page().widthPoints',
187
- '[style.height.pt]': 'page().heightPoints',
533
+ '[style.width.px]': 'scaledWidth()',
534
+ '[style.height.px]': 'scaledHeight()',
188
535
  }, 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
- }
536
+ }], 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 }] }] } });
216
537
 
217
538
  /**
218
539
  * A success answer that is not what the protocol says its action answers: the wrong shape, or an
219
540
  * AngularViewerData answer from another version of the protocol.
220
541
  */
221
- class NxrMalformedAnswerError extends Error {
542
+ class NxeMalformedAnswerError extends Error {
222
543
  action;
223
544
  protocol;
224
545
  constructor(action,
225
546
  /** 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.`);
547
+ protocol = NXE_VIEWER_PROTOCOL) {
548
+ super(`The ${action} answer is not what viewer protocol ${NXE_VIEWER_PROTOCOL} answers.`);
228
549
  this.action = action;
229
550
  this.protocol = protocol;
230
- this.name = 'NxrMalformedAnswerError';
551
+ this.name = 'NxeMalformedAnswerError';
231
552
  }
232
553
  }
233
554
  /** A request that got no answer within the server options' `requestTimeout`. */
234
- class NxrTimeoutError extends Error {
555
+ class NxeTimeoutError extends Error {
235
556
  seconds;
236
557
  constructor(seconds) {
237
558
  super(`No answer within ${seconds} seconds.`);
238
559
  this.seconds = seconds;
239
- this.name = 'NxrTimeoutError';
560
+ this.name = 'NxeTimeoutError';
240
561
  }
241
562
  }
242
563
  const OPTION_GROUPS = ['actions', 'appearance', 'toolbar', 'exports', 'server'];
@@ -279,25 +600,25 @@ function isLocated(value, textKey) {
279
600
  /** Reads the answer to AngularViewerData: the token, the viewer options and the host's translation. */
280
601
  function readViewerData(body) {
281
602
  if (!isObject(body))
282
- throw new NxrMalformedAnswerError('AngularViewerData');
603
+ throw new NxeMalformedAnswerError('AngularViewerData');
283
604
  const { protocol, token, options, localization } = body;
284
- if (isNumber(protocol) && protocol !== NXR_VIEWER_PROTOCOL) {
285
- throw new NxrMalformedAnswerError('AngularViewerData', protocol);
605
+ if (isNumber(protocol) && protocol !== NXE_VIEWER_PROTOCOL) {
606
+ throw new NxeMalformedAnswerError('AngularViewerData', protocol);
286
607
  }
287
- const valid = protocol === NXR_VIEWER_PROTOCOL &&
608
+ const valid = protocol === NXE_VIEWER_PROTOCOL &&
288
609
  typeof token === 'string' &&
289
610
  token !== '' &&
290
611
  isObject(options) &&
291
612
  OPTION_GROUPS.every((group) => isObject(options[group])) &&
292
613
  (localization === null || isObject(localization));
293
614
  if (!valid)
294
- throw new NxrMalformedAnswerError('AngularViewerData');
615
+ throw new NxeMalformedAnswerError('AngularViewerData');
295
616
  return body;
296
617
  }
297
618
  /** Reads the answer to GetReport: the report's name, page count, page sizes, bookmarks and page stylesheet. */
298
619
  function readReport(body) {
299
620
  if (!isObject(body))
300
- throw new NxrMalformedAnswerError('GetReport');
621
+ throw new NxeMalformedAnswerError('GetReport');
301
622
  const { reportName, pageCount, pages, bookmarks, pageCss, pagesHtml } = body;
302
623
  const valid = typeof reportName === 'string' &&
303
624
  isIndex(pageCount) &&
@@ -309,7 +630,7 @@ function readReport(body) {
309
630
  typeof pageCss === 'string' &&
310
631
  (pagesHtml === undefined || isPages(pagesHtml));
311
632
  if (!valid)
312
- throw new NxrMalformedAnswerError('GetReport');
633
+ throw new NxeMalformedAnswerError('GetReport');
313
634
  return {
314
635
  ...body,
315
636
  pagesHtml: pagesHtml ?? null,
@@ -318,20 +639,20 @@ function readReport(body) {
318
639
  /** Reads the answer to GetPages. */
319
640
  function readPages(body) {
320
641
  if (!isPages(body))
321
- throw new NxrMalformedAnswerError('GetPages');
642
+ throw new NxeMalformedAnswerError('GetPages');
322
643
  return body;
323
644
  }
324
645
  /** Reads the answer to Find. */
325
646
  function readFind(body) {
326
647
  const hits = isObject(body) ? body['hits'] : undefined;
327
648
  if (!Array.isArray(hits) || !hits.every((hit) => isLocated(hit, 'text')))
328
- throw new NxrMalformedAnswerError('Find');
649
+ throw new NxeMalformedAnswerError('Find');
329
650
  return body;
330
651
  }
331
652
  /** Reads the answer to UpdateCache. */
332
653
  function readUpdateCache(body) {
333
654
  if (!isObject(body) || typeof body['updated'] !== 'boolean')
334
- throw new NxrMalformedAnswerError('UpdateCache');
655
+ throw new NxeMalformedAnswerError('UpdateCache');
335
656
  return body;
336
657
  }
337
658
  function described(status, message) {
@@ -345,21 +666,21 @@ function described(status, message) {
345
666
  * - a timeout;
346
667
  * - a success answer the viewer cannot read.
347
668
  */
348
- function readFailure(failure, action, strings = NXR_ENGLISH) {
669
+ function readFailure(failure, action, strings = NXE_ENGLISH) {
349
670
  if (failure instanceof HttpErrorResponse) {
350
671
  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;
672
+ // status and message are all that is asked for. The rest of the protocol's error answer is taken when the server
673
+ // sent it and defaulted when it did not, because a host that writes its own error -- bright answers a failed
674
+ // GetReport with {status, message} from its controller's catch -- says the one thing the reader cannot work out
675
+ // for itself, and demanding type and retryable alongside it threw that sentence away and left the viewer showing
676
+ // the status code on its own.
677
+ if (isObject(body) && body['status'] === 'error' && typeof body['message'] === 'string') {
357
678
  return {
358
679
  status: failure.status,
359
- type: body['type'],
680
+ type: typeof body['type'] === 'string' ? body['type'] : 'Error',
360
681
  message: body['message'],
361
- retryable: body['retryable'],
362
- traceId,
682
+ retryable: typeof body['retryable'] === 'boolean' ? body['retryable'] : false,
683
+ traceId: typeof body['traceId'] === 'string' ? body['traceId'] : null,
363
684
  };
364
685
  }
365
686
  if (failure.status === 0)
@@ -368,12 +689,12 @@ function readFailure(failure, action, strings = NXR_ENGLISH) {
368
689
  return described(0, formatString(strings.errorMalformed, action));
369
690
  return described(failure.status, formatString(strings.errorStatus, failure.status));
370
691
  }
371
- if (failure instanceof NxrTimeoutError) {
692
+ if (failure instanceof NxeTimeoutError) {
372
693
  return described(0, formatString(strings.errorTimeout, failure.seconds));
373
694
  }
374
- if (failure instanceof NxrMalformedAnswerError) {
375
- return failure.protocol !== NXR_VIEWER_PROTOCOL
376
- ? described(0, formatString(strings.errorProtocol, failure.protocol, NXR_VIEWER_PROTOCOL))
695
+ if (failure instanceof NxeMalformedAnswerError) {
696
+ return failure.protocol !== NXE_VIEWER_PROTOCOL
697
+ ? described(0, formatString(strings.errorProtocol, failure.protocol, NXE_VIEWER_PROTOCOL))
377
698
  : described(0, formatString(strings.errorMalformed, failure.action));
378
699
  }
379
700
  return described(0, failure instanceof Error ? failure.message : String(failure));
@@ -383,7 +704,7 @@ function readFailure(failure, action, strings = NXR_ENGLISH) {
383
704
  * The seconds a request may take when no positive `requestTimeout` applies. That includes the first
384
705
  * request, which is sent before the server's options are known. Stimulsoft's viewer uses the same value.
385
706
  */
386
- const NXR_UNSET_REQUEST_TIMEOUT_SECONDS = 1000;
707
+ const NXE_UNSET_REQUEST_TIMEOUT_SECONDS = 1000;
387
708
  /**
388
709
  * The URL of a host action. The action's path takes the place of `{action}` in `requestUrl`, and a query
389
710
  * string on the action is appended to the URL's own.
@@ -429,7 +750,7 @@ function fileNameOf(disposition) {
429
750
  }
430
751
  function timeoutOf(session) {
431
752
  const seconds = session?.options.server.requestTimeout ?? 0;
432
- return seconds > 0 ? seconds : NXR_UNSET_REQUEST_TIMEOUT_SECONDS;
753
+ return seconds > 0 ? seconds : NXE_UNSET_REQUEST_TIMEOUT_SECONDS;
433
754
  }
434
755
  function formOf(context, action, session, parameters) {
435
756
  const form = new FormData();
@@ -473,10 +794,10 @@ async function readableFailure(failure) {
473
794
  }
474
795
  /**
475
796
  * 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
797
+ * interceptors apply, and reads each answer. Every observable fails with an `NxeViewerFailure`. A request's
477
798
  * form is built when the observable is subscribed.
478
799
  */
479
- class NxrViewerClientService {
800
+ class NxeViewerClientService {
480
801
  http = inject(HttpClient);
481
802
  /** AngularViewerData, sent to the component's `action`: the token and the options. */
482
803
  openViewer(context, action) {
@@ -516,30 +837,46 @@ class NxrViewerClientService {
516
837
  const seconds = timeoutOf(session);
517
838
  return defer(() => this.http.post(actionUrl(context.requestUrl, hostAction), formOf(context, action, session, parameters))).pipe(timeout({
518
839
  first: seconds * 1000,
519
- with: () => throwError(() => new NxrTimeoutError(seconds)),
840
+ with: () => throwError(() => new NxeTimeoutError(seconds)),
520
841
  }), map((body) => read(body)), catchError((failure) => throwError(() => readFailure(failure, action, context.strings))));
521
842
  }
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 });
843
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerClientService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
844
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerClientService });
524
845
  }
525
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrViewerClientService, decorators: [{
846
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerClientService, decorators: [{
526
847
  type: Injectable
527
848
  }] });
528
849
 
529
850
  /** 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;
851
+ const NXE_KEEP_ALIVE_MS = 180_000;
531
852
  function withPages(pages, answer) {
532
853
  answer?.pages.forEach((html, offset) => pages.set(answer.startIndex + offset, html));
533
854
  return pages;
534
855
  }
856
+ /**
857
+ * Hands a file to the browser to save, under the name the server gave it. A blob URL is used rather
858
+ * than a data one because an exported report is far larger than a URL may be, and it is revoked on
859
+ * the next turn of the event loop, once the click has been taken.
860
+ */
861
+ function saveBlob(blob, fileName) {
862
+ const url = URL.createObjectURL(blob);
863
+ const link = document.createElement('a');
864
+ link.href = url;
865
+ link.download = fileName;
866
+ link.style.display = 'none';
867
+ document.body.append(link);
868
+ link.click();
869
+ link.remove();
870
+ setTimeout(() => URL.revokeObjectURL(url), 0);
871
+ }
535
872
  /**
536
873
  * One viewer's conversation with the report server, and the state the viewer shows.
537
874
  *
538
875
  * Every signal changes only in this service's methods and in the answers they handle; nothing reacts to
539
876
  * a signal changing. Each viewer provides its own session, so viewers on one page never share state.
540
877
  */
541
- class NxrViewerSessionService {
542
- client = inject(NxrViewerClientService);
878
+ class NxeViewerSessionService {
879
+ client = inject(NxeViewerClientService);
543
880
  optionsState = signal(null, /* @ts-ignore */
544
881
  ...(ngDevMode ? [{ debugName: "optionsState" }] : /* istanbul ignore next */ []));
545
882
  reportState = signal(null, /* @ts-ignore */
@@ -575,6 +912,19 @@ class NxrViewerSessionService {
575
912
  return { index, size: report.pages[index], html: this.htmlState().get(index) ?? null };
576
913
  }, /* @ts-ignore */
577
914
  ...(ngDevMode ? [{ debugName: "shownPage" }] : /* istanbul ignore next */ []));
915
+ /**
916
+ * Every page of the report, in order, each with its HTML when it has arrived. The page modes that show
917
+ * more than one page at a time lay these out and ask for the ones that come into view; a page holds its
918
+ * size, and so its place, before its HTML is here.
919
+ */
920
+ allPages = computed(() => {
921
+ const report = this.reportState();
922
+ if (!report)
923
+ return [];
924
+ const html = this.htmlState();
925
+ return report.pages.map((size, index) => ({ index, size, html: html.get(index) ?? null }));
926
+ }, /* @ts-ignore */
927
+ ...(ngDevMode ? [{ debugName: "allPages" }] : /* istanbul ignore next */ []));
578
928
  /** Emits after each successful GetReport answer. */
579
929
  reportLoaded = new Subject();
580
930
  /** Emits each failure put on show. */
@@ -629,6 +979,34 @@ class NxrViewerSessionService {
629
979
  this.pageIndexState.set(index);
630
980
  this.loadPage(index);
631
981
  }
982
+ /**
983
+ * Asks for a page's HTML without moving the page on screen, for the modes that show several pages at
984
+ * once and fetch the ones that come into view. A page already here, or already on its way, is left alone.
985
+ */
986
+ requestPage(index) {
987
+ if (this.destroyed || !Number.isInteger(index) || index < 0)
988
+ return;
989
+ this.loadPage(index);
990
+ }
991
+ /** Moves the page on screen without asking for it, for scroll tracking, which follows what is drawn. */
992
+ markCurrentPage(index) {
993
+ if (this.destroyed || !Number.isInteger(index) || index < 0 || index >= this.pageCount())
994
+ return;
995
+ this.pageIndexState.set(index);
996
+ }
997
+ /**
998
+ * ExportReport: the report as a file, handed to the browser to save. The export never times out, so
999
+ * it is tracked as a request the user waits for and the toolbar stays disabled until the file is here.
1000
+ */
1001
+ exportTo(format) {
1002
+ if (this.destroyed || this.session === null || this.context === null)
1003
+ return;
1004
+ const { context, session } = this.connection();
1005
+ this.track(this.client.exportReport(context, session, format), false, (answer) => {
1006
+ const name = answer.fileName || `${this.reportState()?.reportName ?? 'report'}.${format.toLowerCase()}`;
1007
+ saveBlob(answer.blob, name);
1008
+ });
1009
+ }
632
1010
  /** Takes the failure off show. */
633
1011
  dismissFailure() {
634
1012
  this.failureState.set(null);
@@ -700,7 +1078,7 @@ class NxrViewerSessionService {
700
1078
  const { allowAutoUpdateCache, cacheMode } = session.options.server;
701
1079
  if (!allowAutoUpdateCache || cacheMode === 'None')
702
1080
  return;
703
- this.keepAlive = interval(NXR_KEEP_ALIVE_MS)
1081
+ this.keepAlive = interval(NXE_KEEP_ALIVE_MS)
704
1082
  .pipe(exhaustMap(() => this.client.updateCache(context, session).pipe(catchError(() => EMPTY))))
705
1083
  .subscribe();
706
1084
  }
@@ -719,30 +1097,37 @@ class NxrViewerSessionService {
719
1097
  throw new Error('The viewer has no report open.');
720
1098
  return { context: this.context, session: this.session };
721
1099
  }
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 });
1100
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerSessionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1101
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerSessionService });
724
1102
  }
725
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrViewerSessionService, decorators: [{
1103
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerSessionService, decorators: [{
726
1104
  type: Injectable
727
1105
  }], ctorParameters: () => [] });
728
1106
 
729
- /** The width of the page-number box, as Stimulsoft's page control sizes it. */
730
- const PAGE_BOX_WIDTH_PX = 45;
1107
+ /** The zoom menu's fixed percentages, in Stimulsoft's order, above the two fit items. */
1108
+ const ZOOM_PRESETS = [25, 50, 75, 100, 150, 200];
1109
+ /** The view-mode menu's items, in Stimulsoft's order. */
1110
+ const VIEW_MODES = ['SinglePage', 'Continuous', 'MultiplePages'];
731
1111
  /**
732
- * The toolbar's page navigation.
1112
+ * The toolbar, as Stimulsoft 2025.1.4 draws it in its default theme, Office2022WhiteBlue.
1113
+ *
1114
+ * The items and the rules for each are Stimulsoft's (angular-viewer-toolbar-view.md §1): Save and
1115
+ * Resources, Find, the page navigation and the page control, then Zoom and View mode, each group
1116
+ * opened by a separator whose condition is the group's own. Buttons for what this viewer does not do
1117
+ * yet — Print, Open, Send Email, Bookmarks, Parameters, Editor, Full Screen — are left out; bright and
1118
+ * orbit turn all of them off.
733
1119
  *
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.
1120
+ * The look was measured on Stimulsoft's own viewer, side by side with this one: a 31 px bar with a 1 px
1121
+ * #c6c6c6 border and 3 px corners, 12 px Arial in #202020; 24 x 25 buttons with a transparent 1 px
1122
+ * border, #dadada under the mouse or while their menu is open, #e5e5e5 while selected, and a half
1123
+ * transparent icon when disabled; 1 x 21 separators in #d4d4d4; menus with a 1 px #ababab border, a
1124
+ * 7 px shadow and 24 px items. The icons are this package's own drawings in the same colours.
739
1125
  *
740
1126
  * 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.
1127
+ * unless the report panel or the whole viewer scrolls. A button is disabled at the end of the report it
1128
+ * would move past, and everything is disabled while a request the user waits for is on its way.
744
1129
  */
745
- class NxrViewerToolbarComponent {
1130
+ class NxeViewerToolbarComponent {
746
1131
  /** The options from the server, or null before the first answer, when the toolbar shows nothing. */
747
1132
  options = input(null, /* @ts-ignore */
748
1133
  ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
@@ -756,11 +1141,70 @@ class NxrViewerToolbarComponent {
756
1141
  busy = input(false, /* @ts-ignore */
757
1142
  ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
758
1143
  /** The captions to show. */
759
- strings = input(NXR_ENGLISH, /* @ts-ignore */
1144
+ strings = input(NXE_ENGLISH, /* @ts-ignore */
760
1145
  ...(ngDevMode ? [{ debugName: "strings" }] : /* istanbul ignore next */ []));
1146
+ /** The mode the pages are shown in. */
1147
+ viewMode = input('SinglePage', /* @ts-ignore */
1148
+ ...(ngDevMode ? [{ debugName: "viewMode" }] : /* istanbul ignore next */ []));
1149
+ /** The zoom the pages are drawn at, as a factor: 1 is 100 %. */
1150
+ zoom = input(1, /* @ts-ignore */
1151
+ ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
1152
+ /** Whether the find panel is open, which Stimulsoft's Find button shows as its selected state. */
1153
+ findOpen = input(false, /* @ts-ignore */
1154
+ ...(ngDevMode ? [{ debugName: "findOpen" }] : /* istanbul ignore next */ []));
761
1155
  /** The 0-based page the user asked for. */
762
1156
  goToPage = output();
763
- pageBoxWidth = PAGE_BOX_WIDTH_PX;
1157
+ /** The mode the user chose. */
1158
+ viewModeChange = output();
1159
+ /** The zoom the user chose, as a factor. */
1160
+ zoomChange = output();
1161
+ /** The user asked to fit a whole page, or the page's width, in the panel. */
1162
+ fitRequest = output();
1163
+ /** The format the user asked the report to be saved as. */
1164
+ saveRequest = output();
1165
+ /** The user pressed Find, which opens the find panel or closes it. */
1166
+ findToggle = output();
1167
+ zoomPresets = ZOOM_PRESETS;
1168
+ viewModes = VIEW_MODES;
1169
+ /** Which menu is open, or null. Stimulsoft opens one at a time and closes it on the next click. */
1170
+ openMenuState = signal(null, /* @ts-ignore */
1171
+ ...(ngDevMode ? [{ debugName: "openMenuState" }] : /* istanbul ignore next */ []));
1172
+ openMenu = this.openMenuState.asReadonly();
1173
+ /**
1174
+ * The save menu's formats, in the order Stimulsoft lists them, and only those the server turned on.
1175
+ * The button itself is left out when no format is on, as Stimulsoft forces it off then.
1176
+ */
1177
+ saveFormats = computed(() => {
1178
+ const exports = this.options()?.exports;
1179
+ if (!exports)
1180
+ return [];
1181
+ const formats = [];
1182
+ if (exports.showExportToPdf)
1183
+ formats.push('Pdf');
1184
+ if (exports.showExportToHtml)
1185
+ formats.push('Html');
1186
+ return formats;
1187
+ }, /* @ts-ignore */
1188
+ ...(ngDevMode ? [{ debugName: "saveFormats" }] : /* istanbul ignore next */ []));
1189
+ showSave = computed(() => this.toolbar()?.showSaveButton === true && this.saveFormats().length > 0, /* @ts-ignore */
1190
+ ...(ngDevMode ? [{ debugName: "showSave" }] : /* istanbul ignore next */ []));
1191
+ /** The caption Stimulsoft's save menu gives a format. */
1192
+ formatCaption(format) {
1193
+ return format === 'Pdf' ? this.strings().formatPdf : this.strings().formatHtml;
1194
+ }
1195
+ save(format) {
1196
+ this.openMenuState.set(null);
1197
+ this.saveRequest.emit(format);
1198
+ }
1199
+ /**
1200
+ * The zoom as a rounded percentage, as Stimulsoft captions the button, and empty when the zoom is
1201
+ * unknown or zero.
1202
+ */
1203
+ zoomCaption = computed(() => {
1204
+ const zoom = this.zoom();
1205
+ return Number.isFinite(zoom) && zoom > 0 ? `${Math.round(zoom * 100)}%` : '';
1206
+ }, /* @ts-ignore */
1207
+ ...(ngDevMode ? [{ debugName: "zoomCaption" }] : /* istanbul ignore next */ []));
764
1208
  toolbar = computed(() => this.options()?.toolbar ?? null, /* @ts-ignore */
765
1209
  ...(ngDevMode ? [{ debugName: "toolbar" }] : /* istanbul ignore next */ []));
766
1210
  appearance = computed(() => this.options()?.appearance ?? null, /* @ts-ignore */
@@ -781,6 +1225,13 @@ class NxrViewerToolbarComponent {
781
1225
  !appearance.fullScreenMode);
782
1226
  }, /* @ts-ignore */
783
1227
  ...(ngDevMode ? [{ debugName: "navigationDisabled" }] : /* istanbul ignore next */ []));
1228
+ /** Save, Zoom and View mode show their captions unless the server turns captions off. */
1229
+ showCaptions = computed(() => this.toolbar()?.showButtonCaptions !== false, /* @ts-ignore */
1230
+ ...(ngDevMode ? [{ debugName: "showCaptions" }] : /* istanbul ignore next */ []));
1231
+ showResources = computed(() => this.toolbar()?.showResourcesButton === true, /* @ts-ignore */
1232
+ ...(ngDevMode ? [{ debugName: "showResources" }] : /* istanbul ignore next */ []));
1233
+ showFind = computed(() => this.toolbar()?.showFindButton === true, /* @ts-ignore */
1234
+ ...(ngDevMode ? [{ debugName: "showFind" }] : /* istanbul ignore next */ []));
784
1235
  showFirstPage = computed(() => this.toolbar()?.showFirstPageButton === true, /* @ts-ignore */
785
1236
  ...(ngDevMode ? [{ debugName: "showFirstPage" }] : /* istanbul ignore next */ []));
786
1237
  showPreviousPage = computed(() => this.toolbar()?.showPreviousPageButton === true, /* @ts-ignore */
@@ -791,6 +1242,46 @@ class NxrViewerToolbarComponent {
791
1242
  ...(ngDevMode ? [{ debugName: "showNextPage" }] : /* istanbul ignore next */ []));
792
1243
  showLastPage = computed(() => this.toolbar()?.showLastPageButton === true, /* @ts-ignore */
793
1244
  ...(ngDevMode ? [{ debugName: "showLastPage" }] : /* istanbul ignore next */ []));
1245
+ showZoom = computed(() => this.toolbar()?.showZoomButton === true, /* @ts-ignore */
1246
+ ...(ngDevMode ? [{ debugName: "showZoom" }] : /* istanbul ignore next */ []));
1247
+ showViewMode = computed(() => this.toolbar()?.showViewModeButton === true, /* @ts-ignore */
1248
+ ...(ngDevMode ? [{ debugName: "showViewMode" }] : /* istanbul ignore next */ []));
1249
+ /**
1250
+ * Stimulsoft opens the panels group — Bookmarks, Parameters, Resources — only when Bookmarks or
1251
+ * Parameters is on; Resources alone follows Save with no separator, as bright and orbit show it.
1252
+ */
1253
+ showPanelsSeparator = computed(() => this.toolbar()?.showBookmarksButton === true || this.toolbar()?.showParametersButton === true, /* @ts-ignore */
1254
+ ...(ngDevMode ? [{ debugName: "showPanelsSeparator" }] : /* istanbul ignore next */ []));
1255
+ /** Stimulsoft opens the Find group whenever Find or Editor is on, whatever comes before it. */
1256
+ showFindSeparator = computed(() => this.toolbar()?.showFindButton === true || this.toolbar()?.showEditorButton === true, /* @ts-ignore */
1257
+ ...(ngDevMode ? [{ debugName: "showFindSeparator" }] : /* istanbul ignore next */ []));
1258
+ /** And the navigation group whenever any of its five items is on. */
1259
+ showNavigationSeparator = computed(() => this.showFirstPage() ||
1260
+ this.showPreviousPage() ||
1261
+ this.showCurrentPage() ||
1262
+ this.showNextPage() ||
1263
+ this.showLastPage(), /* @ts-ignore */
1264
+ ...(ngDevMode ? [{ debugName: "showNavigationSeparator" }] : /* istanbul ignore next */ []));
1265
+ /** Whether a preset is the zoom on show, so its menu item reads as chosen. */
1266
+ isZoom(percent) {
1267
+ return Math.round(this.zoom() * 100) === percent;
1268
+ }
1269
+ /** Opens a menu, or closes it when it is the one already open. */
1270
+ toggleMenu(menu) {
1271
+ this.openMenuState.update((open) => (open === menu ? null : menu));
1272
+ }
1273
+ chooseZoom(percent) {
1274
+ this.openMenuState.set(null);
1275
+ this.zoomChange.emit(percent / 100);
1276
+ }
1277
+ fit(what) {
1278
+ this.openMenuState.set(null);
1279
+ this.fitRequest.emit(what);
1280
+ }
1281
+ chooseViewMode(mode) {
1282
+ this.openMenuState.set(null);
1283
+ this.viewModeChange.emit(mode);
1284
+ }
794
1285
  atStart = computed(() => this.busy() || this.navigationDisabled() || this.pageIndex() <= 0, /* @ts-ignore */
795
1286
  ...(ngDevMode ? [{ debugName: "atStart" }] : /* istanbul ignore next */ []));
796
1287
  atEnd = computed(() => this.busy() || this.navigationDisabled() || this.pageIndex() >= this.pageCount() - 1, /* @ts-ignore */
@@ -803,12 +1294,17 @@ class NxrViewerToolbarComponent {
803
1294
  ...(ngDevMode ? [{ debugName: "fontColor" }] : /* istanbul ignore next */ []));
804
1295
  fontFamily = computed(() => this.toolbar()?.fontFamily || null, /* @ts-ignore */
805
1296
  ...(ngDevMode ? [{ debugName: "fontFamily" }] : /* istanbul ignore next */ []));
806
- /** A border only in `Simple`; `Separated` has none, as Stimulsoft's toolbar does. */
1297
+ /**
1298
+ * The server's border colour in `Simple`, and none at all in `Separated`, as Stimulsoft's toolbar
1299
+ * does; without a colour the theme's own border stays.
1300
+ */
807
1301
  border = computed(() => {
808
1302
  const toolbar = this.toolbar();
809
- if (!toolbar || toolbar.displayMode === 'Separated' || !toolbar.borderColor)
1303
+ if (!toolbar)
810
1304
  return null;
811
- return `1px solid ${toolbar.borderColor}`;
1305
+ if (toolbar.displayMode === 'Separated')
1306
+ return '0';
1307
+ return toolbar.borderColor ? `1px solid ${toolbar.borderColor}` : null;
812
1308
  }, /* @ts-ignore */
813
1309
  ...(ngDevMode ? [{ debugName: "border" }] : /* istanbul ignore next */ []));
814
1310
  justifyContent = computed(() => {
@@ -838,153 +1334,614 @@ class NxrViewerToolbarComponent {
838
1334
  return;
839
1335
  this.goToPage.emit(index);
840
1336
  }
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 });
1337
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1338
+ 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 }, findOpen: { classPropertyName: "findOpen", publicName: "findOpen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { goToPage: "goToPage", viewModeChange: "viewModeChange", zoomChange: "zoomChange", fitRequest: "fitRequest", saveRequest: "saveRequest", findToggle: "findToggle" }, host: { attributes: { "role": "toolbar" }, properties: { "style.color": "fontColor()", "style.font-family": "fontFamily()", "class.nxe-busy": "busy()" }, classAttribute: "nxe-viewer-toolbar" }, ngImport: i0, template: `
1339
+ <div
1340
+ class="nxe-toolbar-bar"
1341
+ [style.background-color]="backgroundColor()"
1342
+ [style.border]="border()"
1343
+ [style.justify-content]="justifyContent()"
1344
+ >
1345
+ @if (showSave()) {
1346
+ <div class="nxe-toolbar-menu">
1347
+ <button
1348
+ type="button"
1349
+ class="nxe-toolbar-button"
1350
+ [attr.aria-label]="strings().SaveToolTip"
1351
+ [attr.aria-expanded]="openMenu() === 'save'"
1352
+ (click)="toggleMenu('save')"
1353
+ >
1354
+ <span class="nxe-toolbar-icon">
1355
+ <svg viewBox="0 0 16 16" aria-hidden="true">
1356
+ <path d="M1.5 1.5h13v13h-13z" fill="#d890d8" stroke="#903098" />
1357
+ <path d="M4.5 1.5h7v4.5h-7z" fill="#fff" stroke="#903098" />
1358
+ <path d="M3.5 9.5h9v5h-9z" fill="#fff" stroke="#903098" />
1359
+ </svg>
1360
+ </span>
1361
+ @if (showCaptions()) {
1362
+ <span class="nxe-toolbar-caption">{{ strings().Save }}</span>
1363
+ }
1364
+ <span class="nxe-toolbar-arrow">
1365
+ <svg viewBox="0 0 8 8" aria-hidden="true">
1366
+ <path d="M1.5 2.5 4 5l2.5-2.5" fill="none" stroke="#787878" />
1367
+ </svg>
1368
+ </span>
1369
+ </button>
1370
+ @if (openMenu() === 'save') {
1371
+ <div class="nxe-toolbar-popup" role="menu">
1372
+ @for (format of saveFormats(); track format) {
1373
+ <button type="button" class="nxe-toolbar-item" role="menuitem" (click)="save(format)">
1374
+ <span class="nxe-toolbar-item-icon">
1375
+ <svg viewBox="0 0 16 16" aria-hidden="true">
1376
+ <path d="M3.5.5h7l3 3v12h-10z" fill="#fff" stroke="#6e6e6e" />
1377
+ <path d="M10.5.5v3h3" fill="none" stroke="#6e6e6e" />
1378
+ @if (format === 'Pdf') {
1379
+ <path d="M.5 6.5h11v5H.5z" fill="#f6d0d0" stroke="#d83b3b" />
1380
+ <text x="6" y="10.4" fill="#d83b3b" font-family="Arial" font-size="4.2" font-weight="bold" text-anchor="middle">PDF</text>
1381
+ } @else {
1382
+ <path d="M.5 6.5h13v5H.5z" fill="#d6e9f8" stroke="#2088d0" />
1383
+ <text x="7" y="10.4" fill="#2088d0" font-family="Arial" font-size="4" font-weight="bold" text-anchor="middle">HTML</text>
1384
+ }
1385
+ </svg>
1386
+ </span>
1387
+ <span class="nxe-toolbar-item-text">{{ formatCaption(format) }}</span>
1388
+ </button>
1389
+ }
1390
+ </div>
1391
+ }
1392
+ </div>
1393
+ }
1394
+ @if (showPanelsSeparator()) {
1395
+ <span class="nxe-toolbar-separator"></span>
1396
+ }
1397
+ @if (showResources()) {
1398
+ <!-- A report's resources are not served yet, so there are none to show: Stimulsoft disables the button then. -->
1399
+ <button type="button" class="nxe-toolbar-button" disabled [attr.aria-label]="strings().Resources">
1400
+ <span class="nxe-toolbar-icon">
1401
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="none" stroke="#313131">
1402
+ <path d="M6.5 1.5h5l3 3v8h-8z" fill="#fff" />
1403
+ <path d="M11.5 1.5v3h3" />
1404
+ <path d="M1.5 4.5h5v10h-5z" fill="#fff" />
1405
+ <path d="M2.5 7.5h3m-3 2h3m-3 2h3" stroke="#717171" />
1406
+ </svg>
1407
+ </span>
1408
+ </button>
1409
+ }
1410
+ @if (showFindSeparator()) {
1411
+ <span class="nxe-toolbar-separator"></span>
1412
+ }
1413
+ @if (showFind()) {
1414
+ <button
1415
+ type="button"
1416
+ class="nxe-toolbar-button"
1417
+ [class.nxe-toolbar-selected]="findOpen()"
1418
+ [attr.aria-label]="strings().FindToolTip"
1419
+ [attr.aria-pressed]="findOpen()"
1420
+ (click)="findToggle.emit()"
1421
+ >
1422
+ <span class="nxe-toolbar-icon">
1423
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="none" stroke="#383838">
1424
+ <circle cx="9.5" cy="6.5" r="4.4" stroke-width="1.4" />
1425
+ <path d="M6.4 9.6 2 14" stroke-width="2" stroke-linecap="round" />
1426
+ </svg>
1427
+ </span>
1428
+ </button>
1429
+ }
1430
+ @if (showNavigationSeparator()) {
1431
+ <span class="nxe-toolbar-separator"></span>
1432
+ }
1433
+ @if (showFirstPage()) {
1434
+ <button
1435
+ type="button"
1436
+ class="nxe-toolbar-button"
1437
+ [disabled]="atStart()"
1438
+ [attr.aria-label]="strings().FirstPage"
1439
+ (click)="goTo(0)"
1440
+ >
1441
+ <span class="nxe-toolbar-icon">
1442
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="#81c1f1" stroke="#0161b1" stroke-linejoin="round">
1443
+ <path d="M3.5 2.5h2v11h-2z" />
1444
+ <path d="M12.5 2.5v11l-6-5.5z" />
1445
+ </svg>
1446
+ </span>
1447
+ </button>
1448
+ }
1449
+ @if (showPreviousPage()) {
1450
+ <button
1451
+ type="button"
1452
+ class="nxe-toolbar-button"
1453
+ [disabled]="atStart()"
1454
+ [attr.aria-label]="strings().PrevPage"
1455
+ (click)="goTo(pageIndex() - 1)"
1456
+ >
1457
+ <span class="nxe-toolbar-icon">
1458
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="#81c1f1" stroke="#0161b1" stroke-linejoin="round">
1459
+ <path d="M10.5 2.5v11l-6-5.5z" />
1460
+ </svg>
1461
+ </span>
1462
+ </button>
1463
+ }
1464
+ @if (showCurrentPage()) {
1465
+ <span class="nxe-page-control">
1466
+ <span class="nxe-page-label">{{ strings().Page }}</span>
1467
+ <span class="nxe-page-box-cell" [class.nxe-page-box-off]="pageControlDisabled()">
1468
+ <input
1469
+ type="text"
1470
+ inputmode="numeric"
1471
+ class="nxe-page-box"
1472
+ [disabled]="pageControlDisabled()"
1473
+ [value]="pageIndex() + 1"
1474
+ [attr.aria-label]="strings().Page"
1475
+ (keydown.enter)="enterPage($event)"
1476
+ />
1477
+ </span>
1478
+ <span class="nxe-page-label">{{ strings().PageOf }}</span>
1479
+ <span class="nxe-page-label">{{ pageCount() }}</span>
1480
+ </span>
1481
+ }
1482
+ @if (showNextPage()) {
1483
+ <button
1484
+ type="button"
1485
+ class="nxe-toolbar-button"
1486
+ [disabled]="atEnd()"
1487
+ [attr.aria-label]="strings().NextPage"
1488
+ (click)="goTo(pageIndex() + 1)"
1489
+ >
1490
+ <span class="nxe-toolbar-icon">
1491
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="#81c1f1" stroke="#0161b1" stroke-linejoin="round">
1492
+ <path d="M5.5 2.5v11l6-5.5z" />
1493
+ </svg>
1494
+ </span>
1495
+ </button>
1496
+ }
1497
+ @if (showLastPage()) {
1498
+ <button
1499
+ type="button"
1500
+ class="nxe-toolbar-button"
1501
+ [disabled]="atEnd()"
1502
+ [attr.aria-label]="strings().LastPage"
1503
+ (click)="goTo(pageCount() - 1)"
1504
+ >
1505
+ <span class="nxe-toolbar-icon">
1506
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="#81c1f1" stroke="#0161b1" stroke-linejoin="round">
1507
+ <path d="M3.5 2.5v11l6-5.5z" />
1508
+ <path d="M10.5 2.5h2v11h-2z" />
1509
+ </svg>
1510
+ </span>
1511
+ </button>
1512
+ }
1513
+ @if (showZoom() || showViewMode()) {
1514
+ <span class="nxe-toolbar-separator"></span>
1515
+ }
1516
+ @if (showZoom()) {
1517
+ <div class="nxe-toolbar-menu">
1518
+ <button
1519
+ type="button"
1520
+ class="nxe-toolbar-button"
1521
+ [attr.aria-label]="strings().ZoomToolTip"
1522
+ [attr.aria-expanded]="openMenu() === 'zoom'"
1523
+ (click)="toggleMenu('zoom')"
1524
+ >
1525
+ <span class="nxe-toolbar-icon">
1526
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="none">
1527
+ <path d="M2.5 1.5h6l3 3v5M8.5 1.5v3h3M2.5 1.5v13h7" stroke="#383838" />
1528
+ <circle cx="11.5" cy="11.5" r="2" stroke="#2088d0" />
1529
+ <path d="m13 13 1.8 1.8" stroke="#2088d0" />
1530
+ </svg>
1531
+ </span>
1532
+ @if (showCaptions()) {
1533
+ <span class="nxe-toolbar-caption">{{ zoomCaption() }}</span>
1534
+ }
1535
+ <span class="nxe-toolbar-arrow">
1536
+ <svg viewBox="0 0 8 8" aria-hidden="true">
1537
+ <path d="M1.5 2.5 4 5l2.5-2.5" fill="none" stroke="#787878" />
1538
+ </svg>
1539
+ </span>
1540
+ </button>
1541
+ @if (openMenu() === 'zoom') {
1542
+ <div class="nxe-toolbar-popup" role="menu">
1543
+ @for (percent of zoomPresets; track percent) {
1544
+ <button
1545
+ type="button"
1546
+ class="nxe-toolbar-item"
1547
+ role="menuitem"
1548
+ [class.nxe-toolbar-item-on]="isZoom(percent)"
1549
+ (click)="chooseZoom(percent)"
1550
+ >
1551
+ <span class="nxe-toolbar-item-icon"></span>
1552
+ <span class="nxe-toolbar-item-text">{{ percent }}%</span>
1553
+ </button>
1554
+ }
1555
+ <span class="nxe-toolbar-item-separator"></span>
1556
+ <button type="button" class="nxe-toolbar-item" role="menuitem" (click)="fit('page')">
1557
+ <span class="nxe-toolbar-item-icon">
1558
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="none">
1559
+ <path d="M3.5 1.5h9v13h-9z" fill="#fff" stroke="#383838" />
1560
+ <path d="M8 3.5v9M6 5.5l2-2 2 2M6 10.5l2 2 2-2" stroke="#2088d0" />
1561
+ </svg>
1562
+ </span>
1563
+ <span class="nxe-toolbar-item-text">{{ strings().ZoomOnePage }}</span>
1564
+ </button>
1565
+ <button type="button" class="nxe-toolbar-item" role="menuitem" (click)="fit('width')">
1566
+ <span class="nxe-toolbar-item-icon">
1567
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="none">
1568
+ <path d="M1.5 3.5h13v9h-13z" fill="#fff" stroke="#383838" />
1569
+ <path d="M3.5 8h9M5.5 6l-2 2 2 2M10.5 6l2 2-2 2" stroke="#2088d0" />
1570
+ </svg>
1571
+ </span>
1572
+ <span class="nxe-toolbar-item-text">{{ strings().ZoomPageWidth }}</span>
1573
+ </button>
1574
+ </div>
1575
+ }
1576
+ </div>
1577
+ }
1578
+ @if (showViewMode()) {
1579
+ <div class="nxe-toolbar-menu">
1580
+ <button
1581
+ type="button"
1582
+ class="nxe-toolbar-button"
1583
+ [attr.aria-label]="strings().ViewModeToolTip"
1584
+ [attr.aria-expanded]="openMenu() === 'viewMode'"
1585
+ (click)="toggleMenu('viewMode')"
1586
+ >
1587
+ <!-- Stimulsoft's button keeps the single-page icon whatever the mode. -->
1588
+ <span class="nxe-toolbar-icon">
1589
+ <svg viewBox="0 0 16 16" aria-hidden="true">
1590
+ <path d="M2.5 1.5h11v13h-11z" fill="#fff" stroke="#383838" />
1591
+ </svg>
1592
+ </span>
1593
+ @if (showCaptions()) {
1594
+ <span class="nxe-toolbar-caption">{{ strings()[viewMode()] }}</span>
1595
+ }
1596
+ <span class="nxe-toolbar-arrow">
1597
+ <svg viewBox="0 0 8 8" aria-hidden="true">
1598
+ <path d="M1.5 2.5 4 5l2.5-2.5" fill="none" stroke="#787878" />
1599
+ </svg>
1600
+ </span>
1601
+ </button>
1602
+ @if (openMenu() === 'viewMode') {
1603
+ <!-- Stimulsoft marks no item here as the current one, unlike the zoom menu. -->
1604
+ <div class="nxe-toolbar-popup" role="menu">
1605
+ @for (mode of viewModes; track mode) {
1606
+ <button type="button" class="nxe-toolbar-item" role="menuitem" (click)="chooseViewMode(mode)">
1607
+ <span class="nxe-toolbar-item-icon">
1608
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="#fff" stroke="#383838">
1609
+ @switch (mode) {
1610
+ @case ('SinglePage') {
1611
+ <path d="M2.5 1.5h11v13h-11z" />
1612
+ }
1613
+ @case ('Continuous') {
1614
+ <path d="M2.5 1.5h11v4h-11zm0 6h11v7h-11z" />
1615
+ }
1616
+ @case ('MultiplePages') {
1617
+ <path d="M2.5 2.5h5v5h-5zm6 0h5v5h-5zm-6 6h5v5h-5zm6 0h5v5h-5z" />
1618
+ }
1619
+ }
1620
+ </svg>
1621
+ </span>
1622
+ <span class="nxe-toolbar-item-text">{{ strings()[mode] }}</span>
1623
+ </button>
1624
+ }
1625
+ </div>
1626
+ }
1627
+ </div>
1628
+ }
1629
+ </div>
1630
+ `, isInline: true, styles: [":host{display:block;box-sizing:border-box;padding-top:2px;position:relative;-webkit-user-select:none;user-select:none;font-family:Arial;font-size:12px;color:#202020}:host(.nxe-busy):after{content:\"\";position:absolute;inset:0;cursor:default}.nxe-toolbar-bar{display:flex;align-items:center;box-sizing:border-box;height:31px;padding:1px;border:1px solid #c6c6c6;border-radius:3px;background:#fff;cursor:default}.nxe-toolbar-button{display:inline-flex;align-items:center;box-sizing:border-box;height:25px;margin:1px;padding:0;border:1px solid transparent;border-radius:3px;background:transparent;color:inherit;font:inherit;white-space:nowrap;cursor:default}.nxe-toolbar-button:hover:not(:disabled),.nxe-toolbar-button[aria-expanded=true]{background:#dadada}.nxe-toolbar-button.nxe-toolbar-selected{background:#e5e5e5}.nxe-toolbar-button:disabled{color:#d6d3d6}.nxe-toolbar-button:disabled svg{opacity:.5}.nxe-toolbar-icon{display:flex;flex:none;box-sizing:border-box;width:22px;padding-left:3px}.nxe-toolbar-icon svg{width:16px;height:16px}.nxe-toolbar-arrow{display:flex;flex:none;justify-content:center;width:18px}.nxe-toolbar-arrow svg{width:8px;height:8px}.nxe-toolbar-separator{flex:none;width:1px;height:21px;background:#d4d4d4}.nxe-page-control{display:inline-flex;align-items:center;height:23px;margin:0 1px;white-space:nowrap}.nxe-page-label,.nxe-page-box-cell{padding:0 2px}.nxe-page-box-cell{display:flex}.nxe-page-box{box-sizing:border-box;width:51px;height:23px;margin:0;padding:0 0 0 4px;border:1px solid #ababab;border-radius:3px;background:#fff;color:inherit;font:inherit;line-height:21px;text-align:left;outline:none}.nxe-page-box:hover:not(:disabled){border-color:#6b6b6b}.nxe-page-box:disabled{border-color:#e1e1e1;background:#f7f7f7;color:inherit;-webkit-text-fill-color:currentColor;opacity:1}.nxe-page-box-off{opacity:.5}.nxe-toolbar-menu{position:relative;display:flex;align-items:center}.nxe-toolbar-popup{position:absolute;top:100%;left:0;z-index:10;display:flex;flex-direction:column;gap:1px;box-sizing:border-box;padding:1px;border:1px solid #ababab;border-radius:3px;background:#fff;box-shadow:0 0 7px #0000004d}.nxe-toolbar-item{display:flex;align-items:center;flex:none;box-sizing:border-box;height:24px;margin:0 1px;padding:0;border:0;border-radius:3px;background:transparent;color:#202020;font:inherit;text-align:left;white-space:nowrap;cursor:default}.nxe-toolbar-item:first-child{margin-top:1px}.nxe-toolbar-item:last-child{margin-bottom:1px}.nxe-toolbar-item-on{background:#e5e5e5}.nxe-toolbar-item:hover{background:#dadada}.nxe-toolbar-item-icon{display:flex;flex:none;box-sizing:border-box;width:22px;padding-left:3px}.nxe-toolbar-item-icon svg{width:16px;height:16px}.nxe-toolbar-item-text{padding:0 20px 0 7px}.nxe-toolbar-item-separator{flex:none;height:1px;margin:0 2px 0 30px;background:#d4d4d4}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
908
1631
  }
909
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrViewerToolbarComponent, decorators: [{
1632
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerToolbarComponent, decorators: [{
910
1633
  type: Component,
911
- args: [{ selector: 'nxr-viewer-toolbar', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
912
- class: 'nxr-viewer-toolbar',
1634
+ args: [{ selector: 'nxe-viewer-toolbar', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
1635
+ class: 'nxe-viewer-toolbar',
913
1636
  role: 'toolbar',
914
- '[style.background-color]': 'backgroundColor()',
915
- '[style.border]': 'border()',
916
1637
  '[style.color]': 'fontColor()',
917
1638
  '[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"] }] } });
1639
+ '[class.nxe-busy]': 'busy()',
1640
+ }, template: `
1641
+ <div
1642
+ class="nxe-toolbar-bar"
1643
+ [style.background-color]="backgroundColor()"
1644
+ [style.border]="border()"
1645
+ [style.justify-content]="justifyContent()"
1646
+ >
1647
+ @if (showSave()) {
1648
+ <div class="nxe-toolbar-menu">
1649
+ <button
1650
+ type="button"
1651
+ class="nxe-toolbar-button"
1652
+ [attr.aria-label]="strings().SaveToolTip"
1653
+ [attr.aria-expanded]="openMenu() === 'save'"
1654
+ (click)="toggleMenu('save')"
1655
+ >
1656
+ <span class="nxe-toolbar-icon">
1657
+ <svg viewBox="0 0 16 16" aria-hidden="true">
1658
+ <path d="M1.5 1.5h13v13h-13z" fill="#d890d8" stroke="#903098" />
1659
+ <path d="M4.5 1.5h7v4.5h-7z" fill="#fff" stroke="#903098" />
1660
+ <path d="M3.5 9.5h9v5h-9z" fill="#fff" stroke="#903098" />
1661
+ </svg>
1662
+ </span>
1663
+ @if (showCaptions()) {
1664
+ <span class="nxe-toolbar-caption">{{ strings().Save }}</span>
1665
+ }
1666
+ <span class="nxe-toolbar-arrow">
1667
+ <svg viewBox="0 0 8 8" aria-hidden="true">
1668
+ <path d="M1.5 2.5 4 5l2.5-2.5" fill="none" stroke="#787878" />
1669
+ </svg>
1670
+ </span>
1671
+ </button>
1672
+ @if (openMenu() === 'save') {
1673
+ <div class="nxe-toolbar-popup" role="menu">
1674
+ @for (format of saveFormats(); track format) {
1675
+ <button type="button" class="nxe-toolbar-item" role="menuitem" (click)="save(format)">
1676
+ <span class="nxe-toolbar-item-icon">
1677
+ <svg viewBox="0 0 16 16" aria-hidden="true">
1678
+ <path d="M3.5.5h7l3 3v12h-10z" fill="#fff" stroke="#6e6e6e" />
1679
+ <path d="M10.5.5v3h3" fill="none" stroke="#6e6e6e" />
1680
+ @if (format === 'Pdf') {
1681
+ <path d="M.5 6.5h11v5H.5z" fill="#f6d0d0" stroke="#d83b3b" />
1682
+ <text x="6" y="10.4" fill="#d83b3b" font-family="Arial" font-size="4.2" font-weight="bold" text-anchor="middle">PDF</text>
1683
+ } @else {
1684
+ <path d="M.5 6.5h13v5H.5z" fill="#d6e9f8" stroke="#2088d0" />
1685
+ <text x="7" y="10.4" fill="#2088d0" font-family="Arial" font-size="4" font-weight="bold" text-anchor="middle">HTML</text>
1686
+ }
1687
+ </svg>
1688
+ </span>
1689
+ <span class="nxe-toolbar-item-text">{{ formatCaption(format) }}</span>
1690
+ </button>
1691
+ }
1692
+ </div>
1693
+ }
1694
+ </div>
1695
+ }
1696
+ @if (showPanelsSeparator()) {
1697
+ <span class="nxe-toolbar-separator"></span>
1698
+ }
1699
+ @if (showResources()) {
1700
+ <!-- A report's resources are not served yet, so there are none to show: Stimulsoft disables the button then. -->
1701
+ <button type="button" class="nxe-toolbar-button" disabled [attr.aria-label]="strings().Resources">
1702
+ <span class="nxe-toolbar-icon">
1703
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="none" stroke="#313131">
1704
+ <path d="M6.5 1.5h5l3 3v8h-8z" fill="#fff" />
1705
+ <path d="M11.5 1.5v3h3" />
1706
+ <path d="M1.5 4.5h5v10h-5z" fill="#fff" />
1707
+ <path d="M2.5 7.5h3m-3 2h3m-3 2h3" stroke="#717171" />
1708
+ </svg>
1709
+ </span>
1710
+ </button>
1711
+ }
1712
+ @if (showFindSeparator()) {
1713
+ <span class="nxe-toolbar-separator"></span>
1714
+ }
1715
+ @if (showFind()) {
1716
+ <button
1717
+ type="button"
1718
+ class="nxe-toolbar-button"
1719
+ [class.nxe-toolbar-selected]="findOpen()"
1720
+ [attr.aria-label]="strings().FindToolTip"
1721
+ [attr.aria-pressed]="findOpen()"
1722
+ (click)="findToggle.emit()"
1723
+ >
1724
+ <span class="nxe-toolbar-icon">
1725
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="none" stroke="#383838">
1726
+ <circle cx="9.5" cy="6.5" r="4.4" stroke-width="1.4" />
1727
+ <path d="M6.4 9.6 2 14" stroke-width="2" stroke-linecap="round" />
1728
+ </svg>
1729
+ </span>
1730
+ </button>
1731
+ }
1732
+ @if (showNavigationSeparator()) {
1733
+ <span class="nxe-toolbar-separator"></span>
1734
+ }
1735
+ @if (showFirstPage()) {
1736
+ <button
1737
+ type="button"
1738
+ class="nxe-toolbar-button"
1739
+ [disabled]="atStart()"
1740
+ [attr.aria-label]="strings().FirstPage"
1741
+ (click)="goTo(0)"
1742
+ >
1743
+ <span class="nxe-toolbar-icon">
1744
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="#81c1f1" stroke="#0161b1" stroke-linejoin="round">
1745
+ <path d="M3.5 2.5h2v11h-2z" />
1746
+ <path d="M12.5 2.5v11l-6-5.5z" />
1747
+ </svg>
1748
+ </span>
1749
+ </button>
1750
+ }
1751
+ @if (showPreviousPage()) {
1752
+ <button
1753
+ type="button"
1754
+ class="nxe-toolbar-button"
1755
+ [disabled]="atStart()"
1756
+ [attr.aria-label]="strings().PrevPage"
1757
+ (click)="goTo(pageIndex() - 1)"
1758
+ >
1759
+ <span class="nxe-toolbar-icon">
1760
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="#81c1f1" stroke="#0161b1" stroke-linejoin="round">
1761
+ <path d="M10.5 2.5v11l-6-5.5z" />
1762
+ </svg>
1763
+ </span>
1764
+ </button>
1765
+ }
1766
+ @if (showCurrentPage()) {
1767
+ <span class="nxe-page-control">
1768
+ <span class="nxe-page-label">{{ strings().Page }}</span>
1769
+ <span class="nxe-page-box-cell" [class.nxe-page-box-off]="pageControlDisabled()">
1770
+ <input
1771
+ type="text"
1772
+ inputmode="numeric"
1773
+ class="nxe-page-box"
1774
+ [disabled]="pageControlDisabled()"
1775
+ [value]="pageIndex() + 1"
1776
+ [attr.aria-label]="strings().Page"
1777
+ (keydown.enter)="enterPage($event)"
1778
+ />
1779
+ </span>
1780
+ <span class="nxe-page-label">{{ strings().PageOf }}</span>
1781
+ <span class="nxe-page-label">{{ pageCount() }}</span>
1782
+ </span>
1783
+ }
1784
+ @if (showNextPage()) {
1785
+ <button
1786
+ type="button"
1787
+ class="nxe-toolbar-button"
1788
+ [disabled]="atEnd()"
1789
+ [attr.aria-label]="strings().NextPage"
1790
+ (click)="goTo(pageIndex() + 1)"
1791
+ >
1792
+ <span class="nxe-toolbar-icon">
1793
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="#81c1f1" stroke="#0161b1" stroke-linejoin="round">
1794
+ <path d="M5.5 2.5v11l6-5.5z" />
1795
+ </svg>
1796
+ </span>
1797
+ </button>
1798
+ }
1799
+ @if (showLastPage()) {
1800
+ <button
1801
+ type="button"
1802
+ class="nxe-toolbar-button"
1803
+ [disabled]="atEnd()"
1804
+ [attr.aria-label]="strings().LastPage"
1805
+ (click)="goTo(pageCount() - 1)"
1806
+ >
1807
+ <span class="nxe-toolbar-icon">
1808
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="#81c1f1" stroke="#0161b1" stroke-linejoin="round">
1809
+ <path d="M3.5 2.5v11l6-5.5z" />
1810
+ <path d="M10.5 2.5h2v11h-2z" />
1811
+ </svg>
1812
+ </span>
1813
+ </button>
1814
+ }
1815
+ @if (showZoom() || showViewMode()) {
1816
+ <span class="nxe-toolbar-separator"></span>
1817
+ }
1818
+ @if (showZoom()) {
1819
+ <div class="nxe-toolbar-menu">
1820
+ <button
1821
+ type="button"
1822
+ class="nxe-toolbar-button"
1823
+ [attr.aria-label]="strings().ZoomToolTip"
1824
+ [attr.aria-expanded]="openMenu() === 'zoom'"
1825
+ (click)="toggleMenu('zoom')"
1826
+ >
1827
+ <span class="nxe-toolbar-icon">
1828
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="none">
1829
+ <path d="M2.5 1.5h6l3 3v5M8.5 1.5v3h3M2.5 1.5v13h7" stroke="#383838" />
1830
+ <circle cx="11.5" cy="11.5" r="2" stroke="#2088d0" />
1831
+ <path d="m13 13 1.8 1.8" stroke="#2088d0" />
1832
+ </svg>
1833
+ </span>
1834
+ @if (showCaptions()) {
1835
+ <span class="nxe-toolbar-caption">{{ zoomCaption() }}</span>
1836
+ }
1837
+ <span class="nxe-toolbar-arrow">
1838
+ <svg viewBox="0 0 8 8" aria-hidden="true">
1839
+ <path d="M1.5 2.5 4 5l2.5-2.5" fill="none" stroke="#787878" />
1840
+ </svg>
1841
+ </span>
1842
+ </button>
1843
+ @if (openMenu() === 'zoom') {
1844
+ <div class="nxe-toolbar-popup" role="menu">
1845
+ @for (percent of zoomPresets; track percent) {
1846
+ <button
1847
+ type="button"
1848
+ class="nxe-toolbar-item"
1849
+ role="menuitem"
1850
+ [class.nxe-toolbar-item-on]="isZoom(percent)"
1851
+ (click)="chooseZoom(percent)"
1852
+ >
1853
+ <span class="nxe-toolbar-item-icon"></span>
1854
+ <span class="nxe-toolbar-item-text">{{ percent }}%</span>
1855
+ </button>
1856
+ }
1857
+ <span class="nxe-toolbar-item-separator"></span>
1858
+ <button type="button" class="nxe-toolbar-item" role="menuitem" (click)="fit('page')">
1859
+ <span class="nxe-toolbar-item-icon">
1860
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="none">
1861
+ <path d="M3.5 1.5h9v13h-9z" fill="#fff" stroke="#383838" />
1862
+ <path d="M8 3.5v9M6 5.5l2-2 2 2M6 10.5l2 2 2-2" stroke="#2088d0" />
1863
+ </svg>
1864
+ </span>
1865
+ <span class="nxe-toolbar-item-text">{{ strings().ZoomOnePage }}</span>
1866
+ </button>
1867
+ <button type="button" class="nxe-toolbar-item" role="menuitem" (click)="fit('width')">
1868
+ <span class="nxe-toolbar-item-icon">
1869
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="none">
1870
+ <path d="M1.5 3.5h13v9h-13z" fill="#fff" stroke="#383838" />
1871
+ <path d="M3.5 8h9M5.5 6l-2 2 2 2M10.5 6l2 2-2 2" stroke="#2088d0" />
1872
+ </svg>
1873
+ </span>
1874
+ <span class="nxe-toolbar-item-text">{{ strings().ZoomPageWidth }}</span>
1875
+ </button>
1876
+ </div>
1877
+ }
1878
+ </div>
1879
+ }
1880
+ @if (showViewMode()) {
1881
+ <div class="nxe-toolbar-menu">
1882
+ <button
1883
+ type="button"
1884
+ class="nxe-toolbar-button"
1885
+ [attr.aria-label]="strings().ViewModeToolTip"
1886
+ [attr.aria-expanded]="openMenu() === 'viewMode'"
1887
+ (click)="toggleMenu('viewMode')"
1888
+ >
1889
+ <!-- Stimulsoft's button keeps the single-page icon whatever the mode. -->
1890
+ <span class="nxe-toolbar-icon">
1891
+ <svg viewBox="0 0 16 16" aria-hidden="true">
1892
+ <path d="M2.5 1.5h11v13h-11z" fill="#fff" stroke="#383838" />
1893
+ </svg>
1894
+ </span>
1895
+ @if (showCaptions()) {
1896
+ <span class="nxe-toolbar-caption">{{ strings()[viewMode()] }}</span>
1897
+ }
1898
+ <span class="nxe-toolbar-arrow">
1899
+ <svg viewBox="0 0 8 8" aria-hidden="true">
1900
+ <path d="M1.5 2.5 4 5l2.5-2.5" fill="none" stroke="#787878" />
1901
+ </svg>
1902
+ </span>
1903
+ </button>
1904
+ @if (openMenu() === 'viewMode') {
1905
+ <!-- Stimulsoft marks no item here as the current one, unlike the zoom menu. -->
1906
+ <div class="nxe-toolbar-popup" role="menu">
1907
+ @for (mode of viewModes; track mode) {
1908
+ <button type="button" class="nxe-toolbar-item" role="menuitem" (click)="chooseViewMode(mode)">
1909
+ <span class="nxe-toolbar-item-icon">
1910
+ <svg viewBox="0 0 16 16" aria-hidden="true" fill="#fff" stroke="#383838">
1911
+ @switch (mode) {
1912
+ @case ('SinglePage') {
1913
+ <path d="M2.5 1.5h11v13h-11z" />
1914
+ }
1915
+ @case ('Continuous') {
1916
+ <path d="M2.5 1.5h11v4h-11zm0 6h11v7h-11z" />
1917
+ }
1918
+ @case ('MultiplePages') {
1919
+ <path d="M2.5 2.5h5v5h-5zm6 0h5v5h-5zm-6 6h5v5h-5zm6 0h5v5h-5z" />
1920
+ }
1921
+ }
1922
+ </svg>
1923
+ </span>
1924
+ <span class="nxe-toolbar-item-text">{{ strings()[mode] }}</span>
1925
+ </button>
1926
+ }
1927
+ </div>
1928
+ }
1929
+ </div>
1930
+ }
1931
+ </div>
1932
+ `, styles: [":host{display:block;box-sizing:border-box;padding-top:2px;position:relative;-webkit-user-select:none;user-select:none;font-family:Arial;font-size:12px;color:#202020}:host(.nxe-busy):after{content:\"\";position:absolute;inset:0;cursor:default}.nxe-toolbar-bar{display:flex;align-items:center;box-sizing:border-box;height:31px;padding:1px;border:1px solid #c6c6c6;border-radius:3px;background:#fff;cursor:default}.nxe-toolbar-button{display:inline-flex;align-items:center;box-sizing:border-box;height:25px;margin:1px;padding:0;border:1px solid transparent;border-radius:3px;background:transparent;color:inherit;font:inherit;white-space:nowrap;cursor:default}.nxe-toolbar-button:hover:not(:disabled),.nxe-toolbar-button[aria-expanded=true]{background:#dadada}.nxe-toolbar-button.nxe-toolbar-selected{background:#e5e5e5}.nxe-toolbar-button:disabled{color:#d6d3d6}.nxe-toolbar-button:disabled svg{opacity:.5}.nxe-toolbar-icon{display:flex;flex:none;box-sizing:border-box;width:22px;padding-left:3px}.nxe-toolbar-icon svg{width:16px;height:16px}.nxe-toolbar-arrow{display:flex;flex:none;justify-content:center;width:18px}.nxe-toolbar-arrow svg{width:8px;height:8px}.nxe-toolbar-separator{flex:none;width:1px;height:21px;background:#d4d4d4}.nxe-page-control{display:inline-flex;align-items:center;height:23px;margin:0 1px;white-space:nowrap}.nxe-page-label,.nxe-page-box-cell{padding:0 2px}.nxe-page-box-cell{display:flex}.nxe-page-box{box-sizing:border-box;width:51px;height:23px;margin:0;padding:0 0 0 4px;border:1px solid #ababab;border-radius:3px;background:#fff;color:inherit;font:inherit;line-height:21px;text-align:left;outline:none}.nxe-page-box:hover:not(:disabled){border-color:#6b6b6b}.nxe-page-box:disabled{border-color:#e1e1e1;background:#f7f7f7;color:inherit;-webkit-text-fill-color:currentColor;opacity:1}.nxe-page-box-off{opacity:.5}.nxe-toolbar-menu{position:relative;display:flex;align-items:center}.nxe-toolbar-popup{position:absolute;top:100%;left:0;z-index:10;display:flex;flex-direction:column;gap:1px;box-sizing:border-box;padding:1px;border:1px solid #ababab;border-radius:3px;background:#fff;box-shadow:0 0 7px #0000004d}.nxe-toolbar-item{display:flex;align-items:center;flex:none;box-sizing:border-box;height:24px;margin:0 1px;padding:0;border:0;border-radius:3px;background:transparent;color:#202020;font:inherit;text-align:left;white-space:nowrap;cursor:default}.nxe-toolbar-item:first-child{margin-top:1px}.nxe-toolbar-item:last-child{margin-bottom:1px}.nxe-toolbar-item-on{background:#e5e5e5}.nxe-toolbar-item:hover{background:#dadada}.nxe-toolbar-item-icon{display:flex;flex:none;box-sizing:border-box;width:22px;padding-left:3px}.nxe-toolbar-item-icon svg{width:16px;height:16px}.nxe-toolbar-item-text{padding:0 20px 0 7px}.nxe-toolbar-item-separator{flex:none;height:1px;margin:0 2px 0 30px;background:#d4d4d4}\n"] }]
1933
+ }], 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 }] }], findOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "findOpen", 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"] }], findToggle: [{ type: i0.Output, args: ["findToggle"] }] } });
987
1934
 
1935
+ /** Stimulsoft's scroll tracker waits this long after scrolling stops before it moves the page. */
1936
+ const SCROLL_SETTLE_MS = 300;
1937
+ /** And animates a navigation scroll over this long, in steps of about this many milliseconds. */
1938
+ const SCROLL_ANIMATION_MS = 350;
1939
+ const SCROLL_STEP_MS = 30;
1940
+ /** The viewer never measures itself smaller than this, nor closer than this to the window's bottom. */
1941
+ const MIN_PANEL_HEIGHT = 200;
1942
+ const VIEWER_BOTTOM_GAP = 16;
1943
+ /** Stimulsoft scrolls a hit to its own height plus this far below the top of the report panel. */
1944
+ const FIND_SCROLL_GAP = 50;
988
1945
  /**
989
1946
  * A report served by `Nextera.Reports.Angular.NetCore`, shown page by page.
990
1947
  *
@@ -994,7 +1951,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
994
1951
  *
995
1952
  * Each viewer provides its own session, so several on a page never share a report or a token.
996
1953
  */
997
- class NxrReportViewerComponent {
1954
+ class NxeReportViewerComponent {
998
1955
  /** Each request's URL, with `{action}` in place of the host action it goes to. */
999
1956
  requestUrl = input('', /* @ts-ignore */
1000
1957
  ...(ngDevMode ? [{ debugName: "requestUrl" }] : /* istanbul ignore next */ []));
@@ -1023,13 +1980,64 @@ class NxrReportViewerComponent {
1023
1980
  loaded = output();
1024
1981
  /** Emits every failure the viewer puts on show. */
1025
1982
  error = output();
1026
- session = inject(NxrViewerSessionService);
1983
+ session = inject(NxeViewerSessionService);
1027
1984
  /** Stimulsoft's `api` object: what a host calls to drive the viewer from outside. */
1028
1985
  api = this.buildApi();
1029
1986
  appearance = computed(() => this.session.options()?.appearance ?? null, /* @ts-ignore */
1030
1987
  ...(ngDevMode ? [{ debugName: "appearance" }] : /* istanbul ignore next */ []));
1988
+ pagesBox = viewChild('pagesBox', /* @ts-ignore */
1989
+ ...(ngDevMode ? [{ debugName: "pagesBox" }] : /* istanbul ignore next */ []));
1990
+ rootBox = viewChild('rootBox', /* @ts-ignore */
1991
+ ...(ngDevMode ? [{ debugName: "rootBox" }] : /* istanbul ignore next */ []));
1992
+ /**
1993
+ * The mode the pages are shown in, and the zoom they are drawn at. Both start from the server's
1994
+ * options and are then the user's, until a new report's options arrive and reset them — which is
1995
+ * what `linkedSignal` says and an effect would only imply.
1996
+ */
1997
+ viewMode = linkedSignal(() => this.session.options()?.toolbar.viewMode ?? 'SinglePage', /* @ts-ignore */
1998
+ ...(ngDevMode ? [{ debugName: "viewMode" }] : /* istanbul ignore next */ []));
1999
+ zoom = linkedSignal(() => {
2000
+ const percent = this.session.options()?.toolbar.zoom ?? 100;
2001
+ return percent > 0 ? percent / 100 : 1;
2002
+ }, /* @ts-ignore */
2003
+ ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
2004
+ /**
2005
+ * The pages on screen: one in `SinglePage`, the whole report in the modes that scroll through it.
2006
+ * A page carries its size before its HTML arrives, so the list holds its shape while the pages that
2007
+ * come into view are fetched.
2008
+ */
2009
+ shownPages = computed(() => {
2010
+ if (this.viewMode() === 'SinglePage') {
2011
+ const page = this.session.shownPage();
2012
+ return page ? [page] : [];
2013
+ }
2014
+ return this.session.allPages();
2015
+ }, /* @ts-ignore */
2016
+ ...(ngDevMode ? [{ debugName: "shownPages" }] : /* istanbul ignore next */ []));
2017
+ /** Set while a page-navigation scroll is running, so scroll tracking does not fight it. */
2018
+ scrollingToPage = false;
2019
+ scrollSettled = null;
1031
2020
  toolbarVisible = computed(() => this.session.options()?.toolbar.visible !== false, /* @ts-ignore */
1032
2021
  ...(ngDevMode ? [{ debugName: "toolbarVisible" }] : /* istanbul ignore next */ []));
2022
+ /** See `.nxe-viewer-bookmarks`: the whole of Stimulsoft's bookmarks panel at width 0. */
2023
+ showBookmarksStrip = computed(() => this.session.report() !== null && this.appearance()?.bookmarksTreeWidth === 0, /* @ts-ignore */
2024
+ ...(ngDevMode ? [{ debugName: "showBookmarksStrip" }] : /* istanbul ignore next */ []));
2025
+ /** Whether the find panel is open; Stimulsoft's Find button opens and closes it. */
2026
+ findOpen = signal(false, /* @ts-ignore */
2027
+ ...(ngDevMode ? [{ debugName: "findOpen" }] : /* istanbul ignore next */ []));
2028
+ /** The search on show: what was looked for, the elements found, and which one is current. */
2029
+ findKey = null;
2030
+ hits = [];
2031
+ currentHit = 0;
2032
+ lastFind = null;
2033
+ /**
2034
+ * The pages whose HTML is on screen, so a search can follow them: in SinglePage the page changes
2035
+ * under it, and in the scrolling modes pages keep arriving as they are fetched.
2036
+ */
2037
+ renderedPages = computed(() => this.shownPages()
2038
+ .map((page) => `${page.index}:${page.html === null ? 0 : 1}`)
2039
+ .join(','), /* @ts-ignore */
2040
+ ...(ngDevMode ? [{ debugName: "renderedPages" }] : /* istanbul ignore next */ []));
1033
2041
  pageCss = computed(() => this.session.report()?.pageCss ?? '', /* @ts-ignore */
1034
2042
  ...(ngDevMode ? [{ debugName: "pageCss" }] : /* istanbul ignore next */ []));
1035
2043
  showPageShadow = computed(() => this.appearance()?.showPageShadow === true, /* @ts-ignore */
@@ -1058,7 +2066,22 @@ class NxrReportViewerComponent {
1058
2066
  ...(ngDevMode ? [{ debugName: "rootWidth" }] : /* istanbul ignore next */ []));
1059
2067
  rootBackgroundColor = computed(() => this.backgroundColor() ?? 'White', /* @ts-ignore */
1060
2068
  ...(ngDevMode ? [{ debugName: "rootBackgroundColor" }] : /* istanbul ignore next */ []));
2069
+ /**
2070
+ * The height the viewer was measured to fill, once it is known.
2071
+ *
2072
+ * The report panel is what scrolls, as Stimulsoft's does — its panel measures 836 px against a
2073
+ * 16 513 px report while the document itself does not scroll at all. That needs the viewer to have a
2074
+ * definite height, and a host that asks for `100%` inside a container whose own height is automatic
2075
+ * does not give it one: the viewer would then grow to the whole report, 29 pages of it, and the
2076
+ * page would scroll instead of the panel. So when the height the host asked for cannot resolve, the
2077
+ * viewer measures the room between its top and the bottom of the window and takes that.
2078
+ */
2079
+ measuredHeight = signal(null, /* @ts-ignore */
2080
+ ...(ngDevMode ? [{ debugName: "measuredHeight" }] : /* istanbul ignore next */ []));
1061
2081
  rootHeight = computed(() => {
2082
+ const measured = this.measuredHeight();
2083
+ if (measured !== null)
2084
+ return measured;
1062
2085
  const given = this.height();
1063
2086
  if (given !== null)
1064
2087
  return given;
@@ -1066,12 +2089,126 @@ class NxrReportViewerComponent {
1066
2089
  }, /* @ts-ignore */
1067
2090
  ...(ngDevMode ? [{ debugName: "rootHeight" }] : /* istanbul ignore next */ []));
1068
2091
  constructor() {
1069
- this.session.reportLoaded
1070
- .pipe(takeUntilDestroyed())
1071
- .subscribe(() => this.loaded.emit());
2092
+ this.session.reportLoaded.pipe(takeUntilDestroyed()).subscribe(() => {
2093
+ // The modes that show the whole report start with every page laid out at its size and none of
2094
+ // them fetched, so the first screenful has to be asked for here; scrolling asks for the rest.
2095
+ this.afterPagesSettle();
2096
+ this.loaded.emit();
2097
+ });
1072
2098
  this.session.failed
1073
2099
  .pipe(takeUntilDestroyed())
1074
2100
  .subscribe((failure) => this.error.emit(failure));
2101
+ // While a search is on show, new page HTML on screen means the search runs again over it.
2102
+ toObservable(this.renderedPages)
2103
+ .pipe(distinctUntilChanged(), skip(1), takeUntilDestroyed())
2104
+ .subscribe(() => {
2105
+ if (this.findOpen() && this.lastFind !== null) {
2106
+ requestAnimationFrame(() => this.refreshFind());
2107
+ }
2108
+ });
2109
+ }
2110
+ /** The Find button opens the panel, or closes it and takes the search's highlights away. */
2111
+ toggleFind() {
2112
+ if (this.findOpen()) {
2113
+ this.closeFind();
2114
+ return;
2115
+ }
2116
+ this.findOpen.set(true);
2117
+ this.afterPagesSettle();
2118
+ }
2119
+ closeFind() {
2120
+ this.findOpen.set(false);
2121
+ this.resetFind();
2122
+ this.afterPagesSettle();
2123
+ }
2124
+ /**
2125
+ * A search from the find panel, run as Stimulsoft runs it (angular-viewer-panels-chrome.md §1.3,
2126
+ * §1.6): empty text clears the highlights; new text, or a changed option, finds afresh and makes the
2127
+ * first hit current; the same search again steps to the next or previous hit, round the ends. Each
2128
+ * current hit is scrolled into view. Stimulsoft finds only in the pages it has on screen; this viewer
2129
+ * fetches the pages a scrolling mode has not fetched yet, and finds in them as they arrive.
2130
+ */
2131
+ find(request) {
2132
+ this.lastFind = request;
2133
+ if (request.text === '') {
2134
+ this.resetFind();
2135
+ return;
2136
+ }
2137
+ const key = `${request.matchCase ? 1 : 0}${request.wholeWord ? 1 : 0}${request.text}`;
2138
+ if (key !== this.findKey) {
2139
+ this.findKey = key;
2140
+ this.requestAllPages();
2141
+ this.runFind(request, 0);
2142
+ this.revealHit(true);
2143
+ return;
2144
+ }
2145
+ if (this.hits.length === 0)
2146
+ return;
2147
+ const previous = this.currentHit;
2148
+ const count = this.hits.length;
2149
+ this.currentHit = request.forward ? (previous + 1) % count : (previous - 1 + count) % count;
2150
+ markCurrent(this.hits, previous, this.currentHit);
2151
+ this.revealHit(false);
2152
+ }
2153
+ /** Finds again over the pages now on screen, keeping the current hit when it is still there. */
2154
+ refreshFind() {
2155
+ const request = this.lastFind;
2156
+ if (request === null || request.text === '')
2157
+ return;
2158
+ const kept = this.hits[this.currentHit];
2159
+ this.runFind(request, 0);
2160
+ const index = kept ? this.hits.indexOf(kept) : -1;
2161
+ if (index > 0) {
2162
+ markCurrent(this.hits, 0, index);
2163
+ this.currentHit = index;
2164
+ }
2165
+ }
2166
+ runFind(request, current) {
2167
+ const roots = this.pageRoots();
2168
+ clearHits(roots);
2169
+ this.hits = findHits(roots, request.text, request);
2170
+ this.currentHit = current;
2171
+ labelHits(this.hits, this.currentHit);
2172
+ }
2173
+ resetFind() {
2174
+ clearHits(this.pageRoots());
2175
+ this.hits = [];
2176
+ this.currentHit = 0;
2177
+ this.findKey = null;
2178
+ this.lastFind = null;
2179
+ }
2180
+ /** Each page on screen keeps its HTML in its own shadow root. */
2181
+ pageRoots() {
2182
+ const box = this.pagesBox()?.nativeElement;
2183
+ if (!box)
2184
+ return [];
2185
+ return Array.from(box.querySelectorAll('nxe-report-page'))
2186
+ .map((page) => page.shadowRoot)
2187
+ .filter((root) => root !== null);
2188
+ }
2189
+ requestAllPages() {
2190
+ if (this.viewMode() === 'SinglePage')
2191
+ return;
2192
+ for (let index = 0; index < this.session.pageCount(); index++)
2193
+ this.session.requestPage(index);
2194
+ }
2195
+ /**
2196
+ * Scrolls the current hit to its own height plus 50 px below the top of the report panel, over
2197
+ * 350 ms: always after a fresh search, and after a step only when the hit is out of view. That is
2198
+ * what Stimulsoft's viewer was seen to do; the exact place it scrolls to also carries offsets of its
2199
+ * own page markup, which this viewer's pages do not have.
2200
+ */
2201
+ revealHit(always) {
2202
+ const box = this.pagesBox()?.nativeElement;
2203
+ const hit = this.hits[this.currentHit];
2204
+ if (!box || !hit)
2205
+ return;
2206
+ const panel = box.getBoundingClientRect();
2207
+ const target = hit.getBoundingClientRect();
2208
+ if (!always && target.top >= panel.top && target.bottom <= panel.bottom)
2209
+ return;
2210
+ const to = box.scrollTop + target.top - panel.top - (target.height + FIND_SCROLL_GAP);
2211
+ this.animateScroll(box, Math.max(0, to));
1075
2212
  }
1076
2213
  /**
1077
2214
  * A change to any input reloads, as Stimulsoft's viewer does: its `ngOnChanges` never looks at
@@ -1081,6 +2218,146 @@ class NxrReportViewerComponent {
1081
2218
  ngOnChanges() {
1082
2219
  this.session.open(this.context(), this.action());
1083
2220
  }
2221
+ /**
2222
+ * Page navigation. In a mode that scrolls through the whole report the page is reached by scrolling
2223
+ * to it, over 350 ms, which is what Stimulsoft's navigation actions do; in `SinglePage` the page is
2224
+ * simply the one shown.
2225
+ */
2226
+ navigateTo(index) {
2227
+ this.session.goToPage(index);
2228
+ if (this.viewMode() === 'SinglePage')
2229
+ return;
2230
+ this.scrollToPage(index);
2231
+ }
2232
+ /** The page number is kept and nothing scrolls, as Stimulsoft's view-mode actions leave it. */
2233
+ setViewMode(mode) {
2234
+ this.viewMode.set(mode);
2235
+ this.afterPagesSettle();
2236
+ }
2237
+ /** Nothing preserves the scroll offset across a zoom change, so the visible region shifts. */
2238
+ setZoom(zoom) {
2239
+ this.zoom.set(zoom);
2240
+ this.afterPagesSettle();
2241
+ }
2242
+ /**
2243
+ * Fit a whole page, or a page's width, in the panel. Stimulsoft computes this once from what is on
2244
+ * screen and stores the number as the zoom; no fit mode is remembered, so a later resize does not
2245
+ * re-fit. The 40 px it subtracts is the panel's padding and the page's margins.
2246
+ */
2247
+ fitTo(what) {
2248
+ const box = this.pagesBox()?.nativeElement;
2249
+ const page = box?.querySelector('nxe-report-page');
2250
+ if (!box || !(page instanceof HTMLElement))
2251
+ return;
2252
+ const available = what === 'width' ? box.clientWidth - 40 : box.clientHeight - 40;
2253
+ const drawn = what === 'width' ? page.offsetWidth : page.offsetHeight;
2254
+ if (drawn <= 0 || available <= 0)
2255
+ return;
2256
+ this.zoom.set((available * this.zoom()) / drawn);
2257
+ this.afterPagesSettle();
2258
+ }
2259
+ /**
2260
+ * Scrolling asks for the pages that have come into view, and, once it has stopped for 300 ms, moves
2261
+ * the page the toolbar reports to the first one still on screen — Stimulsoft's scroll tracking.
2262
+ */
2263
+ onScroll() {
2264
+ this.requestVisiblePages();
2265
+ if (this.scrollSettled !== null)
2266
+ clearTimeout(this.scrollSettled);
2267
+ this.scrollSettled = setTimeout(() => {
2268
+ this.scrollSettled = null;
2269
+ if (!this.scrollingToPage)
2270
+ this.trackCurrentPage();
2271
+ }, SCROLL_SETTLE_MS);
2272
+ }
2273
+ /** The room on screen changed, so the viewer measures itself against it again. */
2274
+ onWindowResize() {
2275
+ this.fitToAvailableRoom();
2276
+ }
2277
+ /** Lets the new layout reach the DOM, then fits the viewer to it and asks for what is on screen. */
2278
+ afterPagesSettle() {
2279
+ requestAnimationFrame(() => {
2280
+ this.fitToAvailableRoom();
2281
+ this.requestVisiblePages();
2282
+ });
2283
+ }
2284
+ /**
2285
+ * Takes the room between the viewer's top and the bottom of the window when the height the host
2286
+ * asked for did not resolve, so the panel inside scrolls rather than the page. Measured before the
2287
+ * height is applied, and again only when the window changes, so it never chases its own answer.
2288
+ */
2289
+ fitToAvailableRoom() {
2290
+ const root = this.rootBox()?.nativeElement;
2291
+ if (!root || !this.appearance()?.scrollbarsMode)
2292
+ return;
2293
+ const rect = root.getBoundingClientRect();
2294
+ // A height that resolved is left alone, as Stimulsoft leaves it, even when it ends at the very
2295
+ // bottom of the window; one that grew past the window's bottom is taken over.
2296
+ if (this.measuredHeight() === null && rect.bottom <= window.innerHeight + 1)
2297
+ return;
2298
+ const room = Math.max(MIN_PANEL_HEIGHT, Math.round(window.innerHeight - rect.top - VIEWER_BOTTOM_GAP));
2299
+ this.measuredHeight.set(`${room}px`);
2300
+ }
2301
+ /** Every page whose box meets the panel, plus one either side, so scrolling stays ahead of the eye. */
2302
+ requestVisiblePages() {
2303
+ if (this.viewMode() === 'SinglePage')
2304
+ return;
2305
+ const box = this.pagesBox()?.nativeElement;
2306
+ if (!box)
2307
+ return;
2308
+ const pages = box.querySelectorAll('nxe-report-page');
2309
+ const top = box.scrollTop - box.clientHeight;
2310
+ const bottom = box.scrollTop + box.clientHeight * 2;
2311
+ pages.forEach((element, index) => {
2312
+ if (!(element instanceof HTMLElement))
2313
+ return;
2314
+ const pageTop = element.offsetTop - box.offsetTop;
2315
+ if (pageTop + element.offsetHeight >= top && pageTop <= bottom)
2316
+ this.session.requestPage(index);
2317
+ });
2318
+ }
2319
+ /** The first page whose bottom is still below the panel's scroll offset. */
2320
+ trackCurrentPage() {
2321
+ const box = this.pagesBox()?.nativeElement;
2322
+ if (!box)
2323
+ return;
2324
+ const pages = box.querySelectorAll('nxe-report-page');
2325
+ for (let index = 0; index < pages.length; index++) {
2326
+ const element = pages[index];
2327
+ if (!(element instanceof HTMLElement))
2328
+ continue;
2329
+ const pageTop = element.offsetTop - box.offsetTop;
2330
+ if (pageTop + element.offsetHeight > box.scrollTop) {
2331
+ this.session.markCurrentPage(index);
2332
+ return;
2333
+ }
2334
+ }
2335
+ }
2336
+ /** Stimulsoft animates the panel to the page's top over 350 ms, in steps of about 30 ms. */
2337
+ scrollToPage(index) {
2338
+ const box = this.pagesBox()?.nativeElement;
2339
+ const element = box?.querySelectorAll('nxe-report-page')[index];
2340
+ if (!box || !(element instanceof HTMLElement))
2341
+ return;
2342
+ this.animateScroll(box, element.offsetTop - box.offsetTop);
2343
+ }
2344
+ /** Scrolls the report panel over 350 ms in steps of about 30 ms, as Stimulsoft's scrolls all run. */
2345
+ animateScroll(box, to) {
2346
+ const from = box.scrollTop;
2347
+ const started = performance.now();
2348
+ this.scrollingToPage = true;
2349
+ const step = () => {
2350
+ const done = Math.min(1, (performance.now() - started) / SCROLL_ANIMATION_MS);
2351
+ box.scrollTop = from + (to - from) * done;
2352
+ if (done < 1) {
2353
+ setTimeout(step, SCROLL_STEP_MS);
2354
+ return;
2355
+ }
2356
+ this.scrollingToPage = false;
2357
+ this.requestVisiblePages();
2358
+ };
2359
+ step();
2360
+ }
1084
2361
  /**
1085
2362
  * The getters read the session's signals, so a host that asks between renders gets what is on
1086
2363
  * screen now. Built from a captured session rather than `this`, because `this` inside an object
@@ -1108,104 +2385,148 @@ class NxrReportViewerComponent {
1108
2385
  requestUrl: this.requestUrl(),
1109
2386
  properties: this.properties(),
1110
2387
  postParametersFunction: this.postParametersFunction(),
1111
- strings: NXR_ENGLISH,
2388
+ strings: NXE_ENGLISH,
1112
2389
  };
1113
2390
  }
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 });
2391
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeReportViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2392
+ 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: `
2393
+ <div
2394
+ #rootBox
2395
+ class="nxe-viewer-root"
2396
+ [style]="style()"
2397
+ [style.background-color]="rootBackgroundColor()"
2398
+ [style.width]="rootWidth()"
2399
+ [style.height]="rootHeight()"
2400
+ >
2401
+ @if (toolbarVisible()) {
2402
+ <nxe-viewer-toolbar
2403
+ [options]="session.options()"
2404
+ [pageIndex]="session.pageIndex()"
2405
+ [pageCount]="session.pageCount()"
2406
+ [busy]="session.busy()"
2407
+ [viewMode]="viewMode()"
2408
+ [zoom]="zoom()"
2409
+ [findOpen]="findOpen()"
2410
+ (goToPage)="navigateTo($event)"
2411
+ (viewModeChange)="setViewMode($event)"
2412
+ (zoomChange)="setZoom($event)"
2413
+ (fitRequest)="fitTo($event)"
2414
+ (saveRequest)="session.exportTo($event)"
2415
+ (findToggle)="toggleFind()"
2416
+ />
2417
+ }
2418
+ @if (findOpen()) {
2419
+ <nxe-viewer-find-panel (search)="find($event)" (closed)="closeFind()" />
2420
+ }
2421
+
2422
+ <div class="nxe-viewer-body">
2423
+ @if (showBookmarksStrip()) {
2424
+ <div class="nxe-viewer-bookmarks"></div>
2425
+ }
2426
+ <div
2427
+ #pagesBox
2428
+ class="nxe-viewer-pages"
2429
+ [class.nxe-viewer-continuous]="viewMode() === 'Continuous'"
2430
+ [class.nxe-viewer-pages-beside]="showBookmarksStrip()"
2431
+ [style.text-align]="pageAlignment()"
2432
+ [style.overflow]="pagesOverflow()"
2433
+ (scroll)="onScroll()"
2434
+ >
2435
+ @for (page of shownPages(); track page.index) {
2436
+ <nxe-report-page
2437
+ class="nxe-viewer-page"
2438
+ [class.nxe-viewer-page-shadow]="showPageShadow()"
2439
+ [style.border]="pageBorder()"
2440
+ [page]="page.size"
2441
+ [html]="page.html"
2442
+ [css]="pageCss()"
2443
+ [zoom]="zoom()"
2444
+ />
2445
+ }
2446
+ </div>
2447
+ </div>
2448
+
2449
+ @if (session.failure(); as failure) {
2450
+ <div class="nxe-viewer-failure" role="alert">
2451
+ <span class="nxe-viewer-failure-text">{{ failure.message }}</span>
2452
+ <button type="button" class="nxe-viewer-failure-close" (click)="session.dismissFailure()">
2453
+ &times;
2454
+ </button>
2455
+ </div>
2456
+ }
2457
+ </div>
2458
+ `, isInline: true, styles: [":host{display:inline}.nxe-viewer-root{display:inline-flex;flex-direction:column;box-sizing:border-box;position:relative}.nxe-viewer-body{position:relative;display:flex;flex:1 1 auto;min-height:0;margin-top:2px}.nxe-viewer-bookmarks{position:absolute;top:0;bottom:0;left:0;box-sizing:border-box;width:5px;border-left:1px solid #c6c6c6;border-right:1px solid #c6c6c6;background:#fff}.nxe-viewer-pages{position:relative;flex:1 1 auto;min-width:0;box-sizing:border-box;padding:0 10px}.nxe-viewer-pages-beside{margin-left:2px}.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: NxeViewerFindPanelComponent, selector: "nxe-viewer-find-panel", inputs: ["strings"], outputs: ["search", "closed"] }, { kind: "component", type: NxeViewerToolbarComponent, selector: "nxe-viewer-toolbar", inputs: ["options", "pageIndex", "pageCount", "busy", "strings", "viewMode", "zoom", "findOpen"], outputs: ["goToPage", "viewModeChange", "zoomChange", "fitRequest", "saveRequest", "findToggle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1160
2459
  }
1161
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxrReportViewerComponent, decorators: [{
2460
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeReportViewerComponent, decorators: [{
1162
2461
  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"] }] } });
2462
+ args: [{ selector: 'nxe-report-viewer', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NxeReportPageComponent, NxeViewerFindPanelComponent, NxeViewerToolbarComponent], providers: [NxeViewerClientService, NxeViewerSessionService], host: { '(window:resize)': 'onWindowResize()' }, template: `
2463
+ <div
2464
+ #rootBox
2465
+ class="nxe-viewer-root"
2466
+ [style]="style()"
2467
+ [style.background-color]="rootBackgroundColor()"
2468
+ [style.width]="rootWidth()"
2469
+ [style.height]="rootHeight()"
2470
+ >
2471
+ @if (toolbarVisible()) {
2472
+ <nxe-viewer-toolbar
2473
+ [options]="session.options()"
2474
+ [pageIndex]="session.pageIndex()"
2475
+ [pageCount]="session.pageCount()"
2476
+ [busy]="session.busy()"
2477
+ [viewMode]="viewMode()"
2478
+ [zoom]="zoom()"
2479
+ [findOpen]="findOpen()"
2480
+ (goToPage)="navigateTo($event)"
2481
+ (viewModeChange)="setViewMode($event)"
2482
+ (zoomChange)="setZoom($event)"
2483
+ (fitRequest)="fitTo($event)"
2484
+ (saveRequest)="session.exportTo($event)"
2485
+ (findToggle)="toggleFind()"
2486
+ />
2487
+ }
2488
+ @if (findOpen()) {
2489
+ <nxe-viewer-find-panel (search)="find($event)" (closed)="closeFind()" />
2490
+ }
2491
+
2492
+ <div class="nxe-viewer-body">
2493
+ @if (showBookmarksStrip()) {
2494
+ <div class="nxe-viewer-bookmarks"></div>
2495
+ }
2496
+ <div
2497
+ #pagesBox
2498
+ class="nxe-viewer-pages"
2499
+ [class.nxe-viewer-continuous]="viewMode() === 'Continuous'"
2500
+ [class.nxe-viewer-pages-beside]="showBookmarksStrip()"
2501
+ [style.text-align]="pageAlignment()"
2502
+ [style.overflow]="pagesOverflow()"
2503
+ (scroll)="onScroll()"
2504
+ >
2505
+ @for (page of shownPages(); track page.index) {
2506
+ <nxe-report-page
2507
+ class="nxe-viewer-page"
2508
+ [class.nxe-viewer-page-shadow]="showPageShadow()"
2509
+ [style.border]="pageBorder()"
2510
+ [page]="page.size"
2511
+ [html]="page.html"
2512
+ [css]="pageCss()"
2513
+ [zoom]="zoom()"
2514
+ />
2515
+ }
2516
+ </div>
2517
+ </div>
2518
+
2519
+ @if (session.failure(); as failure) {
2520
+ <div class="nxe-viewer-failure" role="alert">
2521
+ <span class="nxe-viewer-failure-text">{{ failure.message }}</span>
2522
+ <button type="button" class="nxe-viewer-failure-close" (click)="session.dismissFailure()">
2523
+ &times;
2524
+ </button>
2525
+ </div>
2526
+ }
2527
+ </div>
2528
+ `, styles: [":host{display:inline}.nxe-viewer-root{display:inline-flex;flex-direction:column;box-sizing:border-box;position:relative}.nxe-viewer-body{position:relative;display:flex;flex:1 1 auto;min-height:0;margin-top:2px}.nxe-viewer-bookmarks{position:absolute;top:0;bottom:0;left:0;box-sizing:border-box;width:5px;border-left:1px solid #c6c6c6;border-right:1px solid #c6c6c6;background:#fff}.nxe-viewer-pages{position:relative;flex:1 1 auto;min-width:0;box-sizing:border-box;padding:0 10px}.nxe-viewer-pages-beside{margin-left:2px}.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"] }]
2529
+ }], 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
2530
 
1210
2531
  /*
1211
2532
  * Public API Surface of @nexterajs/report-viewer
@@ -1215,5 +2536,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
1215
2536
  * Generated bundle index. Do not edit.
1216
2537
  */
1217
2538
 
1218
- export { NXR_VIEWER_PROTOCOL, NxrReportViewerComponent };
2539
+ export { NXE_VIEWER_PROTOCOL, NxeReportViewerComponent };
1219
2540
  //# sourceMappingURL=nexterajs-report-viewer.mjs.map