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