@nexterajs/report-viewer 0.1.0 → 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,8 +1,8 @@
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, computed, inject, DOCUMENT, ElementRef, ChangeDetectionStrategy, Component, Injectable, signal, DestroyRef, output, viewChild, linkedSignal } 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
@@ -10,6 +10,326 @@ import { defer, map, catchError, from, switchMap, throwError, timeout, Subject,
10
10
  */
11
11
  const NXE_VIEWER_PROTOCOL = 1;
12
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 }] }] } });
332
+
13
333
  /**
14
334
  * Page HTML from the server, made safe to place in the application's document.
15
335
  *
@@ -137,6 +457,9 @@ function adoptedSheet(cssText) {
137
457
  return sheet;
138
458
  }
139
459
 
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;
140
463
  /**
141
464
  * The viewer draws each page's frame and the space between pages, so the page CSS's own are cancelled.
142
465
  *
@@ -171,9 +494,15 @@ class NxeReportPageComponent {
171
494
  /** The zoom to draw the page at, as a factor: 1 is 100 %. */
172
495
  zoom = input(1, /* @ts-ignore */
173
496
  ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
174
- scaledWidth = computed(() => this.page().widthPoints * this.zoom(), /* @ts-ignore */
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 */
175
504
  ...(ngDevMode ? [{ debugName: "scaledWidth" }] : /* istanbul ignore next */ []));
176
- scaledHeight = computed(() => this.page().heightPoints * this.zoom(), /* @ts-ignore */
505
+ scaledHeight = computed(() => Math.floor(this.page().heightPoints * PX_PER_PT * VIEWER_HEIGHT + 1e-9) * this.zoom(), /* @ts-ignore */
177
506
  ...(ngDevMode ? [{ debugName: "scaledHeight" }] : /* istanbul ignore next */ []));
178
507
  document = inject(DOCUMENT);
179
508
  root = inject(ElementRef).nativeElement.attachShadow({
@@ -194,56 +523,18 @@ class NxeReportPageComponent {
194
523
  }
195
524
  }
196
525
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeReportPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
197
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxeReportPageComponent, isStandalone: true, selector: "nxe-report-page", inputs: { page: { classPropertyName: "page", publicName: "page", isSignal: true, isRequired: true, transformFunction: null }, html: { classPropertyName: "html", publicName: "html", isSignal: true, isRequired: false, transformFunction: null }, css: { classPropertyName: "css", publicName: "css", isSignal: true, isRequired: true, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-page": "page().pageNumber", "style.width.pt": "scaledWidth()", "style.height.pt": "scaledHeight()" }, classAttribute: "nxe-report-page" }, usesOnChanges: true, ngImport: i0, template: '', isInline: true, styles: [":host{display:block;position:relative;box-sizing:content-box;contain:paint;background:#fff}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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 });
198
527
  }
199
528
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeReportPageComponent, decorators: [{
200
529
  type: Component,
201
530
  args: [{ selector: 'nxe-report-page', standalone: true, template: '', changeDetection: ChangeDetectionStrategy.OnPush, host: {
202
531
  class: 'nxe-report-page',
203
532
  '[attr.data-page]': 'page().pageNumber',
204
- '[style.width.pt]': 'scaledWidth()',
205
- '[style.height.pt]': 'scaledHeight()',
533
+ '[style.width.px]': 'scaledWidth()',
534
+ '[style.height.px]': 'scaledHeight()',
206
535
  }, styles: [":host{display:block;position:relative;box-sizing:content-box;contain:paint;background:#fff}\n"] }]
207
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 }] }] } });
208
537
 
209
- /**
210
- * Every caption and message the viewer shows, in its built-in English. A key in PascalCase is the name
211
- * Stimulsoft's viewer gives the same text, such as `PageOf`; a key in camelCase names a message only this
212
- * viewer has. The English is this package's own. A later plan lets a host's translation file replace any
213
- * of them, so code reads them from an `NxeViewerStrings` value wherever one is at hand.
214
- */
215
- const NXE_ENGLISH = {
216
- /** The label before the page-number box. */
217
- Page: 'Page',
218
- /** The label between the page-number box and the page count. */
219
- PageOf: 'of',
220
- FirstPage: 'First page',
221
- PrevPage: 'Previous page',
222
- NextPage: 'Next page',
223
- LastPage: 'Last page',
224
- /** The save button's caption and tooltip; its menu items are captioned by the format they write. */
225
- Save: 'Save',
226
- SaveToolTip: 'Save',
227
- /** The view-mode button's tooltip, and its menu items, whose captions are the mode names. */
228
- ViewModeToolTip: 'View mode',
229
- SinglePage: 'Single Page',
230
- Continuous: 'Continuous',
231
- MultiplePages: 'Multiple Pages',
232
- /** The zoom button's tooltip; its caption is the current percentage, so it has no key. */
233
- ZoomToolTip: 'Zoom',
234
- ZoomOnePage: 'One Page',
235
- ZoomPageWidth: 'Page Width',
236
- errorUnreachable: 'The report server could not be reached.',
237
- errorStatus: 'The report server answered with HTTP status {0}.',
238
- errorTimeout: 'The report server did not answer within {0} seconds.',
239
- errorMalformed: "The report server's answer to {0} is not one this viewer can read.",
240
- errorProtocol: 'The report server speaks viewer protocol {0}, and this viewer speaks protocol {1}.',
241
- };
242
- /** Puts each value in place of its numbered placeholder: `{0}` is the first value. */
243
- function formatString(template, ...values) {
244
- return template.replace(/\{(\d+)\}/g, (placeholder, index) => Number(index) < values.length ? String(values[Number(index)]) : placeholder);
245
- }
246
-
247
538
  /**
248
539
  * A success answer that is not what the protocol says its action answers: the wrong shape, or an
249
540
  * AngularViewerData answer from another version of the protocol.
@@ -813,25 +1104,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
813
1104
  type: Injectable
814
1105
  }], ctorParameters: () => [] });
815
1106
 
816
- /** The width of the page-number box, as Stimulsoft's page control sizes it. */
817
- const PAGE_BOX_WIDTH_PX = 45;
818
1107
  /** The zoom menu's fixed percentages, in Stimulsoft's order, above the two fit items. */
819
1108
  const ZOOM_PRESETS = [25, 50, 75, 100, 150, 200];
820
1109
  /** The view-mode menu's items, in Stimulsoft's order. */
821
1110
  const VIEW_MODES = ['SinglePage', 'Continuous', 'MultiplePages'];
822
1111
  /**
823
- * 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.
824
1119
  *
825
- * Stimulsoft's toolbar is a long list of groups; this is the navigation group of it, which is what
826
- * a report viewer cannot do without: first, previous, the page control, next, last. Each button
827
- * appears only when its `toolbar.show…Button` option is on, and the group is laid out where
828
- * `toolbar.alignment` says. The other groups — save, print, find, bookmarks, zoom, view mode —
829
- * 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.
830
1125
  *
831
1126
  * Navigation is disabled the way Stimulsoft disables it: in `MultiplePages`, and in `Continuous`
832
- * unless the report panel or the whole viewer scrolls. A button is disabled at the end of the
833
- * report it would move past, and everything is disabled while a request the user waits for is on
834
- * 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.
835
1129
  */
836
1130
  class NxeViewerToolbarComponent {
837
1131
  /** The options from the server, or null before the first answer, when the toolbar shows nothing. */
@@ -855,6 +1149,9 @@ class NxeViewerToolbarComponent {
855
1149
  /** The zoom the pages are drawn at, as a factor: 1 is 100 %. */
856
1150
  zoom = input(1, /* @ts-ignore */
857
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 */ []));
858
1155
  /** The 0-based page the user asked for. */
859
1156
  goToPage = output();
860
1157
  /** The mode the user chose. */
@@ -865,7 +1162,8 @@ class NxeViewerToolbarComponent {
865
1162
  fitRequest = output();
866
1163
  /** The format the user asked the report to be saved as. */
867
1164
  saveRequest = output();
868
- pageBoxWidth = PAGE_BOX_WIDTH_PX;
1165
+ /** The user pressed Find, which opens the find panel or closes it. */
1166
+ findToggle = output();
869
1167
  zoomPresets = ZOOM_PRESETS;
870
1168
  viewModes = VIEW_MODES;
871
1169
  /** Which menu is open, or null. Stimulsoft opens one at a time and closes it on the next click. */
@@ -890,6 +1188,10 @@ class NxeViewerToolbarComponent {
890
1188
  ...(ngDevMode ? [{ debugName: "saveFormats" }] : /* istanbul ignore next */ []));
891
1189
  showSave = computed(() => this.toolbar()?.showSaveButton === true && this.saveFormats().length > 0, /* @ts-ignore */
892
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
+ }
893
1195
  save(format) {
894
1196
  this.openMenuState.set(null);
895
1197
  this.saveRequest.emit(format);
@@ -923,6 +1225,13 @@ class NxeViewerToolbarComponent {
923
1225
  !appearance.fullScreenMode);
924
1226
  }, /* @ts-ignore */
925
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 */ []));
926
1235
  showFirstPage = computed(() => this.toolbar()?.showFirstPageButton === true, /* @ts-ignore */
927
1236
  ...(ngDevMode ? [{ debugName: "showFirstPage" }] : /* istanbul ignore next */ []));
928
1237
  showPreviousPage = computed(() => this.toolbar()?.showPreviousPageButton === true, /* @ts-ignore */
@@ -937,6 +1246,22 @@ class NxeViewerToolbarComponent {
937
1246
  ...(ngDevMode ? [{ debugName: "showZoom" }] : /* istanbul ignore next */ []));
938
1247
  showViewMode = computed(() => this.toolbar()?.showViewModeButton === true, /* @ts-ignore */
939
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 */ []));
940
1265
  /** Whether a preset is the zoom on show, so its menu item reads as chosen. */
941
1266
  isZoom(percent) {
942
1267
  return Math.round(this.zoom() * 100) === percent;
@@ -969,12 +1294,17 @@ class NxeViewerToolbarComponent {
969
1294
  ...(ngDevMode ? [{ debugName: "fontColor" }] : /* istanbul ignore next */ []));
970
1295
  fontFamily = computed(() => this.toolbar()?.fontFamily || null, /* @ts-ignore */
971
1296
  ...(ngDevMode ? [{ debugName: "fontFamily" }] : /* istanbul ignore next */ []));
972
- /** 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
+ */
973
1301
  border = computed(() => {
974
1302
  const toolbar = this.toolbar();
975
- if (!toolbar || toolbar.displayMode === 'Separated' || !toolbar.borderColor)
1303
+ if (!toolbar)
976
1304
  return null;
977
- return `1px solid ${toolbar.borderColor}`;
1305
+ if (toolbar.displayMode === 'Separated')
1306
+ return '0';
1307
+ return toolbar.borderColor ? `1px solid ${toolbar.borderColor}` : null;
978
1308
  }, /* @ts-ignore */
979
1309
  ...(ngDevMode ? [{ debugName: "border" }] : /* istanbul ignore next */ []));
980
1310
  justifyContent = computed(() => {
@@ -1005,435 +1335,602 @@ class NxeViewerToolbarComponent {
1005
1335
  this.goToPage.emit(index);
1006
1336
  }
1007
1337
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1008
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeViewerToolbarComponent, isStandalone: true, selector: "nxe-viewer-toolbar", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, pageIndex: { classPropertyName: "pageIndex", publicName: "pageIndex", isSignal: true, isRequired: false, transformFunction: null }, pageCount: { classPropertyName: "pageCount", publicName: "pageCount", isSignal: true, isRequired: false, transformFunction: null }, busy: { classPropertyName: "busy", publicName: "busy", isSignal: true, isRequired: false, transformFunction: null }, strings: { classPropertyName: "strings", publicName: "strings", isSignal: true, isRequired: false, transformFunction: null }, viewMode: { classPropertyName: "viewMode", publicName: "viewMode", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { goToPage: "goToPage", viewModeChange: "viewModeChange", zoomChange: "zoomChange", fitRequest: "fitRequest", saveRequest: "saveRequest" }, host: { attributes: { "role": "toolbar" }, properties: { "style.background-color": "backgroundColor()", "style.border": "border()", "style.color": "fontColor()", "style.font-family": "fontFamily()", "style.justify-content": "justifyContent()", "class.nxe-busy": "busy()" }, classAttribute: "nxe-viewer-toolbar" }, ngImport: i0, template: `
1009
- @if (showSave()) {
1010
- <div class="nxe-toolbar-menu">
1011
- <button
1012
- type="button"
1013
- class="nxe-toolbar-button nxe-toolbar-captioned"
1014
- [attr.aria-label]="strings().SaveToolTip"
1015
- [attr.aria-expanded]="openMenu() === 'save'"
1016
- (click)="toggleMenu('save')"
1017
- >
1018
- <svg viewBox="0 0 16 16" aria-hidden="true">
1019
- <path
1020
- d="M2 2h9.5L14 4.5V14H2zm2.5 0v4.5h6V2zm-1 7.5h9V14h-9zM8.5 3h1.5v2.5H8.5z"
1021
- />
1022
- </svg>
1023
- <span class="nxe-toolbar-caption">{{ strings().Save }}</span>
1024
- <svg class="nxe-toolbar-arrow" viewBox="0 0 8 8" aria-hidden="true">
1025
- <path d="M1 3h6L4 6.2z" />
1026
- </svg>
1027
- </button>
1028
- @if (openMenu() === 'save') {
1029
- <div class="nxe-toolbar-popup" role="menu">
1030
- @for (format of saveFormats(); track format) {
1031
- <button
1032
- type="button"
1033
- class="nxe-toolbar-item"
1034
- role="menuitem"
1035
- (click)="save(format)"
1036
- >
1037
- <span class="nxe-toolbar-item-icon">
1038
- <svg viewBox="0 0 16 16" aria-hidden="true">
1039
- <path d="M3 1h7l3 3v11H3zm6.6 1.2v2.2h2.2z" />
1040
- </svg>
1041
- </span>
1042
- {{ format }}
1043
- </button>
1044
- }
1045
- </div>
1046
- }
1047
- </div>
1048
- <span class="nxe-toolbar-separator"></span>
1049
- }
1050
- @if (showFirstPage()) {
1051
- <button
1052
- type="button"
1053
- class="nxe-toolbar-button"
1054
- [disabled]="atStart()"
1055
- [attr.aria-label]="strings().FirstPage"
1056
- (click)="goTo(0)"
1057
- >
1058
- <svg viewBox="0 0 16 16" aria-hidden="true">
1059
- <path d="M12.5 3v10L6 8zM4.5 3h1.2v10H4.5z" />
1060
- </svg>
1061
- </button>
1062
- }
1063
- @if (showPreviousPage()) {
1064
- <button
1065
- type="button"
1066
- class="nxe-toolbar-button"
1067
- [disabled]="atStart()"
1068
- [attr.aria-label]="strings().PrevPage"
1069
- (click)="goTo(pageIndex() - 1)"
1070
- >
1071
- <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M11 3v10L4.5 8z" /></svg>
1072
- </button>
1073
- }
1074
- @if (showCurrentPage()) {
1075
- <span class="nxe-page-control">
1076
- <span class="nxe-page-label">{{ strings().Page }}</span>
1077
- <input
1078
- type="text"
1079
- inputmode="numeric"
1080
- class="nxe-page-box"
1081
- [style.width.px]="pageBoxWidth"
1082
- [disabled]="pageControlDisabled()"
1083
- [value]="pageIndex() + 1"
1084
- [attr.aria-label]="strings().Page"
1085
- (keydown.enter)="enterPage($event)"
1086
- />
1087
- <span class="nxe-page-label">{{ strings().PageOf }} {{ pageCount() }}</span>
1088
- </span>
1089
- }
1090
- @if (showNextPage()) {
1091
- <button
1092
- type="button"
1093
- class="nxe-toolbar-button"
1094
- [disabled]="atEnd()"
1095
- [attr.aria-label]="strings().NextPage"
1096
- (click)="goTo(pageIndex() + 1)"
1097
- >
1098
- <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3v10l6.5-5z" /></svg>
1099
- </button>
1100
- }
1101
- @if (showLastPage()) {
1102
- <button
1103
- type="button"
1104
- class="nxe-toolbar-button"
1105
- [disabled]="atEnd()"
1106
- [attr.aria-label]="strings().LastPage"
1107
- (click)="goTo(pageCount() - 1)"
1108
- >
1109
- <svg viewBox="0 0 16 16" aria-hidden="true">
1110
- <path d="M3.5 3v10L10 8zM10.3 3h1.2v10h-1.2z" />
1111
- </svg>
1112
- </button>
1113
- }
1114
- @if (showZoom() || showViewMode()) {
1115
- <span class="nxe-toolbar-separator"></span>
1116
- }
1117
- @if (showZoom()) {
1118
- <div class="nxe-toolbar-menu">
1119
- <button
1120
- type="button"
1121
- class="nxe-toolbar-button nxe-toolbar-captioned"
1122
- [attr.aria-label]="strings().ZoomToolTip"
1123
- [attr.aria-expanded]="openMenu() === 'zoom'"
1124
- (click)="toggleMenu('zoom')"
1125
- >
1126
- <svg viewBox="0 0 16 16" aria-hidden="true">
1127
- <path
1128
- d="M3 2h7l3 3v9H3zm6.6 1.2v2.2h2.2zM5 8h6v1.1H5zm0 2.4h6v1.1H5z"
1129
- />
1130
- </svg>
1131
- <span class="nxe-toolbar-caption">{{ zoomCaption() }}</span>
1132
- <svg class="nxe-toolbar-arrow" viewBox="0 0 8 8" aria-hidden="true">
1133
- <path d="M1 3h6L4 6.2z" />
1134
- </svg>
1135
- </button>
1136
- @if (openMenu() === 'zoom') {
1137
- <div class="nxe-toolbar-popup" role="menu">
1138
- @for (percent of zoomPresets; track percent) {
1139
- <button
1140
- type="button"
1141
- class="nxe-toolbar-item"
1142
- role="menuitem"
1143
- [class.nxe-toolbar-item-on]="isZoom(percent)"
1144
- (click)="chooseZoom(percent)"
1145
- >
1146
- <span class="nxe-toolbar-item-icon"></span>{{ percent }}%
1147
- </button>
1148
- }
1149
- <span class="nxe-toolbar-item-separator"></span>
1150
- <button type="button" class="nxe-toolbar-item" role="menuitem" (click)="fit('page')">
1151
- <span class="nxe-toolbar-item-icon">
1152
- <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 2h10v12H3zm1.2 1.2v9.6h7.6V3.2z" /></svg>
1153
- </span>
1154
- {{ strings().ZoomOnePage }}
1155
- </button>
1156
- <button type="button" class="nxe-toolbar-item" role="menuitem" (click)="fit('width')">
1157
- <span class="nxe-toolbar-item-icon">
1158
- <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M1 6h14v4H1zm1.2 1.2v1.6h11.6V7.2z" /></svg>
1159
- </span>
1160
- {{ strings().ZoomPageWidth }}
1161
- </button>
1162
- </div>
1163
- }
1164
- </div>
1165
- }
1166
- @if (showViewMode()) {
1167
- <div class="nxe-toolbar-menu">
1168
- <button
1169
- type="button"
1170
- class="nxe-toolbar-button nxe-toolbar-captioned"
1171
- [attr.aria-label]="strings().ViewModeToolTip"
1172
- [attr.aria-expanded]="openMenu() === 'viewMode'"
1173
- (click)="toggleMenu('viewMode')"
1174
- >
1175
- <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 2h10v12H3zm1.2 1.2v9.6h7.6V3.2z" /></svg>
1176
- <span class="nxe-toolbar-caption">{{ strings()[viewMode()] }}</span>
1177
- <svg class="nxe-toolbar-arrow" viewBox="0 0 8 8" aria-hidden="true">
1178
- <path d="M1 3h6L4 6.2z" />
1179
- </svg>
1180
- </button>
1181
- @if (openMenu() === 'viewMode') {
1182
- <div class="nxe-toolbar-popup" role="menu">
1183
- @for (mode of viewModes; track mode) {
1184
- <button
1185
- type="button"
1186
- class="nxe-toolbar-item"
1187
- role="menuitem"
1188
- [class.nxe-toolbar-item-on]="viewMode() === mode"
1189
- (click)="chooseViewMode(mode)"
1190
- >
1191
- <span class="nxe-toolbar-item-icon">
1192
- @switch (mode) {
1193
- @case ('SinglePage') {
1194
- <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 2h10v12H3zm1.2 1.2v9.6h7.6V3.2z" /></svg>
1195
- }
1196
- @case ('Continuous') {
1197
- <svg viewBox="0 0 16 16" aria-hidden="true">
1198
- <path d="M3 1h10v6H3zm1.2 1.2v3.6h7.6V2.2zM3 9h10v6H3zm1.2 1.2v3.6h7.6v-3.6z" />
1199
- </svg>
1200
- }
1201
- @case ('MultiplePages') {
1202
- <svg viewBox="0 0 16 16" aria-hidden="true">
1203
- <path d="M2 2h5v5H2zm7 0h5v5H9zM2 9h5v5H2zm7 0h5v5H9z" />
1204
- </svg>
1205
- }
1206
- }
1207
- </span>
1208
- {{ strings()[mode] }}
1209
- </button>
1210
- }
1211
- </div>
1212
- }
1213
- </div>
1214
- }
1215
- `, isInline: true, styles: [":host{display:flex;align-items:center;gap:2px;box-sizing:border-box;padding:3px 6px;min-height:30px;position:relative;-webkit-user-select:none;user-select:none}:host(.nxe-busy):after{content:\"\";position:absolute;inset:0;cursor:default}.nxe-toolbar-button{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;padding:0;border:0;border-radius:2px;background:transparent;color:inherit;font:inherit;cursor:pointer}.nxe-toolbar-button:hover:not(:disabled){background:#00000014}.nxe-toolbar-button:disabled{opacity:.5;cursor:default}.nxe-toolbar-button svg{width:16px;height:16px;fill:currentColor}.nxe-page-control{display:inline-flex;align-items:center;gap:4px;padding:0 4px;white-space:nowrap}.nxe-page-box{height:21px;box-sizing:border-box;text-align:center;font:inherit;color:inherit}.nxe-page-box:disabled{color:inherit;-webkit-text-fill-color:currentColor;opacity:1}.nxe-toolbar-separator{width:1px;height:18px;margin:0 4px;background:currentColor;opacity:.25}.nxe-toolbar-menu{position:relative;display:flex;align-items:center}.nxe-toolbar-captioned{width:auto;gap:4px;padding:0 6px}.nxe-toolbar-caption{font-size:12px;white-space:nowrap}.nxe-toolbar-arrow{width:8px;height:8px;fill:currentColor;flex:none}.nxe-toolbar-popup{position:absolute;top:100%;left:0;z-index:10;min-width:150px;margin-top:2px;padding:3px 0;background:#fff;color:#000;border:1px solid #c8c8c8;box-shadow:0 2px 8px #00000040;display:flex;flex-direction:column}.nxe-toolbar-item{display:flex;align-items:center;gap:8px;border:0;background:transparent;color:inherit;font:inherit;font-size:12px;text-align:left;white-space:nowrap;padding:5px 14px 5px 6px;cursor:pointer}.nxe-toolbar-item:hover{background:#00000012}.nxe-toolbar-item-on{background:#0000001c}.nxe-toolbar-item-icon{width:16px;height:16px;flex:none;display:inline-flex;align-items:center;justify-content:center}.nxe-toolbar-item-icon svg{width:16px;height:16px;fill:currentColor}.nxe-toolbar-item-separator{height:1px;margin:3px 0;background:#c8c8c8}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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 });
1216
1631
  }
1217
1632
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeViewerToolbarComponent, decorators: [{
1218
1633
  type: Component,
1219
1634
  args: [{ selector: 'nxe-viewer-toolbar', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
1220
1635
  class: 'nxe-viewer-toolbar',
1221
1636
  role: 'toolbar',
1222
- '[style.background-color]': 'backgroundColor()',
1223
- '[style.border]': 'border()',
1224
1637
  '[style.color]': 'fontColor()',
1225
1638
  '[style.font-family]': 'fontFamily()',
1226
- '[style.justify-content]': 'justifyContent()',
1227
1639
  '[class.nxe-busy]': 'busy()',
1228
- }, template: `
1229
- @if (showSave()) {
1230
- <div class="nxe-toolbar-menu">
1231
- <button
1232
- type="button"
1233
- class="nxe-toolbar-button nxe-toolbar-captioned"
1234
- [attr.aria-label]="strings().SaveToolTip"
1235
- [attr.aria-expanded]="openMenu() === 'save'"
1236
- (click)="toggleMenu('save')"
1237
- >
1238
- <svg viewBox="0 0 16 16" aria-hidden="true">
1239
- <path
1240
- d="M2 2h9.5L14 4.5V14H2zm2.5 0v4.5h6V2zm-1 7.5h9V14h-9zM8.5 3h1.5v2.5H8.5z"
1241
- />
1242
- </svg>
1243
- <span class="nxe-toolbar-caption">{{ strings().Save }}</span>
1244
- <svg class="nxe-toolbar-arrow" viewBox="0 0 8 8" aria-hidden="true">
1245
- <path d="M1 3h6L4 6.2z" />
1246
- </svg>
1247
- </button>
1248
- @if (openMenu() === 'save') {
1249
- <div class="nxe-toolbar-popup" role="menu">
1250
- @for (format of saveFormats(); track format) {
1251
- <button
1252
- type="button"
1253
- class="nxe-toolbar-item"
1254
- role="menuitem"
1255
- (click)="save(format)"
1256
- >
1257
- <span class="nxe-toolbar-item-icon">
1258
- <svg viewBox="0 0 16 16" aria-hidden="true">
1259
- <path d="M3 1h7l3 3v11H3zm6.6 1.2v2.2h2.2z" />
1260
- </svg>
1261
- </span>
1262
- {{ format }}
1263
- </button>
1264
- }
1265
- </div>
1266
- }
1267
- </div>
1268
- <span class="nxe-toolbar-separator"></span>
1269
- }
1270
- @if (showFirstPage()) {
1271
- <button
1272
- type="button"
1273
- class="nxe-toolbar-button"
1274
- [disabled]="atStart()"
1275
- [attr.aria-label]="strings().FirstPage"
1276
- (click)="goTo(0)"
1277
- >
1278
- <svg viewBox="0 0 16 16" aria-hidden="true">
1279
- <path d="M12.5 3v10L6 8zM4.5 3h1.2v10H4.5z" />
1280
- </svg>
1281
- </button>
1282
- }
1283
- @if (showPreviousPage()) {
1284
- <button
1285
- type="button"
1286
- class="nxe-toolbar-button"
1287
- [disabled]="atStart()"
1288
- [attr.aria-label]="strings().PrevPage"
1289
- (click)="goTo(pageIndex() - 1)"
1290
- >
1291
- <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M11 3v10L4.5 8z" /></svg>
1292
- </button>
1293
- }
1294
- @if (showCurrentPage()) {
1295
- <span class="nxe-page-control">
1296
- <span class="nxe-page-label">{{ strings().Page }}</span>
1297
- <input
1298
- type="text"
1299
- inputmode="numeric"
1300
- class="nxe-page-box"
1301
- [style.width.px]="pageBoxWidth"
1302
- [disabled]="pageControlDisabled()"
1303
- [value]="pageIndex() + 1"
1304
- [attr.aria-label]="strings().Page"
1305
- (keydown.enter)="enterPage($event)"
1306
- />
1307
- <span class="nxe-page-label">{{ strings().PageOf }} {{ pageCount() }}</span>
1308
- </span>
1309
- }
1310
- @if (showNextPage()) {
1311
- <button
1312
- type="button"
1313
- class="nxe-toolbar-button"
1314
- [disabled]="atEnd()"
1315
- [attr.aria-label]="strings().NextPage"
1316
- (click)="goTo(pageIndex() + 1)"
1317
- >
1318
- <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3v10l6.5-5z" /></svg>
1319
- </button>
1320
- }
1321
- @if (showLastPage()) {
1322
- <button
1323
- type="button"
1324
- class="nxe-toolbar-button"
1325
- [disabled]="atEnd()"
1326
- [attr.aria-label]="strings().LastPage"
1327
- (click)="goTo(pageCount() - 1)"
1328
- >
1329
- <svg viewBox="0 0 16 16" aria-hidden="true">
1330
- <path d="M3.5 3v10L10 8zM10.3 3h1.2v10h-1.2z" />
1331
- </svg>
1332
- </button>
1333
- }
1334
- @if (showZoom() || showViewMode()) {
1335
- <span class="nxe-toolbar-separator"></span>
1336
- }
1337
- @if (showZoom()) {
1338
- <div class="nxe-toolbar-menu">
1339
- <button
1340
- type="button"
1341
- class="nxe-toolbar-button nxe-toolbar-captioned"
1342
- [attr.aria-label]="strings().ZoomToolTip"
1343
- [attr.aria-expanded]="openMenu() === 'zoom'"
1344
- (click)="toggleMenu('zoom')"
1345
- >
1346
- <svg viewBox="0 0 16 16" aria-hidden="true">
1347
- <path
1348
- d="M3 2h7l3 3v9H3zm6.6 1.2v2.2h2.2zM5 8h6v1.1H5zm0 2.4h6v1.1H5z"
1349
- />
1350
- </svg>
1351
- <span class="nxe-toolbar-caption">{{ zoomCaption() }}</span>
1352
- <svg class="nxe-toolbar-arrow" viewBox="0 0 8 8" aria-hidden="true">
1353
- <path d="M1 3h6L4 6.2z" />
1354
- </svg>
1355
- </button>
1356
- @if (openMenu() === 'zoom') {
1357
- <div class="nxe-toolbar-popup" role="menu">
1358
- @for (percent of zoomPresets; track percent) {
1359
- <button
1360
- type="button"
1361
- class="nxe-toolbar-item"
1362
- role="menuitem"
1363
- [class.nxe-toolbar-item-on]="isZoom(percent)"
1364
- (click)="chooseZoom(percent)"
1365
- >
1366
- <span class="nxe-toolbar-item-icon"></span>{{ percent }}%
1367
- </button>
1368
- }
1369
- <span class="nxe-toolbar-item-separator"></span>
1370
- <button type="button" class="nxe-toolbar-item" role="menuitem" (click)="fit('page')">
1371
- <span class="nxe-toolbar-item-icon">
1372
- <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 2h10v12H3zm1.2 1.2v9.6h7.6V3.2z" /></svg>
1373
- </span>
1374
- {{ strings().ZoomOnePage }}
1375
- </button>
1376
- <button type="button" class="nxe-toolbar-item" role="menuitem" (click)="fit('width')">
1377
- <span class="nxe-toolbar-item-icon">
1378
- <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M1 6h14v4H1zm1.2 1.2v1.6h11.6V7.2z" /></svg>
1379
- </span>
1380
- {{ strings().ZoomPageWidth }}
1381
- </button>
1382
- </div>
1383
- }
1384
- </div>
1385
- }
1386
- @if (showViewMode()) {
1387
- <div class="nxe-toolbar-menu">
1388
- <button
1389
- type="button"
1390
- class="nxe-toolbar-button nxe-toolbar-captioned"
1391
- [attr.aria-label]="strings().ViewModeToolTip"
1392
- [attr.aria-expanded]="openMenu() === 'viewMode'"
1393
- (click)="toggleMenu('viewMode')"
1394
- >
1395
- <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 2h10v12H3zm1.2 1.2v9.6h7.6V3.2z" /></svg>
1396
- <span class="nxe-toolbar-caption">{{ strings()[viewMode()] }}</span>
1397
- <svg class="nxe-toolbar-arrow" viewBox="0 0 8 8" aria-hidden="true">
1398
- <path d="M1 3h6L4 6.2z" />
1399
- </svg>
1400
- </button>
1401
- @if (openMenu() === 'viewMode') {
1402
- <div class="nxe-toolbar-popup" role="menu">
1403
- @for (mode of viewModes; track mode) {
1404
- <button
1405
- type="button"
1406
- class="nxe-toolbar-item"
1407
- role="menuitem"
1408
- [class.nxe-toolbar-item-on]="viewMode() === mode"
1409
- (click)="chooseViewMode(mode)"
1410
- >
1411
- <span class="nxe-toolbar-item-icon">
1412
- @switch (mode) {
1413
- @case ('SinglePage') {
1414
- <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 2h10v12H3zm1.2 1.2v9.6h7.6V3.2z" /></svg>
1415
- }
1416
- @case ('Continuous') {
1417
- <svg viewBox="0 0 16 16" aria-hidden="true">
1418
- <path d="M3 1h10v6H3zm1.2 1.2v3.6h7.6V2.2zM3 9h10v6H3zm1.2 1.2v3.6h7.6v-3.6z" />
1419
- </svg>
1420
- }
1421
- @case ('MultiplePages') {
1422
- <svg viewBox="0 0 16 16" aria-hidden="true">
1423
- <path d="M2 2h5v5H2zm7 0h5v5H9zM2 9h5v5H2zm7 0h5v5H9z" />
1424
- </svg>
1425
- }
1426
- }
1427
- </span>
1428
- {{ strings()[mode] }}
1429
- </button>
1430
- }
1431
- </div>
1432
- }
1433
- </div>
1434
- }
1435
- `, styles: [":host{display:flex;align-items:center;gap:2px;box-sizing:border-box;padding:3px 6px;min-height:30px;position:relative;-webkit-user-select:none;user-select:none}:host(.nxe-busy):after{content:\"\";position:absolute;inset:0;cursor:default}.nxe-toolbar-button{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;padding:0;border:0;border-radius:2px;background:transparent;color:inherit;font:inherit;cursor:pointer}.nxe-toolbar-button:hover:not(:disabled){background:#00000014}.nxe-toolbar-button:disabled{opacity:.5;cursor:default}.nxe-toolbar-button svg{width:16px;height:16px;fill:currentColor}.nxe-page-control{display:inline-flex;align-items:center;gap:4px;padding:0 4px;white-space:nowrap}.nxe-page-box{height:21px;box-sizing:border-box;text-align:center;font:inherit;color:inherit}.nxe-page-box:disabled{color:inherit;-webkit-text-fill-color:currentColor;opacity:1}.nxe-toolbar-separator{width:1px;height:18px;margin:0 4px;background:currentColor;opacity:.25}.nxe-toolbar-menu{position:relative;display:flex;align-items:center}.nxe-toolbar-captioned{width:auto;gap:4px;padding:0 6px}.nxe-toolbar-caption{font-size:12px;white-space:nowrap}.nxe-toolbar-arrow{width:8px;height:8px;fill:currentColor;flex:none}.nxe-toolbar-popup{position:absolute;top:100%;left:0;z-index:10;min-width:150px;margin-top:2px;padding:3px 0;background:#fff;color:#000;border:1px solid #c8c8c8;box-shadow:0 2px 8px #00000040;display:flex;flex-direction:column}.nxe-toolbar-item{display:flex;align-items:center;gap:8px;border:0;background:transparent;color:inherit;font:inherit;font-size:12px;text-align:left;white-space:nowrap;padding:5px 14px 5px 6px;cursor:pointer}.nxe-toolbar-item:hover{background:#00000012}.nxe-toolbar-item-on{background:#0000001c}.nxe-toolbar-item-icon{width:16px;height:16px;flex:none;display:inline-flex;align-items:center;justify-content:center}.nxe-toolbar-item-icon svg{width:16px;height:16px;fill:currentColor}.nxe-toolbar-item-separator{height:1px;margin:3px 0;background:#c8c8c8}\n"] }]
1436
- }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], pageIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageIndex", required: false }] }], pageCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageCount", required: false }] }], busy: [{ type: i0.Input, args: [{ isSignal: true, alias: "busy", required: false }] }], strings: [{ type: i0.Input, args: [{ isSignal: true, alias: "strings", required: false }] }], viewMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewMode", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }], goToPage: [{ type: i0.Output, args: ["goToPage"] }], viewModeChange: [{ type: i0.Output, args: ["viewModeChange"] }], zoomChange: [{ type: i0.Output, args: ["zoomChange"] }], fitRequest: [{ type: i0.Output, args: ["fitRequest"] }], saveRequest: [{ type: i0.Output, args: ["saveRequest"] }] } });
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"] }] } });
1437
1934
 
1438
1935
  /** Stimulsoft's scroll tracker waits this long after scrolling stops before it moves the page. */
1439
1936
  const SCROLL_SETTLE_MS = 300;
@@ -1443,6 +1940,8 @@ const SCROLL_STEP_MS = 30;
1443
1940
  /** The viewer never measures itself smaller than this, nor closer than this to the window's bottom. */
1444
1941
  const MIN_PANEL_HEIGHT = 200;
1445
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;
1446
1945
  /**
1447
1946
  * A report served by `Nextera.Reports.Angular.NetCore`, shown page by page.
1448
1947
  *
@@ -1520,6 +2019,25 @@ class NxeReportViewerComponent {
1520
2019
  scrollSettled = null;
1521
2020
  toolbarVisible = computed(() => this.session.options()?.toolbar.visible !== false, /* @ts-ignore */
1522
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 */ []));
1523
2041
  pageCss = computed(() => this.session.report()?.pageCss ?? '', /* @ts-ignore */
1524
2042
  ...(ngDevMode ? [{ debugName: "pageCss" }] : /* istanbul ignore next */ []));
1525
2043
  showPageShadow = computed(() => this.appearance()?.showPageShadow === true, /* @ts-ignore */
@@ -1580,6 +2098,117 @@ class NxeReportViewerComponent {
1580
2098
  this.session.failed
1581
2099
  .pipe(takeUntilDestroyed())
1582
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));
1583
2212
  }
1584
2213
  /**
1585
2214
  * A change to any input reloads, as Stimulsoft's viewer does: its `ngOnChanges` never looks at
@@ -1662,10 +2291,11 @@ class NxeReportViewerComponent {
1662
2291
  if (!root || !this.appearance()?.scrollbarsMode)
1663
2292
  return;
1664
2293
  const rect = root.getBoundingClientRect();
1665
- const room = Math.max(MIN_PANEL_HEIGHT, Math.round(window.innerHeight - rect.top - VIEWER_BOTTOM_GAP));
1666
- // A height that resolved is left alone; one that grew past the room on screen is taken over.
1667
- if (this.measuredHeight() === null && rect.height <= room + 1)
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)
1668
2297
  return;
2298
+ const room = Math.max(MIN_PANEL_HEIGHT, Math.round(window.innerHeight - rect.top - VIEWER_BOTTOM_GAP));
1669
2299
  this.measuredHeight.set(`${room}px`);
1670
2300
  }
1671
2301
  /** Every page whose box meets the panel, plus one either side, so scrolling stays ahead of the eye. */
@@ -1709,8 +2339,11 @@ class NxeReportViewerComponent {
1709
2339
  const element = box?.querySelectorAll('nxe-report-page')[index];
1710
2340
  if (!box || !(element instanceof HTMLElement))
1711
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) {
1712
2346
  const from = box.scrollTop;
1713
- const to = element.offsetTop - box.offsetTop;
1714
2347
  const started = performance.now();
1715
2348
  this.scrollingToPage = true;
1716
2349
  const step = () => {
@@ -1756,121 +2389,143 @@ class NxeReportViewerComponent {
1756
2389
  };
1757
2390
  }
1758
2391
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeReportViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1759
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeReportViewerComponent, isStandalone: true, selector: "nxe-report-viewer", inputs: { requestUrl: { classPropertyName: "requestUrl", publicName: "requestUrl", isSignal: true, isRequired: false, transformFunction: null }, action: { classPropertyName: "action", publicName: "action", isSignal: true, isRequired: false, transformFunction: null }, properties: { classPropertyName: "properties", publicName: "properties", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, backgroundColor: { classPropertyName: "backgroundColor", publicName: "backgroundColor", isSignal: true, isRequired: false, transformFunction: null }, style: { classPropertyName: "style", publicName: "style", isSignal: true, isRequired: false, transformFunction: null }, postParametersFunction: { classPropertyName: "postParametersFunction", publicName: "postParametersFunction", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loaded: "loaded", error: "error" }, host: { listeners: { "window:resize": "onWindowResize()" } }, providers: [NxeViewerClientService, NxeViewerSessionService], viewQueries: [{ propertyName: "pagesBox", first: true, predicate: ["pagesBox"], descendants: true, isSignal: true }, { propertyName: "rootBox", first: true, predicate: ["rootBox"], descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: `
1760
- <div
1761
- #rootBox
1762
- class="nxe-viewer-root"
1763
- [style]="style()"
1764
- [style.background-color]="rootBackgroundColor()"
1765
- [style.width]="rootWidth()"
1766
- [style.height]="rootHeight()"
1767
- >
1768
- @if (toolbarVisible()) {
1769
- <nxe-viewer-toolbar
1770
- [options]="session.options()"
1771
- [pageIndex]="session.pageIndex()"
1772
- [pageCount]="session.pageCount()"
1773
- [busy]="session.busy()"
1774
- [viewMode]="viewMode()"
1775
- [zoom]="zoom()"
1776
- (goToPage)="navigateTo($event)"
1777
- (viewModeChange)="setViewMode($event)"
1778
- (zoomChange)="setZoom($event)"
1779
- (fitRequest)="fitTo($event)"
1780
- (saveRequest)="session.exportTo($event)"
1781
- />
1782
- }
1783
-
1784
- <div
1785
- #pagesBox
1786
- class="nxe-viewer-pages"
1787
- [class.nxe-viewer-continuous]="viewMode() === 'Continuous'"
1788
- [style.text-align]="pageAlignment()"
1789
- [style.overflow]="pagesOverflow()"
1790
- (scroll)="onScroll()"
1791
- >
1792
- @for (page of shownPages(); track page.index) {
1793
- <nxe-report-page
1794
- class="nxe-viewer-page"
1795
- [class.nxe-viewer-page-shadow]="showPageShadow()"
1796
- [style.border]="pageBorder()"
1797
- [page]="page.size"
1798
- [html]="page.html"
1799
- [css]="pageCss()"
1800
- [zoom]="zoom()"
1801
- />
1802
- }
1803
- </div>
1804
-
1805
- @if (session.failure(); as failure) {
1806
- <div class="nxe-viewer-failure" role="alert">
1807
- <span class="nxe-viewer-failure-text">{{ failure.message }}</span>
1808
- <button type="button" class="nxe-viewer-failure-close" (click)="session.dismissFailure()">
1809
- &times;
1810
- </button>
1811
- </div>
1812
- }
1813
- </div>
1814
- `, isInline: true, styles: [":host{display:inline-block;position:relative}.nxe-viewer-root{display:flex;flex-direction:column;box-sizing:border-box;position:relative}.nxe-viewer-pages{flex:1 1 auto;min-height:0;box-sizing:border-box}.nxe-viewer-page{display:inline-block;vertical-align:top;margin:10px}.nxe-viewer-continuous .nxe-viewer-page{display:block;margin:10px auto}.nxe-viewer-page-shadow{box-shadow:2px 2px 6px #0000004d}.nxe-viewer-failure{position:absolute;left:0;right:0;bottom:0;display:flex;align-items:center;gap:8px;padding:8px 12px;background:#fdecea;color:#611a15;font-size:13px}.nxe-viewer-failure-text{flex:1 1 auto}.nxe-viewer-failure-close{border:0;background:transparent;color:inherit;font-size:18px;line-height:1;cursor:pointer}\n"], dependencies: [{ kind: "component", type: NxeReportPageComponent, selector: "nxe-report-page", inputs: ["page", "html", "css", "zoom"] }, { kind: "component", type: NxeViewerToolbarComponent, selector: "nxe-viewer-toolbar", inputs: ["options", "pageIndex", "pageCount", "busy", "strings", "viewMode", "zoom"], outputs: ["goToPage", "viewModeChange", "zoomChange", "fitRequest", "saveRequest"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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 });
1815
2459
  }
1816
2460
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeReportViewerComponent, decorators: [{
1817
2461
  type: Component,
1818
- args: [{ selector: 'nxe-report-viewer', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NxeReportPageComponent, NxeViewerToolbarComponent], providers: [NxeViewerClientService, NxeViewerSessionService], host: { '(window:resize)': 'onWindowResize()' }, template: `
1819
- <div
1820
- #rootBox
1821
- class="nxe-viewer-root"
1822
- [style]="style()"
1823
- [style.background-color]="rootBackgroundColor()"
1824
- [style.width]="rootWidth()"
1825
- [style.height]="rootHeight()"
1826
- >
1827
- @if (toolbarVisible()) {
1828
- <nxe-viewer-toolbar
1829
- [options]="session.options()"
1830
- [pageIndex]="session.pageIndex()"
1831
- [pageCount]="session.pageCount()"
1832
- [busy]="session.busy()"
1833
- [viewMode]="viewMode()"
1834
- [zoom]="zoom()"
1835
- (goToPage)="navigateTo($event)"
1836
- (viewModeChange)="setViewMode($event)"
1837
- (zoomChange)="setZoom($event)"
1838
- (fitRequest)="fitTo($event)"
1839
- (saveRequest)="session.exportTo($event)"
1840
- />
1841
- }
1842
-
1843
- <div
1844
- #pagesBox
1845
- class="nxe-viewer-pages"
1846
- [class.nxe-viewer-continuous]="viewMode() === 'Continuous'"
1847
- [style.text-align]="pageAlignment()"
1848
- [style.overflow]="pagesOverflow()"
1849
- (scroll)="onScroll()"
1850
- >
1851
- @for (page of shownPages(); track page.index) {
1852
- <nxe-report-page
1853
- class="nxe-viewer-page"
1854
- [class.nxe-viewer-page-shadow]="showPageShadow()"
1855
- [style.border]="pageBorder()"
1856
- [page]="page.size"
1857
- [html]="page.html"
1858
- [css]="pageCss()"
1859
- [zoom]="zoom()"
1860
- />
1861
- }
1862
- </div>
1863
-
1864
- @if (session.failure(); as failure) {
1865
- <div class="nxe-viewer-failure" role="alert">
1866
- <span class="nxe-viewer-failure-text">{{ failure.message }}</span>
1867
- <button type="button" class="nxe-viewer-failure-close" (click)="session.dismissFailure()">
1868
- &times;
1869
- </button>
1870
- </div>
1871
- }
1872
- </div>
1873
- `, styles: [":host{display:inline-block;position:relative}.nxe-viewer-root{display:flex;flex-direction:column;box-sizing:border-box;position:relative}.nxe-viewer-pages{flex:1 1 auto;min-height:0;box-sizing:border-box}.nxe-viewer-page{display:inline-block;vertical-align:top;margin:10px}.nxe-viewer-continuous .nxe-viewer-page{display:block;margin:10px auto}.nxe-viewer-page-shadow{box-shadow:2px 2px 6px #0000004d}.nxe-viewer-failure{position:absolute;left:0;right:0;bottom:0;display:flex;align-items:center;gap:8px;padding:8px 12px;background:#fdecea;color:#611a15;font-size:13px}.nxe-viewer-failure-text{flex:1 1 auto}.nxe-viewer-failure-close{border:0;background:transparent;color:inherit;font-size:18px;line-height:1;cursor:pointer}\n"] }]
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"] }]
1874
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 }] }] } });
1875
2530
 
1876
2531
  /*