@arsedizioni/ars-utils 22.0.21 → 22.0.22
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/fesm2022/arsedizioni-ars-utils-core.mjs +2282 -1971
- package/fesm2022/arsedizioni-ars-utils-core.mjs.map +1 -1
- package/fesm2022/arsedizioni-ars-utils-tinymce.mjs +24 -22
- package/fesm2022/arsedizioni-ars-utils-tinymce.mjs.map +1 -1
- package/fesm2022/arsedizioni-ars-utils-ui.application.mjs +129 -98
- package/fesm2022/arsedizioni-ars-utils-ui.application.mjs.map +1 -1
- package/fesm2022/arsedizioni-ars-utils-ui.mjs +3228 -3055
- package/fesm2022/arsedizioni-ars-utils-ui.mjs.map +1 -1
- package/fesm2022/arsedizioni-ars-utils-ui.oauth.mjs +63 -47
- package/fesm2022/arsedizioni-ars-utils-ui.oauth.mjs.map +1 -1
- package/package.json +1 -1
- package/types/arsedizioni-ars-utils-clipper.ui.d.ts +1 -1
- package/types/arsedizioni-ars-utils-core.d.ts +202 -39
- package/types/arsedizioni-ars-utils-tinymce.d.ts +5 -1
- package/types/arsedizioni-ars-utils-ui.application.d.ts +21 -16
- package/types/arsedizioni-ars-utils-ui.d.ts +102 -146
- package/types/arsedizioni-ars-utils-ui.oauth.d.ts +7 -1
|
@@ -1,2091 +1,2008 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
-
import { parseISO, parse, format, getYear, getMonth, getDate, getDay, getDaysInMonth, addYears, addMonths, addDays, formatISO, isDate, isValid, endOfDay } from 'date-fns';
|
|
4
|
-
import { it } from 'date-fns/locale';
|
|
5
|
-
import { TZDate } from '@date-fns/tz';
|
|
6
|
-
import { DomSanitizer } from '@angular/platform-browser';
|
|
2
|
+
import { inject, Injectable, makeEnvironmentProviders, ElementRef, afterNextRender, Directive, input, DestroyRef, HostListener, output, forwardRef, effect, Pipe, EventEmitter, signal, computed, Service, PLATFORM_ID, RendererFactory2 } from '@angular/core';
|
|
7
3
|
import { DateAdapter, MAT_DATE_LOCALE, MAT_DATE_FORMATS } from '@angular/material/core';
|
|
4
|
+
import { TZDate } from '@date-fns/tz';
|
|
5
|
+
import { getYear, getMonth, getDate, getDay, getDaysInMonth, parseISO, parse, format, addYears, addMonths, addDays, formatISO, isDate, isValid, endOfDay } from 'date-fns';
|
|
8
6
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
9
|
-
import { Subject, BehaviorSubject } from 'rxjs';
|
|
10
|
-
import { debounceTime } from 'rxjs/operators';
|
|
7
|
+
import { Subject, filter as filter$1, map as map$1, BehaviorSubject } from 'rxjs';
|
|
8
|
+
import { debounceTime, filter, map } from 'rxjs/operators';
|
|
9
|
+
import { it } from 'date-fns/locale';
|
|
11
10
|
import { NG_VALIDATORS } from '@angular/forms';
|
|
11
|
+
import { DomSanitizer } from '@angular/platform-browser';
|
|
12
12
|
import { SelectionModel } from '@angular/cdk/collections';
|
|
13
13
|
import { isPlatformBrowser } from '@angular/common';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* - Inline formatting never touches code spans / code blocks (stash & restore).
|
|
21
|
-
* - No HTML entity escaping by default in normal text: the source passes as-is.
|
|
22
|
-
* Exception: the content of code spans / code blocks is ALWAYS escaped, so
|
|
23
|
-
* things like List<string> render correctly (the browser displays the
|
|
24
|
-
* original characters; entities never reach the user or the clipboard).
|
|
25
|
-
* Opt-in full escaping via { escapeHtml: true } for untrusted input.
|
|
26
|
-
* - URL sanitization on links/images (javascript:, vbscript:, data: are dropped).
|
|
27
|
-
* - Bounded result cache for repeated renders (Angular change detection friendly).
|
|
28
|
-
*
|
|
29
|
-
* Supported syntax: headings, paragraphs, hard/soft breaks, hr, blockquotes
|
|
30
|
-
* (nested), fenced code blocks, inline code, bold/italic/strikethrough, links
|
|
31
|
-
* (with title), images, autolinked bare URLs, ordered/unordered lists (nested,
|
|
32
|
-
* task lists, ordered start offset), GFM tables with alignment, raw HTML
|
|
33
|
-
* blocks (e.g. <table>...</table>) passed through verbatim when escapeHtml
|
|
34
|
-
* is false: their newlines stay plain newlines, never converted to <br>.
|
|
35
|
-
*
|
|
36
|
-
* Known simplifications (documented, by design):
|
|
37
|
-
* - A blank line terminates a list.
|
|
38
|
-
* - Setext headings (=== / ---) are not supported, use # syntax.
|
|
39
|
-
* - Reference-style links [text][ref] are not supported.
|
|
16
|
+
* Creates an array of the given length, filling each slot with the result of `valueFunction`.
|
|
17
|
+
* @param length - Number of elements to create.
|
|
18
|
+
* @param valueFunction - Factory called with each index to produce the element value.
|
|
19
|
+
* @returns Typed array of `length` elements.
|
|
40
20
|
*/
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
/** Clears the internal result cache. */
|
|
46
|
-
static clearCache() {
|
|
47
|
-
this.cache.clear();
|
|
21
|
+
function range(length, valueFunction) {
|
|
22
|
+
const valuesArray = Array(length);
|
|
23
|
+
for (let i = 0; i < length; i++) {
|
|
24
|
+
valuesArray[i] = valueFunction(i);
|
|
48
25
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
26
|
+
return valuesArray;
|
|
27
|
+
}
|
|
28
|
+
// date-fns doesn't have a way to read/print month names or days of the week directly,
|
|
29
|
+
// so we get them by formatting a date with a format that produces the desired month/day.
|
|
30
|
+
const MONTH_FORMATS = {
|
|
31
|
+
long: 'LLLL',
|
|
32
|
+
short: 'LLL',
|
|
33
|
+
narrow: 'LLLLL',
|
|
34
|
+
};
|
|
35
|
+
const DAY_OF_WEEK_FORMATS = {
|
|
36
|
+
long: 'EEEE',
|
|
37
|
+
short: 'EEE',
|
|
38
|
+
narrow: 'EEEEE',
|
|
39
|
+
};
|
|
40
|
+
const MAT_DATE_FNS_FORMATS = {
|
|
41
|
+
parse: {
|
|
42
|
+
dateInput: 'P',
|
|
43
|
+
},
|
|
44
|
+
display: {
|
|
45
|
+
dateInput: 'P',
|
|
46
|
+
monthYearLabel: 'LLL uuuu',
|
|
47
|
+
dateA11yLabel: 'PP',
|
|
48
|
+
monthYearA11yLabel: 'LLLL uuuu',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* date-fns adapter that integrates Angular Material's date picker with the date-fns library,
|
|
53
|
+
* applying `Europe/Rome` timezone for all parsed and created dates.
|
|
54
|
+
*/
|
|
55
|
+
class DateFnsAdapter extends DateAdapter {
|
|
56
|
+
constructor() {
|
|
57
|
+
super();
|
|
58
|
+
const matDateLocale = inject(MAT_DATE_LOCALE, { optional: true });
|
|
59
|
+
// MAT_DATE_LOCALE defaults to LOCALE_ID (a string): only a date-fns
|
|
60
|
+
// Locale OBJECT is usable here, a string would break format()/parse().
|
|
61
|
+
if (matDateLocale && typeof matDateLocale === 'object') {
|
|
62
|
+
this.setLocale(matDateLocale);
|
|
72
63
|
}
|
|
73
|
-
this.cache.set(key, html);
|
|
74
|
-
return html;
|
|
75
64
|
}
|
|
76
|
-
// #endregion
|
|
77
|
-
// #region Block parser (single pass)
|
|
78
|
-
/** Block-level HTML tags that start a raw HTML block (passthrough, no <p>/<br>). */
|
|
79
|
-
static { this.htmlBlockTags = new Set([
|
|
80
|
-
'address', 'article', 'aside', 'blockquote', 'caption', 'colgroup', 'col',
|
|
81
|
-
'dd', 'details', 'dialog', 'div', 'dl', 'dt', 'fieldset', 'figcaption',
|
|
82
|
-
'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header',
|
|
83
|
-
'hr', 'iframe', 'li', 'main', 'nav', 'ol', 'p', 'pre', 'section', 'summary',
|
|
84
|
-
'table', 'tbody', 'td', 'tfoot', 'thead', 'th', 'tr', 'ul',
|
|
85
|
-
]); }
|
|
86
|
-
/** Void/self-contained tags: a single tag, no closing tag expected. */
|
|
87
|
-
static { this.htmlVoidTags = new Set(['hr', 'col', 'img', 'input']); }
|
|
88
65
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* prevents partial matches ('<td' must not match inside '<tdx').
|
|
66
|
+
* Returns the year component of the given date.
|
|
67
|
+
* @param date - The source date.
|
|
92
68
|
*/
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
static findHtmlBlockStart(line) {
|
|
96
|
-
// HTML comment candidate (skipping occurrences inside inline code spans)
|
|
97
|
-
let comment = line.indexOf('<!--');
|
|
98
|
-
while (comment !== -1 && this.insideCodeSpan(line, comment)) {
|
|
99
|
-
comment = line.indexOf('<!--', comment + 4);
|
|
100
|
-
}
|
|
101
|
-
// Block tag candidate (same code-span guard)
|
|
102
|
-
const re = this.htmlBlockScanRe;
|
|
103
|
-
re.lastIndex = 0;
|
|
104
|
-
let m;
|
|
105
|
-
let tagHit;
|
|
106
|
-
while ((m = re.exec(line)) !== null) {
|
|
107
|
-
if (!this.insideCodeSpan(line, m.index)) {
|
|
108
|
-
tagHit = { index: m.index, tag: m[2].toLowerCase(), isClose: m[1] === '/' };
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (comment !== -1 && (!tagHit || comment < tagHit.index)) {
|
|
113
|
-
return { index: comment, tag: '', isClose: false, isComment: true };
|
|
114
|
-
}
|
|
115
|
-
return tagHit ? { ...tagHit, isComment: false } : undefined;
|
|
69
|
+
getYear(date) {
|
|
70
|
+
return getYear(date);
|
|
116
71
|
}
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
return (count & 1) === 1;
|
|
72
|
+
/**
|
|
73
|
+
* Returns the zero-based month index of the given date (0 = January).
|
|
74
|
+
* @param date - The source date.
|
|
75
|
+
*/
|
|
76
|
+
getMonth(date) {
|
|
77
|
+
return getMonth(date);
|
|
125
78
|
}
|
|
126
79
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* Self-closing <tag ... /> forms do not alter depth.
|
|
130
|
-
* @returns [newDepth, endIndex] where endIndex is the position right after
|
|
131
|
-
* the '>' that balanced the element, or -1 when still open.
|
|
80
|
+
* Returns the day-of-month of the given date (1-based).
|
|
81
|
+
* @param date - The source date.
|
|
132
82
|
*/
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const open = '<' + tag;
|
|
136
|
-
const close = '</' + tag;
|
|
137
|
-
let i = 0;
|
|
138
|
-
while ((i = lower.indexOf('<', i)) !== -1) {
|
|
139
|
-
if (lower.startsWith(close, i) && this.isTagBoundary(lower, i + close.length)) {
|
|
140
|
-
depth--;
|
|
141
|
-
const gt = lower.indexOf('>', i);
|
|
142
|
-
const end = gt === -1 ? lower.length : gt + 1;
|
|
143
|
-
if (depth <= 0)
|
|
144
|
-
return [0, end];
|
|
145
|
-
i = end;
|
|
146
|
-
}
|
|
147
|
-
else if (lower.startsWith(open, i) && this.isTagBoundary(lower, i + open.length)) {
|
|
148
|
-
const gt = lower.indexOf('>', i);
|
|
149
|
-
const selfClosing = gt !== -1 && lower.charCodeAt(gt - 1) === 47 /* / */;
|
|
150
|
-
if (selfClosing) {
|
|
151
|
-
if (depth === 0)
|
|
152
|
-
return [0, gt + 1]; // standalone self-closed element
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
depth++;
|
|
156
|
-
}
|
|
157
|
-
i = gt === -1 ? lower.length : gt + 1;
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
i++;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return [depth, -1];
|
|
83
|
+
getDate(date) {
|
|
84
|
+
return getDate(date);
|
|
164
85
|
}
|
|
165
|
-
/**
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return
|
|
86
|
+
/**
|
|
87
|
+
* Returns the day-of-week of the given date (0 = Sunday).
|
|
88
|
+
* @param date - The source date.
|
|
89
|
+
*/
|
|
90
|
+
getDayOfWeek(date) {
|
|
91
|
+
return getDay(date);
|
|
171
92
|
}
|
|
172
93
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
94
|
+
* Returns an array of 12 month name strings formatted for the active locale.
|
|
95
|
+
* @param style - One of `'long'`, `'short'`, or `'narrow'`.
|
|
175
96
|
*/
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return i - 1; // the loop's i++ re-processes the remainder
|
|
180
|
-
}
|
|
181
|
-
return i;
|
|
97
|
+
getMonthNames(style) {
|
|
98
|
+
const pattern = MONTH_FORMATS[style];
|
|
99
|
+
return range(12, i => this.format(new Date(2017, i, 1), pattern));
|
|
182
100
|
}
|
|
183
101
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* the closing line is pushed back for markdown processing.
|
|
187
|
-
* @returns the index of the last consumed line
|
|
102
|
+
* Returns an array of 31 day-of-month label strings formatted using `Intl.DateTimeFormat`
|
|
103
|
+
* when available, falling back to plain numeric strings.
|
|
188
104
|
*/
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
105
|
+
getDateNames() {
|
|
106
|
+
const dtf = typeof Intl !== 'undefined'
|
|
107
|
+
? new Intl.DateTimeFormat(this.locale?.code, {
|
|
108
|
+
day: 'numeric',
|
|
109
|
+
timeZone: 'utc',
|
|
110
|
+
})
|
|
111
|
+
: null;
|
|
112
|
+
return range(31, i => {
|
|
113
|
+
if (dtf) {
|
|
114
|
+
// date-fns doesn't appear to support this functionality.
|
|
115
|
+
// Fall back to `Intl` on supported browsers.
|
|
116
|
+
const date = new Date();
|
|
117
|
+
date.setUTCFullYear(2017, 0, i + 1);
|
|
118
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
119
|
+
return dtf.format(date).replace(/[\u200e\u200f]/g, '');
|
|
197
120
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
121
|
+
return String(i + 1);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Returns an array of 7 day-of-week name strings formatted for the active locale.
|
|
126
|
+
* @param style - One of `'long'`, `'short'`, or `'narrow'`.
|
|
127
|
+
*/
|
|
128
|
+
getDayOfWeekNames(style) {
|
|
129
|
+
const pattern = DAY_OF_WEEK_FORMATS[style];
|
|
130
|
+
return range(7, i => this.format(new Date(2017, 0, i + 1), pattern));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Returns the four-digit year string for the given date.
|
|
134
|
+
* @param date - The source date.
|
|
135
|
+
*/
|
|
136
|
+
getYearName(date) {
|
|
137
|
+
return this.format(date, 'y');
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Returns the first day of the week for the active locale (0 = Sunday, 1 = Monday, …).
|
|
141
|
+
*/
|
|
142
|
+
getFirstDayOfWeek() {
|
|
143
|
+
return this.locale?.options?.weekStartsOn ?? 0;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Returns the number of days in the month of the given date.
|
|
147
|
+
* @param date - The source date.
|
|
148
|
+
*/
|
|
149
|
+
getNumDaysInMonth(date) {
|
|
150
|
+
return getDaysInMonth(date);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Creates an independent copy of the given date.
|
|
154
|
+
* @param date - The date to clone.
|
|
155
|
+
*/
|
|
156
|
+
clone(date) {
|
|
157
|
+
return new Date(date.getTime());
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Creates a `Date` in the `Europe/Rome` timezone for the given year, month, and day.
|
|
161
|
+
* Throws an `Error` when any component is out of range.
|
|
162
|
+
* @param year - Full four-digit year.
|
|
163
|
+
* @param month - Zero-based month index (0 = January, 11 = December).
|
|
164
|
+
* @param date - Day-of-month (1-based).
|
|
165
|
+
*/
|
|
166
|
+
createDate(year, month, date) {
|
|
167
|
+
if (month < 0 || month > 11) {
|
|
168
|
+
throw Error(`Invalid month index "${month}". Month index has to be between 0 and 11.`);
|
|
215
169
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (split !== -1) {
|
|
219
|
-
out.push(line.slice(hit.index, hit.index + split));
|
|
220
|
-
return this.pushBack(lines, i, line.slice(hit.index + split));
|
|
170
|
+
if (date < 1) {
|
|
171
|
+
throw Error(`Invalid date "${date}". Date has to be greater than 0.`);
|
|
221
172
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
173
|
+
// Passing the year to the constructor causes year numbers <100 to be converted to 19xx.
|
|
174
|
+
// To work around this we use `setFullYear` and `setHours` instead.
|
|
175
|
+
const result = new Date();
|
|
176
|
+
result.setFullYear(year, month, date);
|
|
177
|
+
result.setHours(0, 0, 0, 0);
|
|
178
|
+
const result2 = new TZDate(result, 'Europe/Rome');
|
|
179
|
+
if (result2.getMonth() !== month) {
|
|
180
|
+
throw Error(`Invalid date "${date}" for month with index "${month}".`);
|
|
230
181
|
}
|
|
231
|
-
return
|
|
182
|
+
return result2;
|
|
232
183
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
while (i < lines.length && !/^```\s*$/.test(lines[i].trim())) {
|
|
254
|
-
code.push(lines[i]);
|
|
255
|
-
i++;
|
|
256
|
-
}
|
|
257
|
-
out.push(`<pre><code${lang}>${this.escape(code.join('\n'))}</code></pre>`);
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
// --- Blank line --------------------------------------------------------
|
|
261
|
-
if (!trimmed) {
|
|
262
|
-
flush();
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
// --- Raw HTML block passthrough (only with escaping disabled) ------------
|
|
266
|
-
// A block-level HTML element found ANYWHERE in the line starts a raw
|
|
267
|
-
// block: text before it joins the current paragraph, the element is
|
|
268
|
-
// emitted as-is (plain newlines, no <p>, no <br>) until balanced, and
|
|
269
|
-
// any text after the closing tag resumes markdown processing.
|
|
270
|
-
if (!opts.escapeHtml && line.includes('<')) {
|
|
271
|
-
const hit = this.findHtmlBlockStart(line);
|
|
272
|
-
if (hit) {
|
|
273
|
-
const before = line.slice(0, hit.index).trim();
|
|
274
|
-
if (before)
|
|
275
|
-
paragraph.push(before);
|
|
276
|
-
flush();
|
|
277
|
-
i = this.emitHtmlBlock(lines, i, hit, out);
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
// --- ATX heading -------------------------------------------------------
|
|
282
|
-
const heading = trimmed.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
|
|
283
|
-
if (heading) {
|
|
284
|
-
flush();
|
|
285
|
-
const level = heading[1].length;
|
|
286
|
-
out.push(`<h${level}>${this.inline(heading[2], opts)}</h${level}>`);
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
// --- Horizontal rule ---------------------------------------------------
|
|
290
|
-
if (/^(?:-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
|
291
|
-
flush();
|
|
292
|
-
out.push('<hr>');
|
|
293
|
-
continue;
|
|
184
|
+
/**
|
|
185
|
+
* Returns today's date in the local timezone.
|
|
186
|
+
*/
|
|
187
|
+
today() {
|
|
188
|
+
return new Date();
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Parses a value into a `Date`.
|
|
192
|
+
* - Strings are first attempted as ISO 8601, then matched against each format in `parseFormat`.
|
|
193
|
+
* - Numbers are treated as Unix timestamps (milliseconds).
|
|
194
|
+
* - Existing `Date` instances are cloned.
|
|
195
|
+
* @param value - The value to parse.
|
|
196
|
+
* @param parseFormat - A format string or an array of format strings (date-fns tokens).
|
|
197
|
+
* @returns A valid `Date` in `Europe/Rome`, an invalid sentinel, or `null` for unrecognised input.
|
|
198
|
+
*/
|
|
199
|
+
parse(value, parseFormat) {
|
|
200
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
201
|
+
const iso8601Date = parseISO(value);
|
|
202
|
+
if (this.isValid(iso8601Date)) {
|
|
203
|
+
return new TZDate(iso8601Date, 'Europe/Rome');
|
|
294
204
|
}
|
|
295
|
-
|
|
296
|
-
if (
|
|
297
|
-
|
|
298
|
-
const quoted = [];
|
|
299
|
-
while (i < lines.length && /^>\s?/.test(lines[i].trim())) {
|
|
300
|
-
quoted.push(lines[i].trim().replace(/^>\s?/, ''));
|
|
301
|
-
i++;
|
|
302
|
-
}
|
|
303
|
-
i--;
|
|
304
|
-
out.push(`<blockquote>${this.parseBlocks(quoted, opts)}</blockquote>`);
|
|
305
|
-
continue;
|
|
205
|
+
const formats = Array.isArray(parseFormat) ? parseFormat : [parseFormat];
|
|
206
|
+
if (!formats.length) {
|
|
207
|
+
throw Error('Formats array must not be empty.');
|
|
306
208
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
/^\|?[\s:\-|]+\|?$/.test(lines[i + 1].trim())) {
|
|
312
|
-
flush();
|
|
313
|
-
const aligns = this.tableAligns(lines[i + 1].trim());
|
|
314
|
-
const head = this.tableRow(trimmed, 'th', aligns, opts);
|
|
315
|
-
const body = [];
|
|
316
|
-
i += 2;
|
|
317
|
-
while (i < lines.length && lines[i].trim().includes('|')) {
|
|
318
|
-
body.push(this.tableRow(lines[i].trim(), 'td', aligns, opts));
|
|
319
|
-
i++;
|
|
209
|
+
for (const currentFormat of formats) {
|
|
210
|
+
const fromFormat = parse(value, currentFormat, new Date(), { locale: this.locale });
|
|
211
|
+
if (this.isValid(fromFormat)) {
|
|
212
|
+
return new TZDate(fromFormat, 'Europe/Rome');
|
|
320
213
|
}
|
|
321
|
-
i--;
|
|
322
|
-
out.push(`<table>\n<thead>\n${head}\n</thead>` +
|
|
323
|
-
(body.length > 0 ? `\n<tbody>\n${body.join('\n')}\n</tbody>` : '') +
|
|
324
|
-
`\n</table>`);
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
// --- List ---------------------------------------------------------------
|
|
328
|
-
if (MarkdownUtils.listItemRe.test(line)) {
|
|
329
|
-
flush();
|
|
330
|
-
const [html, last] = this.parseList(lines, i, opts);
|
|
331
|
-
out.push(html);
|
|
332
|
-
i = last;
|
|
333
|
-
continue;
|
|
334
214
|
}
|
|
335
|
-
|
|
336
|
-
paragraph.push(trimmed);
|
|
215
|
+
return this.invalid();
|
|
337
216
|
}
|
|
338
|
-
|
|
339
|
-
|
|
217
|
+
else if (typeof value === 'number') {
|
|
218
|
+
return new Date(value);
|
|
219
|
+
}
|
|
220
|
+
else if (value instanceof Date) {
|
|
221
|
+
return this.clone(value);
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
340
224
|
}
|
|
341
|
-
// #endregion
|
|
342
|
-
// #region Lists
|
|
343
|
-
/** Matches "- item", "* item", "+ item", "1. item", "1) item" with leading indent. */
|
|
344
|
-
static { this.listItemRe = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/; }
|
|
345
225
|
/**
|
|
346
|
-
*
|
|
347
|
-
*
|
|
226
|
+
* Formats a `Date` using the given date-fns display format string.
|
|
227
|
+
* Throws an `Error` when `date` is not valid.
|
|
228
|
+
* @param date - The date to format.
|
|
229
|
+
* @param displayFormat - A date-fns format string (e.g. `'P'`, `'LLL uuuu'`).
|
|
348
230
|
*/
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
while (i < lines.length) {
|
|
353
|
-
const l = lines[i];
|
|
354
|
-
if (!l.trim())
|
|
355
|
-
break; // blank line ends the list
|
|
356
|
-
if (this.listItemRe.test(l) || /^\s{2,}\S/.test(l)) {
|
|
357
|
-
block.push(l);
|
|
358
|
-
i++;
|
|
359
|
-
}
|
|
360
|
-
else
|
|
361
|
-
break;
|
|
231
|
+
format(date, displayFormat) {
|
|
232
|
+
if (!this.isValid(date)) {
|
|
233
|
+
throw Error('DateFnsAdapter: Cannot format invalid date.');
|
|
362
234
|
}
|
|
363
|
-
return
|
|
235
|
+
return format(date, displayFormat, { locale: this.locale });
|
|
364
236
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const m = l.match(this.listItemRe);
|
|
373
|
-
if (m && m[1].length <= baseIndent) {
|
|
374
|
-
items.push({ text: m[3], childLines: [] });
|
|
375
|
-
}
|
|
376
|
-
else if (items.length > 0) {
|
|
377
|
-
// Continuation or nested content: strip one nesting level of indent
|
|
378
|
-
items[items.length - 1].childLines.push(l.length > baseIndent + 2 ? l.slice(baseIndent + 2) : l.trim());
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
const lis = items.map(item => {
|
|
382
|
-
let inner;
|
|
383
|
-
// GFM task list: - [ ] / - [x]
|
|
384
|
-
const task = item.text.match(/^\[([ xX])\]\s+(.*)$/);
|
|
385
|
-
if (task) {
|
|
386
|
-
const checked = task[1] !== ' ' ? ' checked' : '';
|
|
387
|
-
inner = `<input type="checkbox" disabled${checked}> ${this.inline(task[2], opts)}`;
|
|
388
|
-
}
|
|
389
|
-
else {
|
|
390
|
-
inner = this.inline(item.text, opts);
|
|
391
|
-
}
|
|
392
|
-
if (item.childLines.length > 0) {
|
|
393
|
-
inner += '\n' + this.parseBlocks(item.childLines, opts);
|
|
394
|
-
}
|
|
395
|
-
return `<li>${inner}</li>`;
|
|
396
|
-
});
|
|
397
|
-
const tag = ordered ? 'ol' : 'ul';
|
|
398
|
-
const startAttr = ordered && startNum > 1 ? ` start="${startNum}"` : '';
|
|
399
|
-
return `<${tag}${startAttr}>\n${lis.join('\n')}\n</${tag}>`;
|
|
400
|
-
}
|
|
401
|
-
// #endregion
|
|
402
|
-
// #region Tables
|
|
403
|
-
static tableAligns(separator) {
|
|
404
|
-
return separator
|
|
405
|
-
.replace(/^\||\|$/g, '')
|
|
406
|
-
.split('|')
|
|
407
|
-
.map(cell => {
|
|
408
|
-
const t = cell.trim();
|
|
409
|
-
if (t.startsWith(':') && t.endsWith(':'))
|
|
410
|
-
return 'center';
|
|
411
|
-
if (t.endsWith(':'))
|
|
412
|
-
return 'right';
|
|
413
|
-
if (t.startsWith(':'))
|
|
414
|
-
return 'left';
|
|
415
|
-
return undefined;
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
static tableRow(row, tag, aligns, opts) {
|
|
419
|
-
const cells = row.replace(/^\||\|$/g, '').split('|').map(c => c.trim());
|
|
420
|
-
const cellsHtml = cells
|
|
421
|
-
.map((cell, j) => {
|
|
422
|
-
const align = aligns[j] ? ` align="${aligns[j]}"` : '';
|
|
423
|
-
return `<${tag}${align}>${this.inline(cell, opts)}</${tag}>`;
|
|
424
|
-
})
|
|
425
|
-
.join('');
|
|
426
|
-
return `<tr>${cellsHtml}</tr>`;
|
|
237
|
+
/**
|
|
238
|
+
* Adds the given number of whole years to a date.
|
|
239
|
+
* @param date - The base date.
|
|
240
|
+
* @param years - Number of years to add (can be negative).
|
|
241
|
+
*/
|
|
242
|
+
addCalendarYears(date, years) {
|
|
243
|
+
return addYears(date, years);
|
|
427
244
|
}
|
|
428
|
-
// #endregion
|
|
429
|
-
// #region Inline formatting
|
|
430
245
|
/**
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
246
|
+
* Adds the given number of whole months to a date.
|
|
247
|
+
* @param date - The base date.
|
|
248
|
+
* @param months - Number of months to add (can be negative).
|
|
434
249
|
*/
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const stash = (html) => `\u0000${store.push(html) - 1}\u0000`;
|
|
438
|
-
// 1. Inline code: stash first so later passes can't touch it.
|
|
439
|
-
// Content is ALWAYS escaped so <, >, & inside code survive in the DOM
|
|
440
|
-
// (the browser renders entities back to the original characters).
|
|
441
|
-
let s = text.replace(/`([^`]+)`/g, (_m, code) => stash(`<code>${this.escape(code)}</code>`));
|
|
442
|
-
// 2. Escape raw HTML as entities (opt-in only)
|
|
443
|
-
if (opts.escapeHtml)
|
|
444
|
-
s = this.escape(s);
|
|
445
|
-
// 3. Images: 
|
|
446
|
-
s = s.replace(/!\[([^\]]*)\]\(\s*([^)\s]+)(?:\s+(?:"|")(.+?)(?:"|"))?\s*\)/g, (_m, alt, src, title) => {
|
|
447
|
-
const url = this.safeUrl(src);
|
|
448
|
-
if (!url)
|
|
449
|
-
return alt;
|
|
450
|
-
const t = title ? ` title="${title}"` : '';
|
|
451
|
-
return stash(`<img src="${url}" alt="${alt}"${t} loading="lazy">`);
|
|
452
|
-
});
|
|
453
|
-
// 4. Links: [text](url "title")
|
|
454
|
-
s = s.replace(/\[([^\]]+)\]\(\s*([^)\s]+)(?:\s+(?:"|")(.+?)(?:"|"))?\s*\)/g, (_m, txt, href, title) => {
|
|
455
|
-
const url = this.safeUrl(href);
|
|
456
|
-
if (!url)
|
|
457
|
-
return txt;
|
|
458
|
-
const t = title ? ` title="${title}"` : '';
|
|
459
|
-
return stash(`<a href="${url}"${t} target="_blank" rel="noopener noreferrer">${txt}</a>`);
|
|
460
|
-
});
|
|
461
|
-
// 5. Autolink remaining bare URLs (markdown links are already stashed)
|
|
462
|
-
s = s.replace(/https?:\/\/[^\s<>"']+/g, url => stash(`<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`));
|
|
463
|
-
// 6. Bold / italic / strikethrough.
|
|
464
|
-
// Delimiters must hug the text; underscore emphasis only at word
|
|
465
|
-
// boundaries so identifiers like snake_case are left untouched.
|
|
466
|
-
s = s.replace(/\*\*\*(?=\S)([\s\S]*?\S)\*\*\*/g, '<strong><em>$1</em></strong>');
|
|
467
|
-
s = s.replace(/(?<![\w*])___(?=\S)([\s\S]*?\S)___(?![\w*])/g, '<strong><em>$1</em></strong>');
|
|
468
|
-
s = s.replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, '<strong>$1</strong>');
|
|
469
|
-
s = s.replace(/(?<![\w*])__(?=\S)([\s\S]*?\S)__(?![\w*])/g, '<strong>$1</strong>');
|
|
470
|
-
s = s.replace(/\*(?=\S)([^*\n]*?\S)\*/g, '<em>$1</em>');
|
|
471
|
-
s = s.replace(/(?<![\w_])_(?=\S)([^_\n]*?\S)_(?![\w_])/g, '<em>$1</em>');
|
|
472
|
-
s = s.replace(/~~(?=\S)([\s\S]*?\S)~~/g, '<del>$1</del>');
|
|
473
|
-
// 7. Restore stashed HTML
|
|
474
|
-
return s.replace(/\u0000(\d+)\u0000/g, (_m, idx) => store[Number(idx)] ?? '');
|
|
250
|
+
addCalendarMonths(date, months) {
|
|
251
|
+
return addMonths(date, months);
|
|
475
252
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
.replace(/>/g, '>')
|
|
484
|
-
.replace(/"/g, '"');
|
|
253
|
+
/**
|
|
254
|
+
* Adds the given number of whole days to a date.
|
|
255
|
+
* @param date - The base date.
|
|
256
|
+
* @param days - Number of days to add (can be negative).
|
|
257
|
+
*/
|
|
258
|
+
addCalendarDays(date, days) {
|
|
259
|
+
return addDays(date, days);
|
|
485
260
|
}
|
|
486
261
|
/**
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
* whitespace/control characters, e.g. "java\tscript:").
|
|
262
|
+
* Serialises a date to an ISO 8601 date string (`yyyy-MM-dd`).
|
|
263
|
+
* @param date - The date to serialise.
|
|
490
264
|
*/
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if (/^(javascript|vbscript|data):/i.test(compact))
|
|
494
|
-
return undefined;
|
|
495
|
-
return url.replace(/"/g, '%22');
|
|
265
|
+
toIso8601(date) {
|
|
266
|
+
return formatISO(date, { representation: 'date' });
|
|
496
267
|
}
|
|
497
|
-
// #endregion
|
|
498
|
-
// #region Clipboard
|
|
499
268
|
/**
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* @param
|
|
504
|
-
* @param options : conversion options for the HTML flavor
|
|
505
|
-
* @returns : true on success
|
|
269
|
+
* Returns the given value when it is a valid `Date`, or `null` for an empty string.
|
|
270
|
+
* Deserialises valid ISO 8601 strings into `Date` instances.
|
|
271
|
+
* Delegates all other values to the base-class implementation.
|
|
272
|
+
* @param value - The raw value to deserialise.
|
|
506
273
|
*/
|
|
507
|
-
|
|
508
|
-
if (
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if (typeof ClipboardItem !== 'undefined') {
|
|
512
|
-
const html = this.toHtml(markdown, options);
|
|
513
|
-
await navigator.clipboard.write([
|
|
514
|
-
new ClipboardItem({
|
|
515
|
-
'text/html': new Blob([html], { type: 'text/html' }),
|
|
516
|
-
'text/plain': new Blob([markdown], { type: 'text/plain' }),
|
|
517
|
-
}),
|
|
518
|
-
]);
|
|
274
|
+
deserialize(value) {
|
|
275
|
+
if (typeof value === 'string') {
|
|
276
|
+
if (!value) {
|
|
277
|
+
return null;
|
|
519
278
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
279
|
+
const date = parseISO(value);
|
|
280
|
+
if (this.isValid(date)) {
|
|
281
|
+
return date;
|
|
523
282
|
}
|
|
524
|
-
return true;
|
|
525
|
-
}
|
|
526
|
-
catch {
|
|
527
|
-
// Clipboard API requires a secure context and user activation
|
|
528
|
-
return false;
|
|
529
283
|
}
|
|
284
|
+
return super.deserialize(value);
|
|
530
285
|
}
|
|
531
286
|
/**
|
|
532
|
-
*
|
|
533
|
-
* @param
|
|
534
|
-
* @returns : true on success
|
|
287
|
+
* Returns `true` when `obj` is an instance of `Date`.
|
|
288
|
+
* @param obj - The object to test.
|
|
535
289
|
*/
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
return false;
|
|
539
|
-
try {
|
|
540
|
-
await navigator.clipboard.writeText(text);
|
|
541
|
-
return true;
|
|
542
|
-
}
|
|
543
|
-
catch {
|
|
544
|
-
return false;
|
|
545
|
-
}
|
|
290
|
+
isDateInstance(obj) {
|
|
291
|
+
return isDate(obj);
|
|
546
292
|
}
|
|
547
293
|
/**
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
* @param element : the element hosting the rendered markdown
|
|
551
|
-
* @returns : the plain text
|
|
294
|
+
* Returns `true` when `date` represents a valid point in time.
|
|
295
|
+
* @param date - The date to validate.
|
|
552
296
|
*/
|
|
553
|
-
|
|
554
|
-
return
|
|
297
|
+
isValid(date) {
|
|
298
|
+
return isValid(date);
|
|
555
299
|
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
var DateFormat;
|
|
559
|
-
(function (DateFormat) {
|
|
560
|
-
DateFormat[DateFormat["Short"] = 1] = "Short";
|
|
561
|
-
DateFormat[DateFormat["Long"] = 2] = "Long";
|
|
562
|
-
DateFormat[DateFormat["LongWithShortMonth"] = 3] = "LongWithShortMonth";
|
|
563
|
-
DateFormat[DateFormat["LongWithWeekDay"] = 4] = "LongWithWeekDay";
|
|
564
|
-
DateFormat[DateFormat["LongWithShortWeekDay"] = 5] = "LongWithShortWeekDay";
|
|
565
|
-
DateFormat[DateFormat["MonthAndYear"] = 6] = "MonthAndYear";
|
|
566
|
-
DateFormat[DateFormat["LongMonthAndYear"] = 7] = "LongMonthAndYear";
|
|
567
|
-
DateFormat[DateFormat["WeekDay"] = 8] = "WeekDay";
|
|
568
|
-
DateFormat[DateFormat["LongWeekDay"] = 9] = "LongWeekDay";
|
|
569
|
-
DateFormat[DateFormat["DayAndMonth"] = 10] = "DayAndMonth";
|
|
570
|
-
DateFormat[DateFormat["ShortUS"] = 11] = "ShortUS";
|
|
571
|
-
DateFormat[DateFormat["ShortISO8601"] = 12] = "ShortISO8601"; // yyyy-mm-dd
|
|
572
|
-
})(DateFormat || (DateFormat = {}));
|
|
573
|
-
class SystemUtils {
|
|
574
|
-
/** Shared collator for locale-aware, case-insensitive string comparison. */
|
|
575
|
-
static { this.collator = new Intl.Collator('it', { sensitivity: 'base', numeric: true }); }
|
|
576
300
|
/**
|
|
577
|
-
*
|
|
578
|
-
* @param array : the array to scan
|
|
579
|
-
* @param key : key name
|
|
580
|
-
* @param value : the value to search for
|
|
581
|
-
* @returns : the property value or null
|
|
301
|
+
* Returns a sentinel `Date` that represents an invalid date (`new Date(NaN)`).
|
|
582
302
|
*/
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
return undefined;
|
|
586
|
-
for (const item of array) {
|
|
587
|
-
if (item[key] === value) {
|
|
588
|
-
return item;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
return undefined;
|
|
303
|
+
invalid() {
|
|
304
|
+
return new Date(NaN);
|
|
592
305
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
306
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DateFnsAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
307
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DateFnsAdapter }); }
|
|
308
|
+
}
|
|
309
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DateFnsAdapter, decorators: [{
|
|
310
|
+
type: Injectable
|
|
311
|
+
}], ctorParameters: () => [] });
|
|
312
|
+
/**
|
|
313
|
+
* Standalone providers for the scm-utils date-fns adapter.
|
|
314
|
+
*
|
|
315
|
+
* Configures Angular Material to use {@link DateFnsAdapter} (Europe/Rome timezone)
|
|
316
|
+
* and the matching {@link MAT_DATE_FNS_FORMATS}.
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* bootstrapApplication(AppComponent, {
|
|
320
|
+
* providers: [provideSCMDateFns()]
|
|
321
|
+
* });
|
|
322
|
+
*/
|
|
323
|
+
function provideSCMDateFns() {
|
|
324
|
+
return makeEnvironmentProviders([
|
|
325
|
+
{
|
|
326
|
+
provide: DateAdapter,
|
|
327
|
+
useClass: DateFnsAdapter,
|
|
328
|
+
deps: [MAT_DATE_LOCALE],
|
|
329
|
+
},
|
|
330
|
+
{ provide: MAT_DATE_FORMATS, useValue: MAT_DATE_FNS_FORMATS }
|
|
331
|
+
]);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Directive that moves browser focus to the host element after the first render cycle.
|
|
336
|
+
* Apply `autoFocus` to any focusable element to set focus automatically on initialisation.
|
|
337
|
+
*/
|
|
338
|
+
class AutoFocusDirective {
|
|
339
|
+
constructor() {
|
|
340
|
+
this.elementRef = inject(ElementRef);
|
|
341
|
+
afterNextRender(() => {
|
|
342
|
+
this.elementRef.nativeElement?.focus();
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: AutoFocusDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
346
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: AutoFocusDirective, isStandalone: true, selector: "[autoFocus]", ngImport: i0 }); }
|
|
347
|
+
}
|
|
348
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: AutoFocusDirective, decorators: [{
|
|
349
|
+
type: Directive,
|
|
350
|
+
args: [{
|
|
351
|
+
selector: '[autoFocus]',
|
|
352
|
+
standalone: true
|
|
353
|
+
}]
|
|
354
|
+
}], ctorParameters: () => [] });
|
|
355
|
+
|
|
356
|
+
class FileInfo {
|
|
357
|
+
isValid() {
|
|
358
|
+
return this.valid;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
class ValueModel {
|
|
362
|
+
}
|
|
363
|
+
class IDModel {
|
|
364
|
+
}
|
|
365
|
+
class GroupModel {
|
|
366
|
+
}
|
|
367
|
+
class DeleteModel extends GroupModel {
|
|
368
|
+
}
|
|
369
|
+
class RelationModel {
|
|
370
|
+
}
|
|
371
|
+
class UpdateRelationsModel {
|
|
372
|
+
}
|
|
373
|
+
class QueryModel {
|
|
374
|
+
}
|
|
375
|
+
class ImportModel {
|
|
376
|
+
}
|
|
377
|
+
class DateInterval {
|
|
378
|
+
get fromAsDate() {
|
|
379
|
+
if (this.from) {
|
|
380
|
+
if (!(this.from instanceof Date)) {
|
|
381
|
+
this.from = new Date(this.from);
|
|
382
|
+
}
|
|
383
|
+
if (this.from) {
|
|
384
|
+
return new Date(this.from.getFullYear(), this.from.getMonth(), this.from.getDate(), 2, 0, 0);
|
|
606
385
|
}
|
|
607
386
|
}
|
|
608
|
-
return
|
|
387
|
+
return undefined;
|
|
609
388
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if (!array)
|
|
618
|
-
return undefined;
|
|
619
|
-
for (const item of array) {
|
|
620
|
-
const i = item;
|
|
621
|
-
if (i['value'] === value) {
|
|
622
|
-
return i['name'] ?? i['id'];
|
|
389
|
+
get toAsDate() {
|
|
390
|
+
if (this.to) {
|
|
391
|
+
if (!(this.to instanceof Date)) {
|
|
392
|
+
this.to = new Date(this.to);
|
|
393
|
+
}
|
|
394
|
+
if (this.to) {
|
|
395
|
+
return new Date(this.to.getFullYear(), this.to.getMonth(), this.to.getDate(), 2, 0, 0);
|
|
623
396
|
}
|
|
624
397
|
}
|
|
625
398
|
return undefined;
|
|
626
399
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
* @param parent : parent node
|
|
631
|
-
* @returns : an array of INode objects
|
|
632
|
-
*/
|
|
633
|
-
static arrayToNodes(items, parent) {
|
|
634
|
-
const nodes = [];
|
|
635
|
-
items.forEach(n => {
|
|
636
|
-
const node = {
|
|
637
|
-
id: n.id.toString(),
|
|
638
|
-
name: n.name,
|
|
639
|
-
count: n.count ? n.count : 0,
|
|
640
|
-
children: undefined,
|
|
641
|
-
parent,
|
|
642
|
-
bag: n,
|
|
643
|
-
};
|
|
644
|
-
nodes.push(node);
|
|
645
|
-
node.children =
|
|
646
|
-
n.children && n.children.length > 0
|
|
647
|
-
? this.arrayToNodes(n.children, node)
|
|
648
|
-
: [];
|
|
649
|
-
});
|
|
650
|
-
return nodes;
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* Comparator factory for sorting arrays of objects by a given property key.
|
|
654
|
-
* @param key - Name of the property to sort by.
|
|
655
|
-
* @param order - Sort direction: `'asc'` (default) or `'desc'`.
|
|
656
|
-
* @returns A comparator function that returns a negative, zero, or positive number.
|
|
657
|
-
*/
|
|
658
|
-
static arraySortCompare(key, order = 'asc') {
|
|
659
|
-
const dir = order === 'desc' ? -1 : 1;
|
|
660
|
-
return (a, b) => {
|
|
661
|
-
const varA = a[key];
|
|
662
|
-
const varB = b[key];
|
|
663
|
-
if (varA === varB)
|
|
664
|
-
return 0;
|
|
665
|
-
if (varA === undefined || varA === null)
|
|
666
|
-
return -1 * dir;
|
|
667
|
-
if (varB === undefined || varB === null)
|
|
668
|
-
return 1 * dir;
|
|
669
|
-
if (typeof varA === 'string' && typeof varB === 'string') {
|
|
670
|
-
return SystemUtils.collator.compare(varA, varB) * dir;
|
|
671
|
-
}
|
|
672
|
-
return (varA > varB ? 1 : varA < varB ? -1 : 0) * dir;
|
|
673
|
-
};
|
|
400
|
+
constructor(from, to) {
|
|
401
|
+
this.from = from;
|
|
402
|
+
this.to = to;
|
|
674
403
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
* @returns : the formatted string
|
|
679
|
-
*/
|
|
680
|
-
static formatWeight(gr) {
|
|
681
|
-
if (gr > 1000000)
|
|
682
|
-
return `${(gr / 1000000).toFixed(2)} t`;
|
|
683
|
-
else if (gr > 100000)
|
|
684
|
-
return `${(gr / 100000).toFixed(2)} q`;
|
|
685
|
-
else if (gr > 1000)
|
|
686
|
-
return `${(gr / 1000).toFixed(2)} kg`;
|
|
687
|
-
else
|
|
688
|
-
return `${gr} gr`;
|
|
404
|
+
clear() {
|
|
405
|
+
this.from = undefined;
|
|
406
|
+
this.to = undefined;
|
|
689
407
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Zero-dependency Markdown to HTML converter.
|
|
412
|
+
*
|
|
413
|
+
* Design goals:
|
|
414
|
+
* - Single line-based pass over blocks (no repeated full-string regex passes).
|
|
415
|
+
* - Inline formatting never touches code spans / code blocks (stash & restore).
|
|
416
|
+
* - No HTML entity escaping by default in normal text: the source passes as-is.
|
|
417
|
+
* Exception: the content of code spans / code blocks is ALWAYS escaped, so
|
|
418
|
+
* things like List<string> render correctly (the browser displays the
|
|
419
|
+
* original characters; entities never reach the user or the clipboard).
|
|
420
|
+
* Opt-in full escaping via { escapeHtml: true } for untrusted input.
|
|
421
|
+
* - URL sanitization on links/images (javascript:, vbscript:, data: are dropped).
|
|
422
|
+
* - Bounded result cache for repeated renders (Angular change detection friendly).
|
|
423
|
+
*
|
|
424
|
+
* Supported syntax: headings, paragraphs, hard/soft breaks, hr, blockquotes
|
|
425
|
+
* (nested), fenced code blocks, inline code, bold/italic/strikethrough, links
|
|
426
|
+
* (with title), images, autolinked bare URLs, ordered/unordered lists (nested,
|
|
427
|
+
* task lists, ordered start offset), GFM tables with alignment, raw HTML
|
|
428
|
+
* blocks (e.g. <table>...</table>) passed through verbatim when escapeHtml
|
|
429
|
+
* is false: their newlines stay plain newlines, never converted to <br>.
|
|
430
|
+
*
|
|
431
|
+
* Known simplifications (documented, by design):
|
|
432
|
+
* - A blank line terminates a list.
|
|
433
|
+
* - Setext headings (=== / ---) are not supported, use # syntax.
|
|
434
|
+
* - Reference-style links [text][ref] are not supported.
|
|
435
|
+
*/
|
|
436
|
+
class MarkdownUtils {
|
|
437
|
+
// #region Cache
|
|
438
|
+
static { this.cache = new Map(); }
|
|
439
|
+
static { this.CACHE_MAX = 200; }
|
|
440
|
+
/** Clears the internal result cache. */
|
|
441
|
+
static clearCache() {
|
|
442
|
+
this.cache.clear();
|
|
701
443
|
}
|
|
444
|
+
// #endregion
|
|
445
|
+
// #region Public API
|
|
702
446
|
/**
|
|
703
|
-
*
|
|
704
|
-
* @param
|
|
705
|
-
* @param
|
|
706
|
-
* @returns
|
|
447
|
+
* Convert markdown to HTML.
|
|
448
|
+
* @param markdown : the markdown source
|
|
449
|
+
* @param options : conversion options
|
|
450
|
+
* @returns : the HTML string ('' when input is falsy)
|
|
707
451
|
*/
|
|
708
|
-
static
|
|
709
|
-
|
|
452
|
+
static toHtml(markdown, options) {
|
|
453
|
+
if (!markdown)
|
|
454
|
+
return '';
|
|
455
|
+
const opts = { escapeHtml: false, breaks: true, ...options };
|
|
456
|
+
const key = `${opts.escapeHtml ? 1 : 0}${opts.breaks ? 1 : 0}|${markdown}`;
|
|
457
|
+
const hit = this.cache.get(key);
|
|
458
|
+
if (hit !== undefined)
|
|
459
|
+
return hit;
|
|
460
|
+
// Normalize line endings and fix glued sentence punctuation ("dettaglio!Ecco").
|
|
461
|
+
// Only when the next char is uppercase (a new sentence), so URLs with query
|
|
462
|
+
// strings ("/page?id=1") and code identifiers are left untouched.
|
|
463
|
+
const source = markdown
|
|
464
|
+
.replace(/\r\n?/g, '\n')
|
|
465
|
+
.replace(/([a-zà-ÿ][!?])([A-ZÀ-Þ])/g, '$1 $2');
|
|
466
|
+
const html = this.parseBlocks(source.split('\n'), opts).trim();
|
|
467
|
+
if (this.cache.size >= this.CACHE_MAX) {
|
|
468
|
+
this.cache.delete(this.cache.keys().next().value); // FIFO eviction
|
|
469
|
+
}
|
|
470
|
+
this.cache.set(key, html);
|
|
471
|
+
return html;
|
|
710
472
|
}
|
|
473
|
+
// #endregion
|
|
474
|
+
// #region Block parser (single pass)
|
|
475
|
+
/** Block-level HTML tags that start a raw HTML block (passthrough, no <p>/<br>). */
|
|
476
|
+
static { this.htmlBlockTags = new Set([
|
|
477
|
+
'address', 'article', 'aside', 'blockquote', 'caption', 'colgroup', 'col',
|
|
478
|
+
'dd', 'details', 'dialog', 'div', 'dl', 'dt', 'fieldset', 'figcaption',
|
|
479
|
+
'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header',
|
|
480
|
+
'hr', 'iframe', 'li', 'main', 'nav', 'ol', 'p', 'pre', 'section', 'summary',
|
|
481
|
+
'table', 'tbody', 'td', 'tfoot', 'thead', 'th', 'tr', 'ul',
|
|
482
|
+
]); }
|
|
483
|
+
/** Void/self-contained tags: a single tag, no closing tag expected. */
|
|
484
|
+
static { this.htmlVoidTags = new Set(['hr', 'col', 'img', 'input']); }
|
|
711
485
|
/**
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
486
|
+
* Precompiled: first block-level tag occurrence anywhere in a line.
|
|
487
|
+
* Anchored on the '<' literal, so scanning is cheap; the lookahead
|
|
488
|
+
* prevents partial matches ('<td' must not match inside '<tdx').
|
|
715
489
|
*/
|
|
716
|
-
static
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
let
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
490
|
+
static { this.htmlBlockScanRe = new RegExp(`<(\\/?)(${[...MarkdownUtils.htmlBlockTags].join('|')})(?=[\\s/>])`, 'gi'); }
|
|
491
|
+
/** Result of scanning a line for the start of a raw HTML block. */
|
|
492
|
+
static findHtmlBlockStart(line) {
|
|
493
|
+
// HTML comment candidate (skipping occurrences inside inline code spans)
|
|
494
|
+
let comment = line.indexOf('<!--');
|
|
495
|
+
while (comment !== -1 && this.insideCodeSpan(line, comment)) {
|
|
496
|
+
comment = line.indexOf('<!--', comment + 4);
|
|
497
|
+
}
|
|
498
|
+
// Block tag candidate (same code-span guard)
|
|
499
|
+
const re = this.htmlBlockScanRe;
|
|
500
|
+
re.lastIndex = 0;
|
|
501
|
+
let m;
|
|
502
|
+
let tagHit;
|
|
503
|
+
while ((m = re.exec(line)) !== null) {
|
|
504
|
+
if (!this.insideCodeSpan(line, m.index)) {
|
|
505
|
+
tagHit = { index: m.index, tag: m[2].toLowerCase(), isClose: m[1] === '/' };
|
|
506
|
+
break;
|
|
732
507
|
}
|
|
733
508
|
}
|
|
734
|
-
|
|
509
|
+
if (comment !== -1 && (!tagHit || comment < tagHit.index)) {
|
|
510
|
+
return { index: comment, tag: '', isClose: false, isComment: true };
|
|
511
|
+
}
|
|
512
|
+
return tagHit ? { ...tagHit, isComment: false } : undefined;
|
|
735
513
|
}
|
|
736
|
-
/**
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
return undefined;
|
|
745
|
-
if (s.length < max)
|
|
746
|
-
return s;
|
|
747
|
-
const i = s.lastIndexOf(' ', max - 1);
|
|
748
|
-
return i > 0 ? s.slice(0, i) : s;
|
|
514
|
+
/** True when `index` falls inside an inline code span (odd backtick count before it). */
|
|
515
|
+
static insideCodeSpan(line, index) {
|
|
516
|
+
let count = 0;
|
|
517
|
+
for (let i = 0; i < index; i++) {
|
|
518
|
+
if (line.charCodeAt(i) === 96 /* ` */)
|
|
519
|
+
count++;
|
|
520
|
+
}
|
|
521
|
+
return (count & 1) === 1;
|
|
749
522
|
}
|
|
750
523
|
/**
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
* @
|
|
755
|
-
*
|
|
524
|
+
* indexOf-based depth scanner (no regex, no allocations): walks `text`
|
|
525
|
+
* adjusting `depth` for <tag ...> / </tag> occurrences of the SAME tag.
|
|
526
|
+
* Self-closing <tag ... /> forms do not alter depth.
|
|
527
|
+
* @returns [newDepth, endIndex] where endIndex is the position right after
|
|
528
|
+
* the '>' that balanced the element, or -1 when still open.
|
|
756
529
|
*/
|
|
757
|
-
static
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
530
|
+
static scanHtmlDepth(text, tag, depth) {
|
|
531
|
+
const lower = text.toLowerCase();
|
|
532
|
+
const open = '<' + tag;
|
|
533
|
+
const close = '</' + tag;
|
|
534
|
+
let i = 0;
|
|
535
|
+
while ((i = lower.indexOf('<', i)) !== -1) {
|
|
536
|
+
if (lower.startsWith(close, i) && this.isTagBoundary(lower, i + close.length)) {
|
|
537
|
+
depth--;
|
|
538
|
+
const gt = lower.indexOf('>', i);
|
|
539
|
+
const end = gt === -1 ? lower.length : gt + 1;
|
|
540
|
+
if (depth <= 0)
|
|
541
|
+
return [0, end];
|
|
542
|
+
i = end;
|
|
543
|
+
}
|
|
544
|
+
else if (lower.startsWith(open, i) && this.isTagBoundary(lower, i + open.length)) {
|
|
545
|
+
const gt = lower.indexOf('>', i);
|
|
546
|
+
const selfClosing = gt !== -1 && lower.charCodeAt(gt - 1) === 47 /* / */;
|
|
547
|
+
if (selfClosing) {
|
|
548
|
+
if (depth === 0)
|
|
549
|
+
return [0, gt + 1]; // standalone self-closed element
|
|
766
550
|
}
|
|
767
|
-
|
|
551
|
+
else {
|
|
552
|
+
depth++;
|
|
553
|
+
}
|
|
554
|
+
i = gt === -1 ? lower.length : gt + 1;
|
|
768
555
|
}
|
|
769
|
-
|
|
770
|
-
|
|
556
|
+
else {
|
|
557
|
+
i++;
|
|
771
558
|
}
|
|
772
|
-
return s;
|
|
773
559
|
}
|
|
774
|
-
return
|
|
775
|
-
}
|
|
776
|
-
/**
|
|
777
|
-
* Normalize a string by converting it to lowercase and removing extra spaces, with special handling for acronyms and camelCase.
|
|
778
|
-
* @param s : the string to normalize
|
|
779
|
-
* @returns : The normalized string, or `undefined` if the input is falsy.
|
|
780
|
-
*/
|
|
781
|
-
static normalizeDisplay(s) {
|
|
782
|
-
if (!s)
|
|
783
|
-
return s;
|
|
784
|
-
return s
|
|
785
|
-
.split(' ')
|
|
786
|
-
.map((word, wordIndex) => {
|
|
787
|
-
if (!word)
|
|
788
|
-
return word;
|
|
789
|
-
// If the word is all uppercase and contains at least one letter, we assume it's an acronym and leave it as is
|
|
790
|
-
if (word === word.toUpperCase() && /[A-Z]/.test(word)) {
|
|
791
|
-
return word;
|
|
792
|
-
}
|
|
793
|
-
// Otherwise, convert to lowercase letter by letter
|
|
794
|
-
const chars = word.split('');
|
|
795
|
-
return chars
|
|
796
|
-
.map((char, charIndex) => {
|
|
797
|
-
if (charIndex === 0 && wordIndex === 0) {
|
|
798
|
-
// First letter of the first word:
|
|
799
|
-
// remains uppercase only if the first 2 letters are both uppercase
|
|
800
|
-
const secondChar = chars[1] ?? '';
|
|
801
|
-
const keepUppercase = char === char.toUpperCase() &&
|
|
802
|
-
secondChar === secondChar.toUpperCase() &&
|
|
803
|
-
/[A-Z]/.test(char) &&
|
|
804
|
-
/[A-Z]/.test(secondChar);
|
|
805
|
-
return keepUppercase ? char : char.toLowerCase();
|
|
806
|
-
}
|
|
807
|
-
return char.toLowerCase();
|
|
808
|
-
})
|
|
809
|
-
.join('');
|
|
810
|
-
})
|
|
811
|
-
.join(' ');
|
|
560
|
+
return [depth, -1];
|
|
812
561
|
}
|
|
813
|
-
/**
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
if (!s)
|
|
820
|
-
return '';
|
|
821
|
-
return s.replace(/https?:\/\/[^\s<]+/gi, v => `<a href="${v}" target="_blank" rel="noopener">${v}</a>`);
|
|
562
|
+
/** A tag token must be followed by whitespace, '/', '>' or end of line. */
|
|
563
|
+
static isTagBoundary(s, idx) {
|
|
564
|
+
if (idx >= s.length)
|
|
565
|
+
return true; // tag opening continues on the next line
|
|
566
|
+
const c = s.charCodeAt(idx);
|
|
567
|
+
return c === 62 /* > */ || c === 47 /* / */ || c === 32 /* space */ || c === 9 /* tab */;
|
|
822
568
|
}
|
|
823
569
|
/**
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
* @param escapeHtml : true to escape HTML. Default is false
|
|
827
|
-
* @returns the html
|
|
570
|
+
* Re-injects the remainder of a partially consumed line so the main loop
|
|
571
|
+
* processes it as markdown. Returns the index the loop should resume from.
|
|
828
572
|
*/
|
|
829
|
-
static
|
|
830
|
-
|
|
573
|
+
static pushBack(lines, i, remainder) {
|
|
574
|
+
if (remainder.trim()) {
|
|
575
|
+
lines[i] = remainder;
|
|
576
|
+
return i - 1; // the loop's i++ re-processes the remainder
|
|
577
|
+
}
|
|
578
|
+
return i;
|
|
831
579
|
}
|
|
832
580
|
/**
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
* @returns
|
|
581
|
+
* Emits a raw HTML block starting at `hit.index` of lines[i], consuming
|
|
582
|
+
* following lines until the element is balanced. Text after the block on
|
|
583
|
+
* the closing line is pushed back for markdown processing.
|
|
584
|
+
* @returns the index of the last consumed line
|
|
837
585
|
*/
|
|
838
|
-
static
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
if (
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
for (const s2 of p2) {
|
|
854
|
-
if (this.compareString(s1, s2) === 0) {
|
|
855
|
-
matches--;
|
|
856
|
-
break;
|
|
586
|
+
static emitHtmlBlock(lines, i, hit, out) {
|
|
587
|
+
const line = lines[i];
|
|
588
|
+
// HTML comment: raw until '-->'
|
|
589
|
+
if (hit.isComment) {
|
|
590
|
+
let end = line.indexOf('-->', hit.index);
|
|
591
|
+
if (end !== -1) {
|
|
592
|
+
out.push(line.slice(hit.index, end + 3));
|
|
593
|
+
return this.pushBack(lines, i, line.slice(end + 3));
|
|
594
|
+
}
|
|
595
|
+
out.push(line.slice(hit.index));
|
|
596
|
+
while (++i < lines.length) {
|
|
597
|
+
end = lines[i].indexOf('-->');
|
|
598
|
+
if (end !== -1) {
|
|
599
|
+
out.push(lines[i].slice(0, end + 3));
|
|
600
|
+
return this.pushBack(lines, i, lines[i].slice(end + 3));
|
|
857
601
|
}
|
|
602
|
+
out.push(lines[i]);
|
|
858
603
|
}
|
|
604
|
+
return lines.length - 1; // unterminated: consumed to EOF
|
|
859
605
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
* @param reverse : true to decode, false to encode
|
|
867
|
-
* @returns :the ciphered text
|
|
868
|
-
*/
|
|
869
|
-
static cipher(text, key, reverse = false) {
|
|
870
|
-
if (!text || !key)
|
|
871
|
-
return undefined;
|
|
872
|
-
// Surrogate pair limit
|
|
873
|
-
const bound = 0x10000;
|
|
874
|
-
const keyLen = key.length;
|
|
875
|
-
let result = '';
|
|
876
|
-
for (let i = 0; i < text.length; i++) {
|
|
877
|
-
let rotation = key.charCodeAt(i % keyLen);
|
|
878
|
-
if (reverse)
|
|
879
|
-
rotation = -rotation;
|
|
880
|
-
result += String.fromCharCode((text.charCodeAt(i) + rotation + bound) % bound);
|
|
606
|
+
// Stray closing tag or void tag: emit just the tag, resume after '>'
|
|
607
|
+
if (hit.isClose || this.htmlVoidTags.has(hit.tag)) {
|
|
608
|
+
const gt = line.indexOf('>', hit.index);
|
|
609
|
+
const end = gt === -1 ? line.length : gt + 1;
|
|
610
|
+
out.push(line.slice(hit.index, end));
|
|
611
|
+
return this.pushBack(lines, i, line.slice(end));
|
|
881
612
|
}
|
|
882
|
-
|
|
613
|
+
// Opening block tag: raw until the element is balanced (depth === 0)
|
|
614
|
+
let [depth, split] = this.scanHtmlDepth(line.slice(hit.index), hit.tag, 0);
|
|
615
|
+
if (split !== -1) {
|
|
616
|
+
out.push(line.slice(hit.index, hit.index + split));
|
|
617
|
+
return this.pushBack(lines, i, line.slice(hit.index + split));
|
|
618
|
+
}
|
|
619
|
+
out.push(line.slice(hit.index));
|
|
620
|
+
while (depth > 0 && ++i < lines.length) {
|
|
621
|
+
[depth, split] = this.scanHtmlDepth(lines[i], hit.tag, depth);
|
|
622
|
+
if (split !== -1) {
|
|
623
|
+
out.push(lines[i].slice(0, split));
|
|
624
|
+
return this.pushBack(lines, i, lines[i].slice(split));
|
|
625
|
+
}
|
|
626
|
+
out.push(lines[i]);
|
|
627
|
+
}
|
|
628
|
+
return Math.min(i, lines.length - 1);
|
|
883
629
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
630
|
+
static parseBlocks(lines, opts) {
|
|
631
|
+
const out = [];
|
|
632
|
+
let paragraph = [];
|
|
633
|
+
const flush = () => {
|
|
634
|
+
if (paragraph.length > 0) {
|
|
635
|
+
const sep = opts.breaks ? '<br>' : ' ';
|
|
636
|
+
out.push(`<p>${paragraph.map(l => this.inline(l, opts)).join(sep)}</p>`);
|
|
637
|
+
paragraph = [];
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
for (let i = 0; i < lines.length; i++) {
|
|
641
|
+
const line = lines[i];
|
|
642
|
+
const trimmed = line.trim();
|
|
643
|
+
// --- Fenced code block -------------------------------------------------
|
|
644
|
+
const fence = trimmed.match(/^```([\w+-]+)?\s*$/);
|
|
645
|
+
if (fence) {
|
|
646
|
+
flush();
|
|
647
|
+
const lang = fence[1] ? ` class="language-${fence[1]}"` : '';
|
|
648
|
+
const code = [];
|
|
649
|
+
i++;
|
|
650
|
+
while (i < lines.length && !/^```\s*$/.test(lines[i].trim())) {
|
|
651
|
+
code.push(lines[i]);
|
|
652
|
+
i++;
|
|
653
|
+
}
|
|
654
|
+
out.push(`<pre><code${lang}>${this.escape(code.join('\n'))}</code></pre>`);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
// --- Blank line --------------------------------------------------------
|
|
658
|
+
if (!trimmed) {
|
|
659
|
+
flush();
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
// --- Raw HTML block passthrough (only with escaping disabled) ------------
|
|
663
|
+
// A block-level HTML element found ANYWHERE in the line starts a raw
|
|
664
|
+
// block: text before it joins the current paragraph, the element is
|
|
665
|
+
// emitted as-is (plain newlines, no <p>, no <br>) until balanced, and
|
|
666
|
+
// any text after the closing tag resumes markdown processing.
|
|
667
|
+
if (!opts.escapeHtml && line.includes('<')) {
|
|
668
|
+
const hit = this.findHtmlBlockStart(line);
|
|
669
|
+
if (hit) {
|
|
670
|
+
const before = line.slice(0, hit.index).trim();
|
|
671
|
+
if (before)
|
|
672
|
+
paragraph.push(before);
|
|
673
|
+
flush();
|
|
674
|
+
i = this.emitHtmlBlock(lines, i, hit, out);
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// --- ATX heading -------------------------------------------------------
|
|
679
|
+
const heading = trimmed.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
|
|
680
|
+
if (heading) {
|
|
681
|
+
flush();
|
|
682
|
+
const level = heading[1].length;
|
|
683
|
+
out.push(`<h${level}>${this.inline(heading[2], opts)}</h${level}>`);
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
// --- Horizontal rule ---------------------------------------------------
|
|
687
|
+
if (/^(?:-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
|
688
|
+
flush();
|
|
689
|
+
out.push('<hr>');
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
// --- Blockquote (consecutive lines, one level stripped, recursive) ------
|
|
693
|
+
if (/^>\s?/.test(trimmed)) {
|
|
694
|
+
flush();
|
|
695
|
+
const quoted = [];
|
|
696
|
+
while (i < lines.length && /^>\s?/.test(lines[i].trim())) {
|
|
697
|
+
quoted.push(lines[i].trim().replace(/^>\s?/, ''));
|
|
698
|
+
i++;
|
|
699
|
+
}
|
|
700
|
+
i--;
|
|
701
|
+
out.push(`<blockquote>${this.parseBlocks(quoted, opts)}</blockquote>`);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
// --- Table (header row + separator row) ---------------------------------
|
|
705
|
+
if (trimmed.includes('|') &&
|
|
706
|
+
i + 1 < lines.length &&
|
|
707
|
+
lines[i + 1].includes('-') &&
|
|
708
|
+
/^\|?[\s:\-|]+\|?$/.test(lines[i + 1].trim())) {
|
|
709
|
+
flush();
|
|
710
|
+
const aligns = this.tableAligns(lines[i + 1].trim());
|
|
711
|
+
const head = this.tableRow(trimmed, 'th', aligns, opts);
|
|
712
|
+
const body = [];
|
|
713
|
+
i += 2;
|
|
714
|
+
while (i < lines.length && lines[i].trim().includes('|')) {
|
|
715
|
+
body.push(this.tableRow(lines[i].trim(), 'td', aligns, opts));
|
|
716
|
+
i++;
|
|
717
|
+
}
|
|
718
|
+
i--;
|
|
719
|
+
out.push(`<table>\n<thead>\n${head}\n</thead>` +
|
|
720
|
+
(body.length > 0 ? `\n<tbody>\n${body.join('\n')}\n</tbody>` : '') +
|
|
721
|
+
`\n</table>`);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
// --- List ---------------------------------------------------------------
|
|
725
|
+
if (MarkdownUtils.listItemRe.test(line)) {
|
|
726
|
+
flush();
|
|
727
|
+
const [html, last] = this.parseList(lines, i, opts);
|
|
728
|
+
out.push(html);
|
|
729
|
+
i = last;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
// --- Plain text: accumulate into current paragraph ----------------------
|
|
733
|
+
paragraph.push(trimmed);
|
|
938
734
|
}
|
|
939
|
-
|
|
940
|
-
return '
|
|
941
|
-
const r = (Math.random() * 16) | 0;
|
|
942
|
-
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
943
|
-
return v.toString(16);
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
/**
|
|
947
|
-
* Reconstruct a standard UUID (with dashes) from a 32-char hex string without dashes.
|
|
948
|
-
* @param value : 32-character hex string
|
|
949
|
-
* @returns : the formatted UUID or the original string if it doesn't match the expected format
|
|
950
|
-
*/
|
|
951
|
-
static inflateUUID(value) {
|
|
952
|
-
const s = value.trim();
|
|
953
|
-
if (s.length !== 32)
|
|
954
|
-
return s;
|
|
955
|
-
return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20)}`;
|
|
956
|
-
}
|
|
957
|
-
/** Precompiled validation patterns. */
|
|
958
|
-
static { this.emailRegex = /^(?:[a-z0-9+!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i; }
|
|
959
|
-
static { this.urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)\/?$/i; }
|
|
960
|
-
/**
|
|
961
|
-
* Parse a text and return true if it is a valid email (or empty)
|
|
962
|
-
* @param value : email
|
|
963
|
-
* @returns : true if the email is valid or empty
|
|
964
|
-
*/
|
|
965
|
-
static parseEmail(value) {
|
|
966
|
-
if (!value)
|
|
967
|
-
return true;
|
|
968
|
-
return this.emailRegex.test(value.trim().toLowerCase());
|
|
969
|
-
}
|
|
970
|
-
/**
|
|
971
|
-
* Parse a text containing one or more email addresses separated by `;` or `,`
|
|
972
|
-
* and return true if all of them are valid (or the text is empty).
|
|
973
|
-
* Empty items caused by double or trailing separators are ignored.
|
|
974
|
-
* @param value : the email list to parse (e.g. "a@b.it; c@d.it")
|
|
975
|
-
* @returns : true if all emails are valid or the value is empty
|
|
976
|
-
*/
|
|
977
|
-
static parseEmails(value) {
|
|
978
|
-
if (!value || value.trim().length === 0)
|
|
979
|
-
return true;
|
|
980
|
-
const items = value
|
|
981
|
-
.split(/[;,]/)
|
|
982
|
-
.map(e => e.trim().toLowerCase())
|
|
983
|
-
.filter(e => e.length > 0);
|
|
984
|
-
if (items.length === 0)
|
|
985
|
-
return false; // only separators, no addresses
|
|
986
|
-
return items.every(e => this.emailRegex.test(e));
|
|
987
|
-
}
|
|
988
|
-
/**
|
|
989
|
-
* Parse a text and return true if it is a valid url (or empty)
|
|
990
|
-
* @param value : the url to parse
|
|
991
|
-
* @returns : true if the url is valid or empty
|
|
992
|
-
*/
|
|
993
|
-
static parseUrl(value) {
|
|
994
|
-
if (!value)
|
|
995
|
-
return true;
|
|
996
|
-
return this.urlRegex.test(value.trim().toLowerCase());
|
|
735
|
+
flush();
|
|
736
|
+
return out.join('\n');
|
|
997
737
|
}
|
|
738
|
+
// #endregion
|
|
739
|
+
// #region Lists
|
|
740
|
+
/** Matches "- item", "* item", "+ item", "1. item", "1) item" with leading indent. */
|
|
741
|
+
static { this.listItemRe = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/; }
|
|
998
742
|
/**
|
|
999
|
-
*
|
|
1000
|
-
*
|
|
1001
|
-
* @returns : an array of numbers with year, month, day
|
|
743
|
+
* Gathers the contiguous list block starting at `start`, builds it (with
|
|
744
|
+
* nesting) and returns [html, indexOfLastConsumedLine].
|
|
1002
745
|
*/
|
|
1003
|
-
static
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
return undefined;
|
|
1017
|
-
parts = [parseInt(p[2]), parseInt(p[1]) - 1, parseInt(p[0])];
|
|
746
|
+
static parseList(lines, start, opts) {
|
|
747
|
+
const block = [];
|
|
748
|
+
let i = start;
|
|
749
|
+
while (i < lines.length) {
|
|
750
|
+
const l = lines[i];
|
|
751
|
+
if (!l.trim())
|
|
752
|
+
break; // blank line ends the list
|
|
753
|
+
if (this.listItemRe.test(l) || /^\s{2,}\S/.test(l)) {
|
|
754
|
+
block.push(l);
|
|
755
|
+
i++;
|
|
756
|
+
}
|
|
757
|
+
else
|
|
758
|
+
break;
|
|
1018
759
|
}
|
|
1019
|
-
return
|
|
1020
|
-
}
|
|
1021
|
-
/**
|
|
1022
|
-
* Parse a date
|
|
1023
|
-
* @param value : the value to check
|
|
1024
|
-
* @param locale : the locale to use
|
|
1025
|
-
* @returns : a valid Date object or null
|
|
1026
|
-
*/
|
|
1027
|
-
static parseDate(value, locale = it) {
|
|
1028
|
-
// No value at all
|
|
1029
|
-
if (!value)
|
|
1030
|
-
return undefined;
|
|
1031
|
-
// A Date value
|
|
1032
|
-
if (value instanceof Date && value.getTime())
|
|
1033
|
-
return value;
|
|
1034
|
-
// Parse known formats using date-fns
|
|
1035
|
-
let d = parseISO(value);
|
|
1036
|
-
if (d && d.getTime() && d.getFullYear() > 1750)
|
|
1037
|
-
return d;
|
|
1038
|
-
d = parse(value, 'dd/MM/yyyy', new Date(), { locale: locale });
|
|
1039
|
-
if (d && d.getTime() && d.getFullYear() > 1750)
|
|
1040
|
-
return d;
|
|
1041
|
-
d = parse(value, 'yyyy-MM-dd', new Date(), { locale: locale });
|
|
1042
|
-
if (d && d.getTime() && d.getFullYear() > 1750)
|
|
1043
|
-
return d;
|
|
1044
|
-
// Parse values manually
|
|
1045
|
-
const parts = this.getDateParts(value);
|
|
1046
|
-
if (!parts)
|
|
1047
|
-
return undefined;
|
|
1048
|
-
if (parts[0] < 100)
|
|
1049
|
-
parts[0] += 2000;
|
|
1050
|
-
if (isNaN(parts[0]) || parts[0] < 0 || parts[0] < 1750)
|
|
1051
|
-
return undefined;
|
|
1052
|
-
if (isNaN(parts[1]) || parts[1] < 0)
|
|
1053
|
-
return undefined;
|
|
1054
|
-
if (isNaN(parts[2]) || parts[2] < 0)
|
|
1055
|
-
return undefined;
|
|
1056
|
-
if (parts[1] > 11)
|
|
1057
|
-
return undefined;
|
|
1058
|
-
if (parts[1] === 1 && parts[2] > 29)
|
|
1059
|
-
return undefined;
|
|
1060
|
-
else if (parts[1] !== 1 && parts[2] > 31)
|
|
1061
|
-
return undefined;
|
|
1062
|
-
return new TZDate(parts[0], parts[1], parts[2], 12);
|
|
760
|
+
return [this.buildList(block, opts), i - 1];
|
|
1063
761
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
if (!(value instanceof Date && value.getTime()))
|
|
1080
|
-
return '';
|
|
1081
|
-
// Format
|
|
1082
|
-
switch (fmt) {
|
|
1083
|
-
case DateFormat.Short: return format(value, "dd/MM/yyyy", { locale: locale });
|
|
1084
|
-
case DateFormat.Long: return format(value, "d MMMM yyyy", { locale: locale });
|
|
1085
|
-
case DateFormat.LongWithShortMonth: return format(value, "d MMM yyyy", { locale: locale });
|
|
1086
|
-
case DateFormat.LongWithWeekDay: return format(value, "EEEE, d MMMM yyyy", { locale: locale });
|
|
1087
|
-
case DateFormat.LongWithShortWeekDay: return format(value, "EEE, d MMMM yyyy", { locale: locale });
|
|
1088
|
-
case DateFormat.MonthAndYear: return format(value, "MMM yyyy", { locale: locale });
|
|
1089
|
-
case DateFormat.LongMonthAndYear: return format(value, "MMMM yyyy", { locale: locale });
|
|
1090
|
-
case DateFormat.WeekDay: return format(value, "EEE, d", { locale: locale });
|
|
1091
|
-
case DateFormat.LongWeekDay: return format(value, "EEEE, d", { locale: locale });
|
|
1092
|
-
case DateFormat.DayAndMonth: return format(value, "d MMMM", { locale: locale });
|
|
1093
|
-
case DateFormat.ShortUS: return format(value, "MM/dd/yyyy", { locale: locale });
|
|
1094
|
-
case DateFormat.ShortISO8601: return format(value, "yyyy-MM-dd", { locale: locale });
|
|
1095
|
-
default:
|
|
1096
|
-
return format(value, fmt, { locale: locale });
|
|
762
|
+
static buildList(block, opts) {
|
|
763
|
+
const first = block[0].match(this.listItemRe);
|
|
764
|
+
const baseIndent = first[1].length;
|
|
765
|
+
const ordered = /^\d/.test(first[2]);
|
|
766
|
+
const startNum = ordered ? parseInt(first[2], 10) : 1;
|
|
767
|
+
const items = [];
|
|
768
|
+
for (const l of block) {
|
|
769
|
+
const m = l.match(this.listItemRe);
|
|
770
|
+
if (m && m[1].length <= baseIndent) {
|
|
771
|
+
items.push({ text: m[3], childLines: [] });
|
|
772
|
+
}
|
|
773
|
+
else if (items.length > 0) {
|
|
774
|
+
// Continuation or nested content: strip one nesting level of indent
|
|
775
|
+
items[items.length - 1].childLines.push(l.length > baseIndent + 2 ? l.slice(baseIndent + 2) : l.trim());
|
|
776
|
+
}
|
|
1097
777
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
// No value at all
|
|
1106
|
-
if (!value)
|
|
1107
|
-
return undefined;
|
|
1108
|
-
// A string
|
|
1109
|
-
if (typeof value === 'string' || value instanceof String)
|
|
1110
|
-
value = this.parseDate(value);
|
|
1111
|
-
// Not a date
|
|
1112
|
-
if (!(value instanceof Date && value.getTime()))
|
|
1113
|
-
return undefined;
|
|
1114
|
-
// Update date
|
|
1115
|
-
return new TZDate(value, "Europe/Rome");
|
|
1116
|
-
}
|
|
1117
|
-
/**
|
|
1118
|
-
* Update a DateInterval object according to a string
|
|
1119
|
-
* @param value : string value
|
|
1120
|
-
* @param interval : DateInterval value to update
|
|
1121
|
-
* @param end : true if must be updated the first or the end value
|
|
1122
|
-
* @param copy : copy the same value (works only if not end element)
|
|
1123
|
-
*/
|
|
1124
|
-
static changeDateInterval(value, interval, end = false, copy = false) {
|
|
1125
|
-
if (value) {
|
|
1126
|
-
let year = -1;
|
|
1127
|
-
if (value.length === 4 && (year = parseInt(value)) > 1750) {
|
|
1128
|
-
if (!end) {
|
|
1129
|
-
interval.from = new TZDate(new Date(year, 0, 1), 'Europe/Rome');
|
|
1130
|
-
interval.to = new TZDate(new Date(year, 11, 31), 'Europe/Rome');
|
|
1131
|
-
}
|
|
1132
|
-
else
|
|
1133
|
-
interval.to = new Date(year, 11, 31);
|
|
778
|
+
const lis = items.map(item => {
|
|
779
|
+
let inner;
|
|
780
|
+
// GFM task list: - [ ] / - [x]
|
|
781
|
+
const task = item.text.match(/^\[([ xX])\]\s+(.*)$/);
|
|
782
|
+
if (task) {
|
|
783
|
+
const checked = task[1] !== ' ' ? ' checked' : '';
|
|
784
|
+
inner = `<input type="checkbox" disabled${checked}> ${this.inline(task[2], opts)}`;
|
|
1134
785
|
}
|
|
1135
786
|
else {
|
|
1136
|
-
|
|
1137
|
-
if (!parts || isNaN(parts[0]) || parts[0] < 0 || parts[0] < 1750)
|
|
1138
|
-
return;
|
|
1139
|
-
const d = new TZDate(new Date(parts[0], parts[1], parts[2]), 'Europe/Rome');
|
|
1140
|
-
if (end)
|
|
1141
|
-
interval.to = d;
|
|
1142
|
-
else if (copy) {
|
|
1143
|
-
interval.from = d;
|
|
1144
|
-
interval.to = d;
|
|
1145
|
-
}
|
|
1146
|
-
else
|
|
1147
|
-
interval.from = d;
|
|
787
|
+
inner = this.inline(item.text, opts);
|
|
1148
788
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
*/
|
|
1158
|
-
static formatNumber(value, decimals = 2, locale = 'it-IT') {
|
|
1159
|
-
return Intl.NumberFormat(locale, { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: decimals }).format(value);
|
|
789
|
+
if (item.childLines.length > 0) {
|
|
790
|
+
inner += '\n' + this.parseBlocks(item.childLines, opts);
|
|
791
|
+
}
|
|
792
|
+
return `<li>${inner}</li>`;
|
|
793
|
+
});
|
|
794
|
+
const tag = ordered ? 'ol' : 'ul';
|
|
795
|
+
const startAttr = ordered && startNum > 1 ? ` start="${startNum}"` : '';
|
|
796
|
+
return `<${tag}${startAttr}>\n${lis.join('\n')}\n</${tag}>`;
|
|
1160
797
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
798
|
+
// #endregion
|
|
799
|
+
// #region Tables
|
|
800
|
+
static tableAligns(separator) {
|
|
801
|
+
return separator
|
|
802
|
+
.replace(/^\||\|$/g, '')
|
|
803
|
+
.split('|')
|
|
804
|
+
.map(cell => {
|
|
805
|
+
const t = cell.trim();
|
|
806
|
+
if (t.startsWith(':') && t.endsWith(':'))
|
|
807
|
+
return 'center';
|
|
808
|
+
if (t.endsWith(':'))
|
|
809
|
+
return 'right';
|
|
810
|
+
if (t.startsWith(':'))
|
|
811
|
+
return 'left';
|
|
812
|
+
return undefined;
|
|
813
|
+
});
|
|
1171
814
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
815
|
+
static tableRow(row, tag, aligns, opts) {
|
|
816
|
+
const cells = row.replace(/^\||\|$/g, '').split('|').map(c => c.trim());
|
|
817
|
+
const cellsHtml = cells
|
|
818
|
+
.map((cell, j) => {
|
|
819
|
+
const align = aligns[j] ? ` align="${aligns[j]}"` : '';
|
|
820
|
+
return `<${tag}${align}>${this.inline(cell, opts)}</${tag}>`;
|
|
821
|
+
})
|
|
822
|
+
.join('');
|
|
823
|
+
return `<tr>${cellsHtml}</tr>`;
|
|
1181
824
|
}
|
|
825
|
+
// #endregion
|
|
826
|
+
// #region Inline formatting
|
|
1182
827
|
/**
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
828
|
+
* Applies inline markdown to a single text segment.
|
|
829
|
+
* Generated HTML (code, links, images, autolinks) is stashed behind \u0000
|
|
830
|
+
* placeholders so later regex passes can never corrupt it.
|
|
1186
831
|
*/
|
|
1187
|
-
static
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
832
|
+
static inline(text, opts) {
|
|
833
|
+
const store = [];
|
|
834
|
+
const stash = (html) => `\u0000${store.push(html) - 1}\u0000`;
|
|
835
|
+
// 1. Inline code: stash first so later passes can't touch it.
|
|
836
|
+
// Content is ALWAYS escaped so <, >, & inside code survive in the DOM
|
|
837
|
+
// (the browser renders entities back to the original characters).
|
|
838
|
+
let s = text.replace(/`([^`]+)`/g, (_m, code) => stash(`<code>${this.escape(code)}</code>`));
|
|
839
|
+
// 2. Escape raw HTML as entities (opt-in only)
|
|
840
|
+
if (opts.escapeHtml)
|
|
841
|
+
s = this.escape(s);
|
|
842
|
+
// 3. Images: 
|
|
843
|
+
s = s.replace(/!\[([^\]]*)\]\(\s*([^)\s]+)(?:\s+(?:"|")(.+?)(?:"|"))?\s*\)/g, (_m, alt, src, title) => {
|
|
844
|
+
const url = this.safeUrl(src);
|
|
845
|
+
if (!url)
|
|
846
|
+
return alt;
|
|
847
|
+
const t = title ? ` title="${this.escapeAttr(title)}"` : '';
|
|
848
|
+
return stash(`<img src="${url}" alt="${this.escapeAttr(alt)}"${t} loading="lazy">`);
|
|
849
|
+
});
|
|
850
|
+
// 4. Links: [text](url "title")
|
|
851
|
+
s = s.replace(/\[([^\]]+)\]\(\s*([^)\s]+)(?:\s+(?:"|")(.+?)(?:"|"))?\s*\)/g, (_m, txt, href, title) => {
|
|
852
|
+
const url = this.safeUrl(href);
|
|
853
|
+
if (!url)
|
|
854
|
+
return txt;
|
|
855
|
+
const t = title ? ` title="${this.escapeAttr(title)}"` : '';
|
|
856
|
+
return stash(`<a href="${url}"${t} target="_blank" rel="noopener noreferrer">${txt}</a>`);
|
|
857
|
+
});
|
|
858
|
+
// 5. Autolink remaining bare URLs (markdown links are already stashed)
|
|
859
|
+
s = s.replace(/https?:\/\/[^\s<>"']+/g, url => stash(`<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`));
|
|
860
|
+
// 6. Bold / italic / strikethrough.
|
|
861
|
+
// Delimiters must hug the text; underscore emphasis only at word
|
|
862
|
+
// boundaries so identifiers like snake_case are left untouched.
|
|
863
|
+
s = s.replace(/\*\*\*(?=\S)([\s\S]*?\S)\*\*\*/g, '<strong><em>$1</em></strong>');
|
|
864
|
+
s = s.replace(/(?<![\w*])___(?=\S)([\s\S]*?\S)___(?![\w*])/g, '<strong><em>$1</em></strong>');
|
|
865
|
+
s = s.replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, '<strong>$1</strong>');
|
|
866
|
+
s = s.replace(/(?<![\w*])__(?=\S)([\s\S]*?\S)__(?![\w*])/g, '<strong>$1</strong>');
|
|
867
|
+
s = s.replace(/\*(?=\S)([^*\n]*?\S)\*/g, '<em>$1</em>');
|
|
868
|
+
s = s.replace(/(?<![\w_])_(?=\S)([^_\n]*?\S)_(?![\w_])/g, '<em>$1</em>');
|
|
869
|
+
s = s.replace(/~~(?=\S)([\s\S]*?\S)~~/g, '<del>$1</del>');
|
|
870
|
+
// 7. Restore stashed HTML
|
|
871
|
+
return s.replace(/\u0000(\d+)\u0000/g, (_m, idx) => store[Number(idx)] ?? '');
|
|
872
|
+
}
|
|
873
|
+
// #endregion
|
|
874
|
+
// #region Security helpers
|
|
875
|
+
/** Escapes quotes for safe interpolation inside a double-quoted HTML attribute. */
|
|
876
|
+
static escapeAttr(s) {
|
|
877
|
+
return s.replace(/&(?!(?:amp|lt|gt|quot|#\d+);)/g, '&').replace(/"/g, '"');
|
|
878
|
+
}
|
|
879
|
+
/** Escapes &, <, > and " for safe HTML interpolation. */
|
|
880
|
+
static escape(s) {
|
|
881
|
+
return s
|
|
882
|
+
.replace(/&/g, '&')
|
|
883
|
+
.replace(/</g, '<')
|
|
884
|
+
.replace(/>/g, '>')
|
|
885
|
+
.replace(/"/g, '"');
|
|
1191
886
|
}
|
|
1192
887
|
/**
|
|
1193
|
-
*
|
|
1194
|
-
*
|
|
1195
|
-
*
|
|
888
|
+
* Returns a sanitized URL or undefined when the scheme is dangerous.
|
|
889
|
+
* Blocks javascript:, vbscript: and data: (also when obfuscated with
|
|
890
|
+
* whitespace/control characters, e.g. "java\tscript:").
|
|
1196
891
|
*/
|
|
1197
|
-
static
|
|
1198
|
-
|
|
892
|
+
static safeUrl(url) {
|
|
893
|
+
const compact = url.replace(/[\s\u0000-\u001f]/g, '');
|
|
894
|
+
if (/^(javascript|vbscript|data):/i.test(compact))
|
|
1199
895
|
return undefined;
|
|
1200
|
-
|
|
1201
|
-
return v && v !== 'null' ? v : undefined;
|
|
896
|
+
return url.replace(/"/g, '%22');
|
|
1202
897
|
}
|
|
898
|
+
// #endregion
|
|
899
|
+
// #region Clipboard
|
|
1203
900
|
/**
|
|
1204
|
-
*
|
|
1205
|
-
*
|
|
901
|
+
* Copies markdown content to the clipboard in two flavors:
|
|
902
|
+
* - text/html : the rendered HTML (rich paste into Word, Outlook, Gmail...)
|
|
903
|
+
* - text/plain : the original markdown source (paste into editors, IDEs...)
|
|
904
|
+
* @param markdown : the markdown source
|
|
905
|
+
* @param options : conversion options for the HTML flavor
|
|
906
|
+
* @returns : true on success
|
|
1206
907
|
*/
|
|
1207
|
-
static
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
908
|
+
static async copyToClipboard(markdown, options) {
|
|
909
|
+
if (!markdown || typeof navigator === 'undefined' || !navigator.clipboard)
|
|
910
|
+
return false;
|
|
911
|
+
try {
|
|
912
|
+
if (typeof ClipboardItem !== 'undefined') {
|
|
913
|
+
const html = this.toHtml(markdown, options);
|
|
914
|
+
await navigator.clipboard.write([
|
|
915
|
+
new ClipboardItem({
|
|
916
|
+
'text/html': new Blob([html], { type: 'text/html' }),
|
|
917
|
+
'text/plain': new Blob([markdown], { type: 'text/plain' }),
|
|
918
|
+
}),
|
|
919
|
+
]);
|
|
1215
920
|
}
|
|
1216
921
|
else {
|
|
1217
|
-
|
|
922
|
+
// Older engines: plain text only
|
|
923
|
+
await navigator.clipboard.writeText(markdown);
|
|
1218
924
|
}
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
catch {
|
|
928
|
+
// Clipboard API requires a secure context and user activation
|
|
929
|
+
return false;
|
|
1219
930
|
}
|
|
1220
|
-
if (!hasUpperCase)
|
|
1221
|
-
password = password.substring(0, 11) + "M";
|
|
1222
|
-
return password;
|
|
1223
931
|
}
|
|
1224
932
|
/**
|
|
1225
|
-
*
|
|
1226
|
-
* @param
|
|
1227
|
-
* @returns
|
|
933
|
+
* Copies plain text (e.g. the content of a single code block) to the clipboard.
|
|
934
|
+
* @param text : the text to copy
|
|
935
|
+
* @returns : true on success
|
|
1228
936
|
*/
|
|
1229
|
-
static
|
|
1230
|
-
if (
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
if (password.length >= 12)
|
|
1239
|
-
score++;
|
|
1240
|
-
else if (password.length >= 10)
|
|
1241
|
-
suggestions.push('Considera di usare più di 12 caratteri.');
|
|
1242
|
-
// Lowercase letters
|
|
1243
|
-
if (/[a-z]/.test(password))
|
|
1244
|
-
score++;
|
|
1245
|
-
else
|
|
1246
|
-
suggestions.push('Aggiungi lettere minuscole.');
|
|
1247
|
-
// Uppercase letters
|
|
1248
|
-
if (/[A-Z]/.test(password))
|
|
1249
|
-
score++;
|
|
1250
|
-
else
|
|
1251
|
-
suggestions.push('Aggiungi lettere maiuscole.');
|
|
1252
|
-
// Numbers
|
|
1253
|
-
if (/\d/.test(password))
|
|
1254
|
-
score++;
|
|
1255
|
-
else
|
|
1256
|
-
suggestions.push('Aggiungi numeri.');
|
|
1257
|
-
// Special characters
|
|
1258
|
-
if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password))
|
|
1259
|
-
score++;
|
|
1260
|
-
else
|
|
1261
|
-
suggestions.push('Aggiungi caratteri speciali (!@#$%^&*).');
|
|
1262
|
-
// Common patterns
|
|
1263
|
-
if (/(.)\1{2,}/.test(password)) {
|
|
1264
|
-
score = Math.max(0, score - 1);
|
|
1265
|
-
suggestions.push('Evita di ripetere lo stesso carattere.');
|
|
1266
|
-
}
|
|
1267
|
-
if (/123|abc|qwe/i.test(password)) {
|
|
1268
|
-
score = Math.max(0, score - 1);
|
|
1269
|
-
suggestions.push('Evita sequenze comuni (123, abc, qwe).');
|
|
1270
|
-
}
|
|
1271
|
-
// Label and color
|
|
1272
|
-
let label;
|
|
1273
|
-
let color;
|
|
1274
|
-
let isValid;
|
|
1275
|
-
if (score <= 2) {
|
|
1276
|
-
label = 'Molto debole';
|
|
1277
|
-
color = '#f44336';
|
|
1278
|
-
isValid = false;
|
|
1279
|
-
}
|
|
1280
|
-
else if (score <= 3) {
|
|
1281
|
-
label = 'Debole';
|
|
1282
|
-
color = '#ff9800';
|
|
1283
|
-
isValid = false;
|
|
1284
|
-
}
|
|
1285
|
-
else if (score <= 4) {
|
|
1286
|
-
label = 'Media';
|
|
1287
|
-
color = '#ffc107';
|
|
1288
|
-
isValid = true;
|
|
1289
|
-
}
|
|
1290
|
-
else if (score <= 5) {
|
|
1291
|
-
label = 'Forte';
|
|
1292
|
-
color = '#8bc34a';
|
|
1293
|
-
isValid = true;
|
|
1294
|
-
}
|
|
1295
|
-
else {
|
|
1296
|
-
label = 'Molto forte';
|
|
1297
|
-
color = '#4caf50';
|
|
1298
|
-
isValid = true;
|
|
1299
|
-
}
|
|
1300
|
-
return { score, label, color, suggestions, isValid };
|
|
937
|
+
static async copyText(text) {
|
|
938
|
+
if (!text || typeof navigator === 'undefined' || !navigator.clipboard)
|
|
939
|
+
return false;
|
|
940
|
+
try {
|
|
941
|
+
await navigator.clipboard.writeText(text);
|
|
942
|
+
return true;
|
|
943
|
+
}
|
|
944
|
+
catch {
|
|
945
|
+
return false;
|
|
1301
946
|
}
|
|
1302
|
-
else
|
|
1303
|
-
return {
|
|
1304
|
-
score: 0,
|
|
1305
|
-
isValid: false,
|
|
1306
|
-
suggestions: []
|
|
1307
|
-
};
|
|
1308
947
|
}
|
|
1309
948
|
/**
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
949
|
+
* Extracts the visible plain text from a rendered element
|
|
950
|
+
* (what the user sees, entities already decoded by the browser).
|
|
951
|
+
* @param element : the element hosting the rendered markdown
|
|
952
|
+
* @returns : the plain text
|
|
1312
953
|
*/
|
|
1313
|
-
static
|
|
1314
|
-
return
|
|
1315
|
-
navigator.maxTouchPoints > 0));
|
|
954
|
+
static elementToText(element) {
|
|
955
|
+
return element.innerText ?? element.textContent ?? '';
|
|
1316
956
|
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
var DateFormat;
|
|
960
|
+
(function (DateFormat) {
|
|
961
|
+
DateFormat[DateFormat["Short"] = 1] = "Short";
|
|
962
|
+
DateFormat[DateFormat["Long"] = 2] = "Long";
|
|
963
|
+
DateFormat[DateFormat["LongWithShortMonth"] = 3] = "LongWithShortMonth";
|
|
964
|
+
DateFormat[DateFormat["LongWithWeekDay"] = 4] = "LongWithWeekDay";
|
|
965
|
+
DateFormat[DateFormat["LongWithShortWeekDay"] = 5] = "LongWithShortWeekDay";
|
|
966
|
+
DateFormat[DateFormat["MonthAndYear"] = 6] = "MonthAndYear";
|
|
967
|
+
DateFormat[DateFormat["LongMonthAndYear"] = 7] = "LongMonthAndYear";
|
|
968
|
+
DateFormat[DateFormat["WeekDay"] = 8] = "WeekDay";
|
|
969
|
+
DateFormat[DateFormat["LongWeekDay"] = 9] = "LongWeekDay";
|
|
970
|
+
DateFormat[DateFormat["DayAndMonth"] = 10] = "DayAndMonth";
|
|
971
|
+
DateFormat[DateFormat["ShortUS"] = 11] = "ShortUS";
|
|
972
|
+
DateFormat[DateFormat["ShortISO8601"] = 12] = "ShortISO8601"; // yyyy-mm-dd
|
|
973
|
+
})(DateFormat || (DateFormat = {}));
|
|
974
|
+
class SystemUtils {
|
|
975
|
+
/** Shared collator for locale-aware, case-insensitive string comparison. */
|
|
976
|
+
static { this.collator = new Intl.Collator('it', { sensitivity: 'base', numeric: true }); }
|
|
1317
977
|
/**
|
|
1318
|
-
*
|
|
1319
|
-
*
|
|
1320
|
-
* @
|
|
978
|
+
* Array find by key
|
|
979
|
+
* @param array : the array to scan
|
|
980
|
+
* @param key : key name
|
|
981
|
+
* @param value : the value to search for
|
|
982
|
+
* @returns : the property value or null
|
|
1321
983
|
*/
|
|
1322
|
-
static
|
|
1323
|
-
|
|
984
|
+
static arrayFindByKey(array, key, value) {
|
|
985
|
+
if (!array)
|
|
986
|
+
return undefined;
|
|
987
|
+
for (const item of array) {
|
|
988
|
+
if (item[key] === value) {
|
|
989
|
+
return item;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return undefined;
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Array find index by key
|
|
996
|
+
* @param array : the array to scan
|
|
997
|
+
* @param key : the key name
|
|
998
|
+
* @param value : the value to search for
|
|
999
|
+
* @returns : the array index or -1 if not found
|
|
1000
|
+
*/
|
|
1001
|
+
static arrayFindIndexByKey(array, key, value) {
|
|
1002
|
+
if (!array)
|
|
1003
|
+
return -1;
|
|
1004
|
+
for (let i = 0; i < array.length; i++) {
|
|
1005
|
+
if (array[i][key] === value) {
|
|
1006
|
+
return i;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return -1;
|
|
1324
1010
|
}
|
|
1325
1011
|
/**
|
|
1326
|
-
*
|
|
1327
|
-
* @param
|
|
1328
|
-
* @
|
|
1012
|
+
* Get a value from and array made of name|value items
|
|
1013
|
+
* @param array : the array to scan
|
|
1014
|
+
* @param value : the value to search for
|
|
1015
|
+
* @returns : the property value or null if not found
|
|
1329
1016
|
*/
|
|
1330
|
-
static
|
|
1331
|
-
|
|
1017
|
+
static arrayGetValue(array, value) {
|
|
1018
|
+
if (!array)
|
|
1019
|
+
return undefined;
|
|
1020
|
+
for (const item of array) {
|
|
1021
|
+
const i = item;
|
|
1022
|
+
if (i['value'] === value) {
|
|
1023
|
+
return i['name'] ?? i['id'];
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return undefined;
|
|
1332
1027
|
}
|
|
1333
1028
|
/**
|
|
1334
|
-
* Convert
|
|
1335
|
-
* @param
|
|
1336
|
-
* @param parent :
|
|
1337
|
-
* @returns :
|
|
1029
|
+
* Convert items to nodes into a tree structure
|
|
1030
|
+
* @param items : list of nodes
|
|
1031
|
+
* @param parent : parent node
|
|
1032
|
+
* @returns : an array of INode objects
|
|
1338
1033
|
*/
|
|
1339
|
-
static
|
|
1034
|
+
static arrayToNodes(items, parent) {
|
|
1340
1035
|
const nodes = [];
|
|
1341
|
-
|
|
1036
|
+
items.forEach(n => {
|
|
1342
1037
|
const node = {
|
|
1343
|
-
id: n.id,
|
|
1038
|
+
id: n.id.toString(),
|
|
1344
1039
|
name: n.name,
|
|
1345
|
-
count: n.count,
|
|
1346
|
-
parent: parent,
|
|
1040
|
+
count: n.count ? n.count : 0,
|
|
1347
1041
|
children: undefined,
|
|
1042
|
+
parent,
|
|
1348
1043
|
bag: n,
|
|
1349
1044
|
};
|
|
1350
|
-
const children = n.children ?? n.subFolders ?? [];
|
|
1351
|
-
if (children.length > 0) {
|
|
1352
|
-
node.children = this._toNodes(children, node);
|
|
1353
|
-
}
|
|
1354
1045
|
nodes.push(node);
|
|
1046
|
+
node.children =
|
|
1047
|
+
n.children && n.children.length > 0
|
|
1048
|
+
? this.arrayToNodes(n.children, node)
|
|
1049
|
+
: [];
|
|
1355
1050
|
});
|
|
1356
1051
|
return nodes;
|
|
1357
1052
|
}
|
|
1358
1053
|
/**
|
|
1359
|
-
*
|
|
1360
|
-
*
|
|
1361
|
-
*
|
|
1362
|
-
* @
|
|
1054
|
+
* Comparator factory for sorting arrays of objects by a given property key.
|
|
1055
|
+
* Pass the element type for compile-time key checking:
|
|
1056
|
+
* `items.sort(SystemUtils.arraySortCompare<Employee>('lastName'))`.
|
|
1057
|
+
* @param key - Name of the property to sort by (autocompleted from `T` when provided).
|
|
1058
|
+
* @param order - Sort direction: `'asc'` (default) or `'desc'`.
|
|
1059
|
+
* @returns A comparator function that returns a negative, zero, or positive number.
|
|
1363
1060
|
*/
|
|
1364
|
-
static
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
const
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1061
|
+
static arraySortCompare(key, order = 'asc') {
|
|
1062
|
+
const dir = order === 'desc' ? -1 : 1;
|
|
1063
|
+
return (a, b) => {
|
|
1064
|
+
const varA = a[key];
|
|
1065
|
+
const varB = b[key];
|
|
1066
|
+
if (varA === varB)
|
|
1067
|
+
return 0;
|
|
1068
|
+
if (varA === undefined || varA === null)
|
|
1069
|
+
return -1 * dir;
|
|
1070
|
+
if (varB === undefined || varB === null)
|
|
1071
|
+
return 1 * dir;
|
|
1072
|
+
if (typeof varA === 'string' && typeof varB === 'string') {
|
|
1073
|
+
return SystemUtils.collator.compare(varA, varB) * dir;
|
|
1373
1074
|
}
|
|
1374
|
-
return
|
|
1375
|
-
}
|
|
1075
|
+
return (varA > varB ? 1 : varA < varB ? -1 : 0) * dir;
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Format weight
|
|
1080
|
+
* @param gr : grams
|
|
1081
|
+
* @returns : the formatted string
|
|
1082
|
+
*/
|
|
1083
|
+
static formatWeight(gr) {
|
|
1084
|
+
if (gr > 1000000)
|
|
1085
|
+
return `${(gr / 1000000).toFixed(2)} t`;
|
|
1086
|
+
else if (gr > 100000)
|
|
1087
|
+
return `${(gr / 100000).toFixed(2)} q`;
|
|
1088
|
+
else if (gr > 1000)
|
|
1089
|
+
return `${(gr / 1000).toFixed(2)} kg`;
|
|
1376
1090
|
else
|
|
1377
|
-
return
|
|
1091
|
+
return `${gr} gr`;
|
|
1378
1092
|
}
|
|
1379
|
-
/** Cache for resolved color luminance results. */
|
|
1380
|
-
static { this.colorLightCache = new Map(); }
|
|
1381
1093
|
/**
|
|
1382
|
-
*
|
|
1383
|
-
* @param
|
|
1384
|
-
* @
|
|
1385
|
-
* @returns true if the color is light
|
|
1094
|
+
* Format file size
|
|
1095
|
+
* @param bytes : number of bytes
|
|
1096
|
+
* @returns : the formatted string
|
|
1386
1097
|
*/
|
|
1387
|
-
static
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1098
|
+
static formatFileSize(bytes) {
|
|
1099
|
+
const MB = 1024 * 1024;
|
|
1100
|
+
if (bytes >= MB)
|
|
1101
|
+
return `${(bytes / MB).toFixed(1)} MB`;
|
|
1102
|
+
if (bytes >= 1024)
|
|
1103
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1104
|
+
return `${bytes} byte`;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Compare two strings (case-insensitive, locale-aware, accent-insensitive).
|
|
1108
|
+
* @param a : string a
|
|
1109
|
+
* @param b : string b
|
|
1110
|
+
* @returns : 0 if equals, 1 if bigger, -1 if lower
|
|
1111
|
+
*/
|
|
1112
|
+
static compareString(a, b) {
|
|
1113
|
+
return this.collator.compare(a ?? '', b ?? '');
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Capitalize a string
|
|
1117
|
+
* @param s : the string to capitalize
|
|
1118
|
+
* @returns : the capitalized string
|
|
1119
|
+
*/
|
|
1120
|
+
static capitalize(s) {
|
|
1121
|
+
if (!s)
|
|
1122
|
+
return undefined;
|
|
1123
|
+
let b = "";
|
|
1124
|
+
let cap = true;
|
|
1125
|
+
for (const char of s) {
|
|
1126
|
+
if (char === " ") {
|
|
1127
|
+
b += char;
|
|
1128
|
+
cap = true;
|
|
1129
|
+
}
|
|
1130
|
+
else if (cap) {
|
|
1131
|
+
b += char.toUpperCase();
|
|
1132
|
+
cap = false;
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
b += char.toLowerCase();
|
|
1136
|
+
}
|
|
1406
1137
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1138
|
+
return b;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Truncate a string at the last word boundary before `max`.
|
|
1142
|
+
* @param s : the string to truncate
|
|
1143
|
+
* @param max : the max number of chars
|
|
1144
|
+
* @returns : the truncated string
|
|
1145
|
+
*/
|
|
1146
|
+
static truncate(s, max = 500) {
|
|
1147
|
+
if (!s)
|
|
1148
|
+
return undefined;
|
|
1149
|
+
if (s.length <= max)
|
|
1150
|
+
return s;
|
|
1151
|
+
const i = s.lastIndexOf(' ', max - 1);
|
|
1152
|
+
return i > 0 ? s.slice(0, i) : s;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Join a list of strings
|
|
1156
|
+
* @param items : the list of strings
|
|
1157
|
+
* @param sep : the separator string
|
|
1158
|
+
* @param max : the maximum resulting string
|
|
1159
|
+
* @returns : the joined string
|
|
1160
|
+
*/
|
|
1161
|
+
static join(items, sep = " ", max = 350) {
|
|
1162
|
+
if (!items || items.length === 0)
|
|
1163
|
+
return undefined;
|
|
1164
|
+
if (items.length > 1) {
|
|
1165
|
+
let l = 0;
|
|
1166
|
+
let s = "";
|
|
1167
|
+
while (s.length < max && items.length > l) {
|
|
1168
|
+
if (l > 0) {
|
|
1169
|
+
s += sep;
|
|
1170
|
+
}
|
|
1171
|
+
s += items[l++];
|
|
1172
|
+
}
|
|
1173
|
+
if (items.length > l) {
|
|
1174
|
+
s += "...";
|
|
1175
|
+
}
|
|
1176
|
+
return s;
|
|
1177
|
+
}
|
|
1178
|
+
return items[0];
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Normalize a string by converting it to lowercase and removing extra spaces, with special handling for acronyms and camelCase.
|
|
1182
|
+
* @param s : the string to normalize
|
|
1183
|
+
* @returns : The normalized string, or `undefined` if the input is falsy.
|
|
1184
|
+
*/
|
|
1185
|
+
static normalizeDisplay(s) {
|
|
1186
|
+
if (!s)
|
|
1187
|
+
return s;
|
|
1188
|
+
return s
|
|
1189
|
+
.split(' ')
|
|
1190
|
+
.map((word, wordIndex) => {
|
|
1191
|
+
if (!word)
|
|
1192
|
+
return word;
|
|
1193
|
+
// If the word is all uppercase and contains at least one letter, we assume it's an acronym and leave it as is
|
|
1194
|
+
if (word === word.toUpperCase() && /[A-Z]/.test(word)) {
|
|
1195
|
+
return word;
|
|
1196
|
+
}
|
|
1197
|
+
// Otherwise, convert to lowercase letter by letter
|
|
1198
|
+
const chars = word.split('');
|
|
1199
|
+
return chars
|
|
1200
|
+
.map((char, charIndex) => {
|
|
1201
|
+
if (charIndex === 0 && wordIndex === 0) {
|
|
1202
|
+
// First letter of the first word:
|
|
1203
|
+
// remains uppercase only if the first 2 letters are both uppercase
|
|
1204
|
+
const secondChar = chars[1] ?? '';
|
|
1205
|
+
const keepUppercase = char === char.toUpperCase() &&
|
|
1206
|
+
secondChar === secondChar.toUpperCase() &&
|
|
1207
|
+
/[A-Z]/.test(char) &&
|
|
1208
|
+
/[A-Z]/.test(secondChar);
|
|
1209
|
+
return keepUppercase ? char : char.toLowerCase();
|
|
1210
|
+
}
|
|
1211
|
+
return char.toLowerCase();
|
|
1212
|
+
})
|
|
1213
|
+
.join('');
|
|
1214
|
+
})
|
|
1215
|
+
.join(' ');
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Wraps bare URLs in the given string with `<a>` anchor tags.
|
|
1219
|
+
* @param s - The plain-text or HTML string to process.
|
|
1220
|
+
* @returns The string with URLs replaced by clickable links, or `''` when `s` is falsy.
|
|
1221
|
+
*/
|
|
1222
|
+
static replaceAsHtml(s) {
|
|
1223
|
+
if (!s)
|
|
1224
|
+
return '';
|
|
1225
|
+
return s.replace(/https?:\/\/[^\s<]+/gi, v => `<a href="${v}" target="_blank" rel="noopener">${v}</a>`);
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Convert markdown to html
|
|
1229
|
+
* @param markdown : the markdown data
|
|
1230
|
+
* @param escapeHtml : true to escape HTML. Default is false
|
|
1231
|
+
* @returns the html
|
|
1232
|
+
*/
|
|
1233
|
+
static markdownToHtml(markdown, escapeHtml = false) {
|
|
1234
|
+
return MarkdownUtils.toHtml(markdown, { escapeHtml: escapeHtml });
|
|
1409
1235
|
}
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
/**
|
|
1413
|
-
* General-purpose formatting pipe that converts a raw value to a locale-aware string
|
|
1414
|
-
* based on the specified format type.
|
|
1415
|
-
*
|
|
1416
|
-
* Supported types: `'date'` / `'D'`, `'currency'` / `'C'`, `'number'` / `'N'`,
|
|
1417
|
-
* `'number0'` / `'N0'`, `'percentage'` / `'P'`.
|
|
1418
|
-
*
|
|
1419
|
-
* Usage: `{{ value | format:'currency' }}`
|
|
1420
|
-
*/
|
|
1421
|
-
class FormatPipe {
|
|
1422
1236
|
/**
|
|
1423
|
-
*
|
|
1424
|
-
*
|
|
1425
|
-
* @param
|
|
1426
|
-
* @
|
|
1427
|
-
* @param pattern - The date pattern used when `type` is `'date'` (default: `'dd/MM/yyyy'`).
|
|
1428
|
-
* @returns A formatted string, or `undefined` when the value cannot be formatted.
|
|
1237
|
+
* Compare two names
|
|
1238
|
+
* @param a : name a
|
|
1239
|
+
* @param b : name b
|
|
1240
|
+
* @returns : true if a equals b
|
|
1429
1241
|
*/
|
|
1430
|
-
|
|
1431
|
-
if (
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1242
|
+
static compareNames(a, b) {
|
|
1243
|
+
if (a)
|
|
1244
|
+
a = a.trim();
|
|
1245
|
+
if (b)
|
|
1246
|
+
b = b.trim();
|
|
1247
|
+
if (a && b && a.length !== b.length)
|
|
1248
|
+
return false;
|
|
1249
|
+
if (this.compareString(a, b) === 0)
|
|
1250
|
+
return true;
|
|
1251
|
+
const p1 = (a ?? '').split(' ');
|
|
1252
|
+
const p2 = (b ?? '').split(' ');
|
|
1253
|
+
if (p1.length !== p2.length)
|
|
1254
|
+
return false;
|
|
1255
|
+
let matches = p1.length;
|
|
1256
|
+
const used = new Array(p2.length).fill(false);
|
|
1257
|
+
for (const s1 of p1) {
|
|
1258
|
+
for (let j = 0; j < p2.length; j++) {
|
|
1259
|
+
if (!used[j] && this.compareString(s1, p2[j]) === 0) {
|
|
1260
|
+
used[j] = true;
|
|
1261
|
+
matches--;
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1440
1264
|
}
|
|
1441
|
-
case 'C':
|
|
1442
|
-
case 'currency':
|
|
1443
|
-
return new Intl.NumberFormat('it-IT', { style: 'currency', currency: 'EUR' }).format(value);
|
|
1444
|
-
case 'N':
|
|
1445
|
-
case 'number':
|
|
1446
|
-
return new Intl.NumberFormat('it-IT', { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(value);
|
|
1447
|
-
case 'N0':
|
|
1448
|
-
case 'number0':
|
|
1449
|
-
return new Intl.NumberFormat('it-IT', { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(value);
|
|
1450
|
-
case 'P':
|
|
1451
|
-
case 'percentage':
|
|
1452
|
-
return new Intl.NumberFormat('it-IT', { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(value);
|
|
1453
1265
|
}
|
|
1454
|
-
return
|
|
1455
|
-
}
|
|
1456
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
1457
|
-
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: FormatPipe, isStandalone: true, name: "format" }); }
|
|
1458
|
-
}
|
|
1459
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatPipe, decorators: [{
|
|
1460
|
-
type: Pipe,
|
|
1461
|
-
args: [{
|
|
1462
|
-
name: 'format',
|
|
1463
|
-
standalone: true
|
|
1464
|
-
}]
|
|
1465
|
-
}] });
|
|
1466
|
-
|
|
1467
|
-
/**
|
|
1468
|
-
* Pipe that applies a global regex replacement on a string and returns the result
|
|
1469
|
-
* as sanitized HTML. When `regexValue` is `'\n'` and no `replaceValue` is given,
|
|
1470
|
-
* newlines are replaced with `<br>` tags.
|
|
1471
|
-
*
|
|
1472
|
-
* Usage: `{{ text | replace:'\n':'' }}`
|
|
1473
|
-
*/
|
|
1474
|
-
class ReplacePipe {
|
|
1475
|
-
constructor() {
|
|
1476
|
-
this.sanitizer = inject(DomSanitizer);
|
|
1266
|
+
return matches === 0;
|
|
1477
1267
|
}
|
|
1478
1268
|
/**
|
|
1479
|
-
*
|
|
1480
|
-
*
|
|
1481
|
-
* @
|
|
1482
|
-
*
|
|
1483
|
-
* @
|
|
1484
|
-
*
|
|
1269
|
+
* Obfuscate a text with a key-driven code point rotation.
|
|
1270
|
+
*
|
|
1271
|
+
* @deprecated This is OBFUSCATION, not cryptography: it hides values from
|
|
1272
|
+
* casual inspection but offers no real security. Use {@link encrypt} /
|
|
1273
|
+
* {@link decrypt} (AES-GCM via Web Crypto) for anything sensitive.
|
|
1274
|
+
*
|
|
1275
|
+
* The rotation operates on Unicode code points and skips the surrogate
|
|
1276
|
+
* range, so the output is always a well-formed string: emoji and other
|
|
1277
|
+
* astral characters round-trip correctly (the previous code-unit based
|
|
1278
|
+
* version could emit lone surrogates and corrupt them).
|
|
1279
|
+
*
|
|
1280
|
+
* BREAKING CHANGE: values encoded with the previous code-unit algorithm
|
|
1281
|
+
* cannot be decoded by this implementation.
|
|
1282
|
+
*
|
|
1283
|
+
* @param text : the text to encode or decode
|
|
1284
|
+
* @param key : the key
|
|
1285
|
+
* @param reverse : true to decode, false to encode
|
|
1286
|
+
* @returns : the obfuscated (or restored) text, or undefined when text/key are empty
|
|
1485
1287
|
*/
|
|
1486
|
-
|
|
1487
|
-
if (!
|
|
1288
|
+
static cipher(text, key, reverse = false) {
|
|
1289
|
+
if (!text || !key)
|
|
1488
1290
|
return undefined;
|
|
1489
|
-
|
|
1490
|
-
|
|
1291
|
+
// Valid Unicode scalar values: [0, 0xD7FF] + [0xE000, 0x10FFFF].
|
|
1292
|
+
// We rotate inside this contiguous "logical" space (surrogates excluded)
|
|
1293
|
+
// so the result is always a valid code point.
|
|
1294
|
+
const SURROGATE_START = 0xd800;
|
|
1295
|
+
const SURROGATE_SIZE = 0x800;
|
|
1296
|
+
const SPACE = 0x110000 - SURROGATE_SIZE; // number of valid scalar values
|
|
1297
|
+
const toIndex = (cp) => (cp < SURROGATE_START ? cp : cp - SURROGATE_SIZE);
|
|
1298
|
+
const toCodePoint = (i) => (i < SURROGATE_START ? i : i + SURROGATE_SIZE);
|
|
1299
|
+
const keyLen = key.length;
|
|
1300
|
+
let result = '';
|
|
1301
|
+
let i = 0;
|
|
1302
|
+
for (const ch of text) { // iterates by code point, not code unit
|
|
1303
|
+
let rotation = key.charCodeAt(i % keyLen);
|
|
1304
|
+
if (reverse)
|
|
1305
|
+
rotation = -rotation;
|
|
1306
|
+
const index = toIndex(ch.codePointAt(0));
|
|
1307
|
+
const rotated = ((index + rotation) % SPACE + SPACE) % SPACE;
|
|
1308
|
+
result += String.fromCodePoint(toCodePoint(rotated));
|
|
1309
|
+
i++;
|
|
1310
|
+
}
|
|
1311
|
+
return result;
|
|
1491
1312
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1313
|
+
// #region Real encryption (Web Crypto, AES-GCM)
|
|
1314
|
+
/** Payload format version, first byte of the binary envelope. */
|
|
1315
|
+
static { this.CIPHER_VERSION = 1; }
|
|
1316
|
+
/** PBKDF2 salt length in bytes. */
|
|
1317
|
+
static { this.CIPHER_SALT_LENGTH = 16; }
|
|
1318
|
+
/** AES-GCM IV length in bytes (96 bit, the recommended size for GCM). */
|
|
1319
|
+
static { this.CIPHER_IV_LENGTH = 12; }
|
|
1320
|
+
/** PBKDF2-HMAC-SHA-256 iterations (OWASP recommendation). */
|
|
1321
|
+
static { this.CIPHER_ITERATIONS = 600000; }
|
|
1322
|
+
/**
|
|
1323
|
+
* Derives an AES-GCM 256-bit key from a password using PBKDF2-HMAC-SHA-256.
|
|
1324
|
+
* @param password : the user password / passphrase
|
|
1325
|
+
* @param salt : the random salt bound to this payload
|
|
1326
|
+
* @returns : a non-extractable AES-GCM CryptoKey
|
|
1327
|
+
*/
|
|
1328
|
+
static async deriveKey(password, salt) {
|
|
1329
|
+
const material = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveKey']);
|
|
1330
|
+
return crypto.subtle.deriveKey({ name: 'PBKDF2', salt: salt, iterations: this.CIPHER_ITERATIONS, hash: 'SHA-256' }, material, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Encodes bytes as URL-safe Base64 (no '+', '/' or trailing '=').
|
|
1334
|
+
* @param bytes : the bytes to encode
|
|
1335
|
+
* @returns : the base64url string
|
|
1336
|
+
*/
|
|
1337
|
+
static toBase64Url(bytes) {
|
|
1338
|
+
let binary = '';
|
|
1339
|
+
const CHUNK = 0x8000;
|
|
1340
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
1341
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
1342
|
+
}
|
|
1343
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Decodes a URL-safe Base64 string back to bytes.
|
|
1347
|
+
* @param value : the base64url string
|
|
1348
|
+
* @returns : the decoded bytes
|
|
1349
|
+
*/
|
|
1350
|
+
static fromBase64Url(value) {
|
|
1351
|
+
const b64 = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
1352
|
+
const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);
|
|
1353
|
+
const binary = atob(padded);
|
|
1354
|
+
const bytes = new Uint8Array(binary.length);
|
|
1355
|
+
for (let i = 0; i < binary.length; i++)
|
|
1356
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1357
|
+
return bytes;
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Encrypts a text with a password using AES-GCM (256 bit) and a key derived
|
|
1361
|
+
* via PBKDF2-HMAC-SHA-256 with a random per-message salt.
|
|
1362
|
+
*
|
|
1363
|
+
* The result is a self-contained, URL-safe Base64 payload:
|
|
1364
|
+
* `version (1 byte) | salt (16 bytes) | iv (12 bytes) | ciphertext+tag`.
|
|
1365
|
+
* GCM is authenticated: any tampering makes {@link decrypt} fail.
|
|
1366
|
+
*
|
|
1367
|
+
* Requires a secure context (HTTPS or localhost) for `crypto.subtle`.
|
|
1368
|
+
*
|
|
1369
|
+
* @param text : the plain text to encrypt
|
|
1370
|
+
* @param password : the password / passphrase
|
|
1371
|
+
* @returns : the base64url payload, or undefined when text/password are
|
|
1372
|
+
* empty or the Web Crypto API is unavailable
|
|
1373
|
+
*/
|
|
1374
|
+
static async encrypt(text, password) {
|
|
1375
|
+
if (!text || !password)
|
|
1376
|
+
return undefined;
|
|
1377
|
+
if (typeof crypto === 'undefined' || !crypto.subtle)
|
|
1378
|
+
return undefined;
|
|
1379
|
+
try {
|
|
1380
|
+
const salt = crypto.getRandomValues(new Uint8Array(this.CIPHER_SALT_LENGTH));
|
|
1381
|
+
const iv = crypto.getRandomValues(new Uint8Array(this.CIPHER_IV_LENGTH));
|
|
1382
|
+
const key = await this.deriveKey(password, salt);
|
|
1383
|
+
const cipherText = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, key, new TextEncoder().encode(text)));
|
|
1384
|
+
const payload = new Uint8Array(1 + salt.length + iv.length + cipherText.length);
|
|
1385
|
+
payload[0] = this.CIPHER_VERSION;
|
|
1386
|
+
payload.set(salt, 1);
|
|
1387
|
+
payload.set(iv, 1 + salt.length);
|
|
1388
|
+
payload.set(cipherText, 1 + salt.length + iv.length);
|
|
1389
|
+
return this.toBase64Url(payload);
|
|
1390
|
+
}
|
|
1391
|
+
catch {
|
|
1392
|
+
return undefined;
|
|
1393
|
+
}
|
|
1512
1394
|
}
|
|
1513
1395
|
/**
|
|
1514
|
-
*
|
|
1515
|
-
*
|
|
1516
|
-
*
|
|
1396
|
+
* Decrypts a payload produced by {@link encrypt}.
|
|
1397
|
+
* Returns undefined when the password is wrong, the payload was tampered
|
|
1398
|
+
* with, the format is unknown, or the Web Crypto API is unavailable.
|
|
1399
|
+
*
|
|
1400
|
+
* @param payload : the base64url payload returned by {@link encrypt}
|
|
1401
|
+
* @param password : the password / passphrase used to encrypt
|
|
1402
|
+
* @returns : the original plain text, or undefined on any failure
|
|
1517
1403
|
*/
|
|
1518
|
-
|
|
1519
|
-
|
|
1404
|
+
static async decrypt(payload, password) {
|
|
1405
|
+
if (!payload || !password)
|
|
1406
|
+
return undefined;
|
|
1407
|
+
if (typeof crypto === 'undefined' || !crypto.subtle)
|
|
1408
|
+
return undefined;
|
|
1409
|
+
try {
|
|
1410
|
+
const bytes = this.fromBase64Url(payload);
|
|
1411
|
+
const headerLength = 1 + this.CIPHER_SALT_LENGTH + this.CIPHER_IV_LENGTH;
|
|
1412
|
+
if (bytes.length <= headerLength || bytes[0] !== this.CIPHER_VERSION)
|
|
1413
|
+
return undefined;
|
|
1414
|
+
const salt = bytes.subarray(1, 1 + this.CIPHER_SALT_LENGTH);
|
|
1415
|
+
const iv = bytes.subarray(1 + this.CIPHER_SALT_LENGTH, headerLength);
|
|
1416
|
+
const cipherText = bytes.subarray(headerLength);
|
|
1417
|
+
const key = await this.deriveKey(password, salt);
|
|
1418
|
+
const plain = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv }, key, cipherText);
|
|
1419
|
+
return new TextDecoder().decode(plain);
|
|
1420
|
+
}
|
|
1421
|
+
catch {
|
|
1422
|
+
// Wrong password or tampered payload: GCM authentication failed
|
|
1423
|
+
return undefined;
|
|
1424
|
+
}
|
|
1520
1425
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
/**
|
|
1533
|
-
* Pipe that marks a URL string as a trusted resource URL so Angular does not block it
|
|
1534
|
-
* when bound to attributes such as `[src]` or `[href]` on iframes, objects, or embeds.
|
|
1535
|
-
*
|
|
1536
|
-
* Usage: `<iframe [src]="url | safeUrl"></iframe>`
|
|
1537
|
-
*/
|
|
1538
|
-
class SafeUrlPipe {
|
|
1539
|
-
constructor() {
|
|
1540
|
-
this.sanitizer = inject(DomSanitizer);
|
|
1426
|
+
// #endregion
|
|
1427
|
+
/**
|
|
1428
|
+
* Clone an object (deep copy).
|
|
1429
|
+
* Uses native structuredClone: handles Date, Map, Set, typed arrays and circular references.
|
|
1430
|
+
* @param obj : the object to clone
|
|
1431
|
+
* @returns : a new object
|
|
1432
|
+
*/
|
|
1433
|
+
static clone(obj) {
|
|
1434
|
+
if (obj === null || obj === undefined)
|
|
1435
|
+
return obj;
|
|
1436
|
+
return structuredClone(obj);
|
|
1541
1437
|
}
|
|
1542
1438
|
/**
|
|
1543
|
-
*
|
|
1544
|
-
* @param
|
|
1545
|
-
* @
|
|
1439
|
+
* Creates a deep clone of an object.
|
|
1440
|
+
* @param obj - The source object to clone.
|
|
1441
|
+
* @param dest - Optional pre-allocated destination object to merge the clone into.
|
|
1442
|
+
* @returns A deep copy of `obj`.
|
|
1546
1443
|
*/
|
|
1547
|
-
|
|
1548
|
-
|
|
1444
|
+
static deepClone(obj, dest) {
|
|
1445
|
+
const cloned = structuredClone(obj);
|
|
1446
|
+
if (dest) {
|
|
1447
|
+
Object.assign(dest, cloned);
|
|
1448
|
+
return dest;
|
|
1449
|
+
}
|
|
1450
|
+
return cloned;
|
|
1549
1451
|
}
|
|
1550
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SafeUrlPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
1551
|
-
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: SafeUrlPipe, isStandalone: true, name: "safeUrl" }); }
|
|
1552
|
-
}
|
|
1553
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SafeUrlPipe, decorators: [{
|
|
1554
|
-
type: Pipe,
|
|
1555
|
-
args: [{
|
|
1556
|
-
name: 'safeUrl',
|
|
1557
|
-
standalone: true
|
|
1558
|
-
}]
|
|
1559
|
-
}] });
|
|
1560
|
-
|
|
1561
|
-
/**
|
|
1562
|
-
* Impure pipe that filters an array using a caller-provided predicate function.
|
|
1563
|
-
* Because the pipe is impure it re-evaluates on every change-detection cycle,
|
|
1564
|
-
* which is necessary when the predicate's captured state changes.
|
|
1565
|
-
*
|
|
1566
|
-
* Usage: `*ngFor="let item of items | callback:myFilter"`
|
|
1567
|
-
*/
|
|
1568
|
-
class SearchCallbackPipe {
|
|
1569
1452
|
/**
|
|
1570
|
-
*
|
|
1571
|
-
*
|
|
1572
|
-
* @param items - The source array to filter. May be `undefined`.
|
|
1573
|
-
* @param callback - A predicate function that returns `true` for items to keep.
|
|
1574
|
-
* @returns A new filtered array, the original array when no callback is provided,
|
|
1575
|
-
* or `undefined` when `items` is `undefined`.
|
|
1453
|
+
* Returns `true` when `value` is a syntactically valid UUID string.
|
|
1454
|
+
* @param value - The string to validate.
|
|
1576
1455
|
*/
|
|
1577
|
-
|
|
1578
|
-
if (!
|
|
1579
|
-
return
|
|
1580
|
-
|
|
1456
|
+
static parseUUID(value) {
|
|
1457
|
+
if (!value)
|
|
1458
|
+
return false;
|
|
1459
|
+
let s = value.trim();
|
|
1460
|
+
// Strip a matching wrapper pair, if present
|
|
1461
|
+
if ((s.startsWith('{') && s.endsWith('}')) || (s.startsWith('(') && s.endsWith(')'))) {
|
|
1462
|
+
s = s.slice(1, -1);
|
|
1463
|
+
}
|
|
1464
|
+
return /^[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$/.test(s);
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Returns `true` when `value` is a valid, non-empty (non-zero) UUID.
|
|
1468
|
+
* @param value - The string to validate.
|
|
1469
|
+
*/
|
|
1470
|
+
static parseUUIDNotEmpty(value) {
|
|
1471
|
+
return this.parseUUID(value) && !/^[{(]?0{8}-(?:0{4}-){3}0{12}[)}]?$/.test((value ?? '').trim());
|
|
1581
1472
|
}
|
|
1582
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SearchCallbackPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
1583
|
-
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: SearchCallbackPipe, isStandalone: true, name: "callback", pure: false }); }
|
|
1584
|
-
}
|
|
1585
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SearchCallbackPipe, decorators: [{
|
|
1586
|
-
type: Pipe,
|
|
1587
|
-
args: [{
|
|
1588
|
-
name: 'callback',
|
|
1589
|
-
pure: false,
|
|
1590
|
-
standalone: true
|
|
1591
|
-
}]
|
|
1592
|
-
}] });
|
|
1593
|
-
|
|
1594
|
-
/**
|
|
1595
|
-
* Impure pipe that filters an array of searchable items against a text query.
|
|
1596
|
-
*
|
|
1597
|
-
* Each item is matched either via its `searchBag.name` property (when present)
|
|
1598
|
-
* or by converting the item itself to a lowercase string. The optional `metadata`
|
|
1599
|
-
* argument is updated in-place with the total item count and the filtered count,
|
|
1600
|
-
* making it usable in the template alongside `*ngFor`.
|
|
1601
|
-
*
|
|
1602
|
-
* Usage:
|
|
1603
|
-
* ```html
|
|
1604
|
-
* <div *ngFor="let item of items | search:filterText:meta">...</div>
|
|
1605
|
-
* <div>Showing {{ meta.count }} of {{ meta.total }}</div>
|
|
1606
|
-
* ```
|
|
1607
|
-
*/
|
|
1608
|
-
class SearchFilterPipe {
|
|
1609
1473
|
/**
|
|
1610
|
-
*
|
|
1611
|
-
*
|
|
1612
|
-
* @param items - The source array to filter. May be `undefined`.
|
|
1613
|
-
* @param value - The search text to match against each item. May be `undefined`.
|
|
1614
|
-
* @param metadata - Optional object that is updated with `total` and `count` after filtering.
|
|
1615
|
-
* @returns The filtered array, the original array when no filter text is given,
|
|
1616
|
-
* or `undefined` when `items` is `undefined`.
|
|
1474
|
+
* Return an empty UUID
|
|
1475
|
+
* @returns : the empty UUID
|
|
1617
1476
|
*/
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1477
|
+
static emptyUUID() { return "00000000-0000-0000-0000-000000000000"; }
|
|
1478
|
+
;
|
|
1479
|
+
/**
|
|
1480
|
+
* Create a new UUID
|
|
1481
|
+
* @returns : the string UUID
|
|
1482
|
+
*/
|
|
1483
|
+
static generateUUID() {
|
|
1484
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
1485
|
+
return crypto.randomUUID();
|
|
1486
|
+
}
|
|
1487
|
+
// Fallback (non-secure contexts)
|
|
1488
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
1489
|
+
const r = (Math.random() * 16) | 0;
|
|
1490
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
1491
|
+
return v.toString(16);
|
|
1628
1492
|
});
|
|
1629
|
-
metadata.total = items.length;
|
|
1630
|
-
metadata.count = result.length;
|
|
1631
|
-
return result;
|
|
1632
1493
|
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
/**
|
|
1645
|
-
* Pipe that converts plain-text newlines (`\r\n`, `\r`, `\n`) to HTML `<br>` tags
|
|
1646
|
-
* and marks the result as trusted HTML so Angular does not escape it.
|
|
1647
|
-
*
|
|
1648
|
-
* Usage: `{{ text | formatHtml }}`
|
|
1649
|
-
*/
|
|
1650
|
-
class FormatHtmlPipe {
|
|
1651
|
-
constructor() {
|
|
1652
|
-
this.sanitizer = inject(DomSanitizer);
|
|
1494
|
+
/**
|
|
1495
|
+
* Reconstruct a standard UUID (with dashes) from a 32-char hex string without dashes.
|
|
1496
|
+
* @param value : 32-character hex string
|
|
1497
|
+
* @returns : the formatted UUID or the original string if it doesn't match the expected format
|
|
1498
|
+
*/
|
|
1499
|
+
static inflateUUID(value) {
|
|
1500
|
+
const s = value.trim();
|
|
1501
|
+
if (s.length !== 32)
|
|
1502
|
+
return s;
|
|
1503
|
+
return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20)}`;
|
|
1653
1504
|
}
|
|
1505
|
+
/** Precompiled validation patterns. */
|
|
1506
|
+
static { this.emailRegex = /^(?:[a-z0-9+!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i; }
|
|
1507
|
+
static { this.urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,12})(:\d{1,5})?([/\w .%-]*)?(\?[^\s#]*)?(#\S*)?$/i; }
|
|
1654
1508
|
/**
|
|
1655
|
-
*
|
|
1656
|
-
*
|
|
1657
|
-
* @
|
|
1658
|
-
* @returns A `SafeHtml` value that can be rendered with `[innerHTML]`.
|
|
1509
|
+
* Parse a text and return true if it is a valid email (or empty)
|
|
1510
|
+
* @param value : email
|
|
1511
|
+
* @returns : true if the email is valid or empty
|
|
1659
1512
|
*/
|
|
1660
|
-
|
|
1661
|
-
|
|
1513
|
+
static parseEmail(value) {
|
|
1514
|
+
if (!value)
|
|
1515
|
+
return true;
|
|
1516
|
+
return this.emailRegex.test(value.trim().toLowerCase());
|
|
1662
1517
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
* directly via `imports: [FormatPipe, SafeHtmlPipe, ...]` instead.
|
|
1681
|
-
*
|
|
1682
|
-
* @example
|
|
1683
|
-
* bootstrapApplication(AppComponent, {
|
|
1684
|
-
* providers: [provideArsCore()]
|
|
1685
|
-
* });
|
|
1686
|
-
*/
|
|
1687
|
-
function provideArsCore() {
|
|
1688
|
-
return makeEnvironmentProviders([
|
|
1689
|
-
SearchFilterPipe,
|
|
1690
|
-
SearchCallbackPipe,
|
|
1691
|
-
SafeHtmlPipe,
|
|
1692
|
-
SafeUrlPipe,
|
|
1693
|
-
ReplacePipe,
|
|
1694
|
-
FormatPipe,
|
|
1695
|
-
FormatHtmlPipe
|
|
1696
|
-
]);
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
/**
|
|
1700
|
-
* Creates an array of the given length, filling each slot with the result of `valueFunction`.
|
|
1701
|
-
* @param length - Number of elements to create.
|
|
1702
|
-
* @param valueFunction - Factory called with each index to produce the element value.
|
|
1703
|
-
* @returns Typed array of `length` elements.
|
|
1704
|
-
*/
|
|
1705
|
-
function range(length, valueFunction) {
|
|
1706
|
-
const valuesArray = Array(length);
|
|
1707
|
-
for (let i = 0; i < length; i++) {
|
|
1708
|
-
valuesArray[i] = valueFunction(i);
|
|
1518
|
+
/**
|
|
1519
|
+
* Parse a text containing one or more email addresses separated by `;` or `,`
|
|
1520
|
+
* and return true if all of them are valid (or the text is empty).
|
|
1521
|
+
* Empty items caused by double or trailing separators are ignored.
|
|
1522
|
+
* @param value : the email list to parse (e.g. "a@b.it; c@d.it")
|
|
1523
|
+
* @returns : true if all emails are valid or the value is empty
|
|
1524
|
+
*/
|
|
1525
|
+
static parseEmails(value) {
|
|
1526
|
+
if (!value || value.trim().length === 0)
|
|
1527
|
+
return true;
|
|
1528
|
+
const items = value
|
|
1529
|
+
.split(/[;,]/)
|
|
1530
|
+
.map(e => e.trim().toLowerCase())
|
|
1531
|
+
.filter(e => e.length > 0);
|
|
1532
|
+
if (items.length === 0)
|
|
1533
|
+
return false; // only separators, no addresses
|
|
1534
|
+
return items.every(e => this.emailRegex.test(e));
|
|
1709
1535
|
}
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
if (matDateLocale) {
|
|
1744
|
-
this.setLocale(matDateLocale);
|
|
1536
|
+
/**
|
|
1537
|
+
* Parse a text and return true if it is a valid url (or empty)
|
|
1538
|
+
* @param value : the url to parse
|
|
1539
|
+
* @returns : true if the url is valid or empty
|
|
1540
|
+
*/
|
|
1541
|
+
static parseUrl(value) {
|
|
1542
|
+
if (!value)
|
|
1543
|
+
return true;
|
|
1544
|
+
return this.urlRegex.test(value.trim().toLowerCase());
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Get date parts from a string value
|
|
1548
|
+
* @param value : the string to parse
|
|
1549
|
+
* @returns : an array of numbers with year, month, day
|
|
1550
|
+
*/
|
|
1551
|
+
static getDateParts(value) {
|
|
1552
|
+
if (!value)
|
|
1553
|
+
return undefined;
|
|
1554
|
+
let parts = [];
|
|
1555
|
+
if (value.indexOf("-") !== -1) {
|
|
1556
|
+
const p = value.split("-");
|
|
1557
|
+
if (p.length !== 3)
|
|
1558
|
+
return undefined;
|
|
1559
|
+
parts = [parseInt(p[0]), parseInt(p[1]) - 1, parseInt(p[2])];
|
|
1560
|
+
}
|
|
1561
|
+
else if (value.indexOf("/") !== -1) {
|
|
1562
|
+
const p = value.split("/");
|
|
1563
|
+
if (p.length !== 3)
|
|
1564
|
+
return undefined;
|
|
1565
|
+
parts = [parseInt(p[2]), parseInt(p[1]) - 1, parseInt(p[0])];
|
|
1566
|
+
}
|
|
1567
|
+
else {
|
|
1568
|
+
return undefined;
|
|
1745
1569
|
}
|
|
1570
|
+
return parts;
|
|
1746
1571
|
}
|
|
1747
1572
|
/**
|
|
1748
|
-
*
|
|
1749
|
-
*
|
|
1573
|
+
* Checks whether a value is a valid Date instance.
|
|
1574
|
+
* Note: do not use `date.getTime()` as a truthiness test, it is 0 (falsy)
|
|
1575
|
+
* for the Unix epoch (1970-01-01T00:00:00Z) even though the date is valid.
|
|
1576
|
+
* @param d : the value to check
|
|
1577
|
+
* @returns : true when `d` is a Date representing a valid point in time
|
|
1750
1578
|
*/
|
|
1751
|
-
|
|
1752
|
-
return
|
|
1579
|
+
static isValidDate(d) {
|
|
1580
|
+
return d instanceof Date && !isNaN(d.getTime());
|
|
1753
1581
|
}
|
|
1754
1582
|
/**
|
|
1755
|
-
*
|
|
1756
|
-
* @param
|
|
1583
|
+
* Parse a date
|
|
1584
|
+
* @param value : the value to check
|
|
1585
|
+
* @param locale : the locale to use
|
|
1586
|
+
* @returns : a valid Date object or undefined
|
|
1757
1587
|
*/
|
|
1758
|
-
|
|
1759
|
-
|
|
1588
|
+
static parseDate(value, locale = it) {
|
|
1589
|
+
// No value at all
|
|
1590
|
+
if (!value)
|
|
1591
|
+
return undefined;
|
|
1592
|
+
// A Date value
|
|
1593
|
+
if (value instanceof Date)
|
|
1594
|
+
return this.isValidDate(value) ? value : undefined;
|
|
1595
|
+
// Parse known formats using date-fns
|
|
1596
|
+
let d = parseISO(value);
|
|
1597
|
+
if (this.isValidDate(d) && d.getFullYear() > 1750)
|
|
1598
|
+
return d;
|
|
1599
|
+
d = parse(value, 'dd/MM/yyyy', new Date(), { locale: locale });
|
|
1600
|
+
if (this.isValidDate(d) && d.getFullYear() > 1750)
|
|
1601
|
+
return d;
|
|
1602
|
+
d = parse(value, 'yyyy-MM-dd', new Date(), { locale: locale });
|
|
1603
|
+
if (this.isValidDate(d) && d.getFullYear() > 1750)
|
|
1604
|
+
return d;
|
|
1605
|
+
// Parse values manually
|
|
1606
|
+
const parts = this.getDateParts(value);
|
|
1607
|
+
if (!parts)
|
|
1608
|
+
return undefined;
|
|
1609
|
+
if (parts[0] < 100)
|
|
1610
|
+
parts[0] += 2000;
|
|
1611
|
+
if (isNaN(parts[0]) || parts[0] < 0 || parts[0] < 1750)
|
|
1612
|
+
return undefined;
|
|
1613
|
+
if (isNaN(parts[1]) || parts[1] < 0)
|
|
1614
|
+
return undefined;
|
|
1615
|
+
if (isNaN(parts[2]) || parts[2] < 0)
|
|
1616
|
+
return undefined;
|
|
1617
|
+
if (parts[1] > 11)
|
|
1618
|
+
return undefined;
|
|
1619
|
+
if (parts[1] === 1 && parts[2] > 29)
|
|
1620
|
+
return undefined;
|
|
1621
|
+
else if (parts[1] !== 1 && parts[2] > 31)
|
|
1622
|
+
return undefined;
|
|
1623
|
+
return new TZDate(parts[0], parts[1], parts[2], 12);
|
|
1760
1624
|
}
|
|
1761
1625
|
/**
|
|
1762
|
-
*
|
|
1763
|
-
* @param date
|
|
1626
|
+
* Format a date
|
|
1627
|
+
* @param value : the date or string to format
|
|
1628
|
+
* @param fmt : the DateFormat to use or the string pattern
|
|
1629
|
+
* @param locale : the locale to use (default is IT)
|
|
1630
|
+
* @returns : the formatted string
|
|
1764
1631
|
*/
|
|
1765
|
-
|
|
1766
|
-
|
|
1632
|
+
static formatDate(value, fmt = DateFormat.Short, locale = it) {
|
|
1633
|
+
// No value at all
|
|
1634
|
+
if (!value)
|
|
1635
|
+
return '';
|
|
1636
|
+
// A string
|
|
1637
|
+
if (typeof value === 'string' || value instanceof String)
|
|
1638
|
+
value = this.parseDate(value);
|
|
1639
|
+
// Not a date
|
|
1640
|
+
if (!this.isValidDate(value))
|
|
1641
|
+
return '';
|
|
1642
|
+
// Format
|
|
1643
|
+
switch (fmt) {
|
|
1644
|
+
case DateFormat.Short: return format(value, "dd/MM/yyyy", { locale: locale });
|
|
1645
|
+
case DateFormat.Long: return format(value, "d MMMM yyyy", { locale: locale });
|
|
1646
|
+
case DateFormat.LongWithShortMonth: return format(value, "d MMM yyyy", { locale: locale });
|
|
1647
|
+
case DateFormat.LongWithWeekDay: return format(value, "EEEE, d MMMM yyyy", { locale: locale });
|
|
1648
|
+
case DateFormat.LongWithShortWeekDay: return format(value, "EEE, d MMMM yyyy", { locale: locale });
|
|
1649
|
+
case DateFormat.MonthAndYear: return format(value, "MMM yyyy", { locale: locale });
|
|
1650
|
+
case DateFormat.LongMonthAndYear: return format(value, "MMMM yyyy", { locale: locale });
|
|
1651
|
+
case DateFormat.WeekDay: return format(value, "EEE, d", { locale: locale });
|
|
1652
|
+
case DateFormat.LongWeekDay: return format(value, "EEEE, d", { locale: locale });
|
|
1653
|
+
case DateFormat.DayAndMonth: return format(value, "d MMMM", { locale: locale });
|
|
1654
|
+
case DateFormat.ShortUS: return format(value, "MM/dd/yyyy", { locale: locale });
|
|
1655
|
+
case DateFormat.ShortISO8601: return format(value, "yyyy-MM-dd", { locale: locale });
|
|
1656
|
+
default:
|
|
1657
|
+
return format(value, fmt, { locale: locale });
|
|
1658
|
+
}
|
|
1767
1659
|
}
|
|
1768
1660
|
/**
|
|
1769
|
-
*
|
|
1770
|
-
* @param
|
|
1661
|
+
* Return an italian local date
|
|
1662
|
+
* @param value : the date
|
|
1663
|
+
* @returns : the new date
|
|
1771
1664
|
*/
|
|
1772
|
-
|
|
1773
|
-
|
|
1665
|
+
static toLocalDate(value) {
|
|
1666
|
+
// No value at all
|
|
1667
|
+
if (!value)
|
|
1668
|
+
return undefined;
|
|
1669
|
+
// A string
|
|
1670
|
+
if (typeof value === 'string' || value instanceof String)
|
|
1671
|
+
value = this.parseDate(value);
|
|
1672
|
+
// Not a date
|
|
1673
|
+
if (!this.isValidDate(value))
|
|
1674
|
+
return undefined;
|
|
1675
|
+
// Update date
|
|
1676
|
+
return new TZDate(value, "Europe/Rome");
|
|
1774
1677
|
}
|
|
1775
1678
|
/**
|
|
1776
|
-
*
|
|
1777
|
-
* @param
|
|
1679
|
+
* Update a DateInterval object according to a string
|
|
1680
|
+
* @param value : string value
|
|
1681
|
+
* @param interval : DateInterval value to update
|
|
1682
|
+
* @param end : true if must be updated the first or the end value
|
|
1683
|
+
* @param copy : copy the same value (works only if not end element)
|
|
1778
1684
|
*/
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1685
|
+
static changeDateInterval(value, interval, end = false, copy = false) {
|
|
1686
|
+
if (value) {
|
|
1687
|
+
let year = -1;
|
|
1688
|
+
if (value.length === 4 && (year = parseInt(value)) > 1750) {
|
|
1689
|
+
if (!end) {
|
|
1690
|
+
interval.from = new TZDate(new Date(year, 0, 1), 'Europe/Rome');
|
|
1691
|
+
interval.to = new TZDate(new Date(year, 11, 31), 'Europe/Rome');
|
|
1692
|
+
}
|
|
1693
|
+
else
|
|
1694
|
+
interval.to = new Date(year, 11, 31);
|
|
1695
|
+
}
|
|
1696
|
+
else {
|
|
1697
|
+
const parts = this.getDateParts(value);
|
|
1698
|
+
if (!parts || parts.length !== 3)
|
|
1699
|
+
return;
|
|
1700
|
+
if (isNaN(parts[0]) || parts[0] < 1750)
|
|
1701
|
+
return;
|
|
1702
|
+
if (isNaN(parts[1]) || parts[1] < 0 || parts[1] > 11)
|
|
1703
|
+
return;
|
|
1704
|
+
if (isNaN(parts[2]) || parts[2] < 1 || parts[2] > 31)
|
|
1705
|
+
return;
|
|
1706
|
+
const d = new TZDate(new Date(parts[0], parts[1], parts[2]), 'Europe/Rome');
|
|
1707
|
+
if (end)
|
|
1708
|
+
interval.to = d;
|
|
1709
|
+
else if (copy) {
|
|
1710
|
+
interval.from = d;
|
|
1711
|
+
interval.to = d;
|
|
1712
|
+
}
|
|
1713
|
+
else
|
|
1714
|
+
interval.from = d;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1782
1717
|
}
|
|
1718
|
+
/** Cache of `Intl.NumberFormat` instances (their construction is expensive). */
|
|
1719
|
+
static { this.numberFormatCache = new Map(); }
|
|
1783
1720
|
/**
|
|
1784
|
-
* Returns
|
|
1785
|
-
*
|
|
1721
|
+
* Returns a cached `Intl.NumberFormat` for the given options, creating it on first use.
|
|
1722
|
+
* @param locale - BCP 47 locale tag.
|
|
1723
|
+
* @param options - The `Intl.NumberFormat` options.
|
|
1724
|
+
* @returns A shared formatter instance.
|
|
1786
1725
|
*/
|
|
1787
|
-
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
return
|
|
1795
|
-
if (dtf) {
|
|
1796
|
-
// date-fns doesn't appear to support this functionality.
|
|
1797
|
-
// Fall back to `Intl` on supported browsers.
|
|
1798
|
-
const date = new Date();
|
|
1799
|
-
date.setUTCFullYear(2017, 0, i + 1);
|
|
1800
|
-
date.setUTCHours(0, 0, 0, 0);
|
|
1801
|
-
return dtf.format(date).replace(/[\u200e\u200f]/g, '');
|
|
1802
|
-
}
|
|
1803
|
-
return String(i + 1);
|
|
1804
|
-
});
|
|
1726
|
+
static getNumberFormat(locale, options) {
|
|
1727
|
+
const key = `${locale}|${options.style}|${options.currency ?? ''}|${options.minimumFractionDigits}|${options.maximumFractionDigits}`;
|
|
1728
|
+
let fmt = this.numberFormatCache.get(key);
|
|
1729
|
+
if (!fmt) {
|
|
1730
|
+
fmt = new Intl.NumberFormat(locale, options);
|
|
1731
|
+
this.numberFormatCache.set(key, fmt);
|
|
1732
|
+
}
|
|
1733
|
+
return fmt;
|
|
1805
1734
|
}
|
|
1806
1735
|
/**
|
|
1807
|
-
*
|
|
1808
|
-
* @param
|
|
1736
|
+
* Formats a number using a cached `Intl.NumberFormat`.
|
|
1737
|
+
* @param value - The number to format.
|
|
1738
|
+
* @param decimals - Maximum decimal places (default: `2`).
|
|
1739
|
+
* @param locale - BCP 47 locale tag (default: `'it-IT'`).
|
|
1740
|
+
* @returns The formatted number string.
|
|
1809
1741
|
*/
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
return range(7, i => this.format(new Date(2017, 0, i + 1), pattern));
|
|
1742
|
+
static formatNumber(value, decimals = 2, locale = 'it-IT') {
|
|
1743
|
+
return this.getNumberFormat(locale, { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: decimals }).format(value);
|
|
1813
1744
|
}
|
|
1814
1745
|
/**
|
|
1815
|
-
*
|
|
1816
|
-
* @param
|
|
1746
|
+
* Formats a number as a currency string using a cached `Intl.NumberFormat`.
|
|
1747
|
+
* @param value - The number to format.
|
|
1748
|
+
* @param currency - ISO 4217 currency code (default: `'EUR'`).
|
|
1749
|
+
* @param decimals - Maximum decimal places (default: `2`).
|
|
1750
|
+
* @param locale - BCP 47 locale tag (default: `'it-IT'`).
|
|
1751
|
+
* @returns The formatted currency string.
|
|
1817
1752
|
*/
|
|
1818
|
-
|
|
1819
|
-
return this.
|
|
1753
|
+
static formatCurrency(value, currency = 'EUR', decimals = 2, locale = 'it-IT') {
|
|
1754
|
+
return this.getNumberFormat(locale, { style: 'currency', currency, minimumFractionDigits: 0, maximumFractionDigits: decimals }).format(value);
|
|
1820
1755
|
}
|
|
1821
1756
|
/**
|
|
1822
|
-
*
|
|
1757
|
+
* Percent-encodes a string for safe inclusion in a URL.
|
|
1758
|
+
* @param value - The string to encode.
|
|
1759
|
+
* @returns The encoded string, or `undefined` when `value` is empty.
|
|
1823
1760
|
*/
|
|
1824
|
-
|
|
1825
|
-
return
|
|
1761
|
+
static urlEncode(value) {
|
|
1762
|
+
return value.length > 0
|
|
1763
|
+
? encodeURIComponent(value)
|
|
1764
|
+
: undefined;
|
|
1826
1765
|
}
|
|
1827
1766
|
/**
|
|
1828
|
-
*
|
|
1829
|
-
* @param
|
|
1767
|
+
* Decodes a percent-encoded URL string, treating `+` as a space.
|
|
1768
|
+
* @param value - The encoded string to decode.
|
|
1769
|
+
* @returns The decoded string, or `undefined` when `value` is empty or absent.
|
|
1830
1770
|
*/
|
|
1831
|
-
|
|
1832
|
-
return
|
|
1771
|
+
static urlDecode(value) {
|
|
1772
|
+
return value && value.length > 0
|
|
1773
|
+
? decodeURIComponent(value.replace(/\+/g, '%20'))
|
|
1774
|
+
: undefined;
|
|
1833
1775
|
}
|
|
1834
1776
|
/**
|
|
1835
|
-
*
|
|
1836
|
-
* @param
|
|
1777
|
+
* Reads a query string parameter value from the current page URL.
|
|
1778
|
+
* @param name - The parameter name to look up.
|
|
1779
|
+
* @returns The decoded parameter value, or `undefined` when absent or running server-side.
|
|
1837
1780
|
*/
|
|
1838
|
-
|
|
1839
|
-
|
|
1781
|
+
static getQueryStringValueByName(name) {
|
|
1782
|
+
if (!this.isBrowser())
|
|
1783
|
+
return undefined;
|
|
1784
|
+
const v = new URLSearchParams(window.location.search).get(name);
|
|
1785
|
+
return v && v !== 'null' ? v : undefined;
|
|
1840
1786
|
}
|
|
1841
1787
|
/**
|
|
1842
|
-
*
|
|
1843
|
-
*
|
|
1844
|
-
* @param year - Full four-digit year.
|
|
1845
|
-
* @param month - Zero-based month index (0 = January, 11 = December).
|
|
1846
|
-
* @param date - Day-of-month (1-based).
|
|
1788
|
+
* Generate a password
|
|
1789
|
+
* @returns : the password string
|
|
1847
1790
|
*/
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1791
|
+
static generatePassword(length = 12) {
|
|
1792
|
+
const lower = 'abcdefghijkmnopqrstuvwxyz'; // no 'l' (ambiguous)
|
|
1793
|
+
const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // no 'I'/'O' (ambiguous)
|
|
1794
|
+
const digits = '23456789'; // no '0'/'1' (ambiguous)
|
|
1795
|
+
const symbols = '$!#%&*+-';
|
|
1796
|
+
const all = lower + upper + digits + symbols;
|
|
1797
|
+
// Secure random indices (crypto is available in browsers and Node)
|
|
1798
|
+
const rnd = (max) => {
|
|
1799
|
+
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
|
|
1800
|
+
const buf = new Uint32Array(1);
|
|
1801
|
+
crypto.getRandomValues(buf);
|
|
1802
|
+
return buf[0] % max;
|
|
1803
|
+
}
|
|
1804
|
+
return Math.floor(Math.random() * max);
|
|
1805
|
+
};
|
|
1806
|
+
// Guarantee at least one char per class, fill the rest, then shuffle
|
|
1807
|
+
const chars = [
|
|
1808
|
+
lower[rnd(lower.length)],
|
|
1809
|
+
upper[rnd(upper.length)],
|
|
1810
|
+
digits[rnd(digits.length)],
|
|
1811
|
+
symbols[rnd(symbols.length)],
|
|
1812
|
+
];
|
|
1813
|
+
while (chars.length < Math.max(length, 8)) {
|
|
1814
|
+
chars.push(all[rnd(all.length)]);
|
|
1854
1815
|
}
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
result.setFullYear(year, month, date);
|
|
1859
|
-
result.setHours(0, 0, 0, 0);
|
|
1860
|
-
const result2 = new TZDate(result, 'Europe/Rome');
|
|
1861
|
-
if (result2.getMonth() !== month) {
|
|
1862
|
-
throw Error(`Invalid date "${date}" for month with index "${month}".`);
|
|
1816
|
+
for (let i = chars.length - 1; i > 0; i--) {
|
|
1817
|
+
const j = rnd(i + 1);
|
|
1818
|
+
[chars[i], chars[j]] = [chars[j], chars[i]];
|
|
1863
1819
|
}
|
|
1864
|
-
return
|
|
1865
|
-
}
|
|
1866
|
-
/**
|
|
1867
|
-
* Returns today's date in the local timezone.
|
|
1868
|
-
*/
|
|
1869
|
-
today() {
|
|
1870
|
-
return new Date();
|
|
1820
|
+
return chars.join('');
|
|
1871
1821
|
}
|
|
1872
1822
|
/**
|
|
1873
|
-
*
|
|
1874
|
-
*
|
|
1875
|
-
*
|
|
1876
|
-
* - Existing `Date` instances are cloned.
|
|
1877
|
-
* @param value - The value to parse.
|
|
1878
|
-
* @param parseFormat - A format string or an array of format strings (date-fns tokens).
|
|
1879
|
-
* @returns A valid `Date` in `Europe/Rome`, an invalid sentinel, or `null` for unrecognised input.
|
|
1823
|
+
* Calculate password strength
|
|
1824
|
+
* @param password: the password to evaluate
|
|
1825
|
+
* @returns the password strength info
|
|
1880
1826
|
*/
|
|
1881
|
-
|
|
1882
|
-
if (
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1827
|
+
static calculatePasswordStrength(password) {
|
|
1828
|
+
if (password && password.length > 0) {
|
|
1829
|
+
let score = 0;
|
|
1830
|
+
const suggestions = [];
|
|
1831
|
+
// Length
|
|
1832
|
+
if (password.length >= 10)
|
|
1833
|
+
score++;
|
|
1834
|
+
else
|
|
1835
|
+
suggestions.push('Usa almeno 10 caratteri.');
|
|
1836
|
+
if (password.length >= 12)
|
|
1837
|
+
score++;
|
|
1838
|
+
else if (password.length >= 10)
|
|
1839
|
+
suggestions.push('Considera di usare più di 12 caratteri.');
|
|
1840
|
+
// Lowercase letters
|
|
1841
|
+
if (/[a-z]/.test(password))
|
|
1842
|
+
score++;
|
|
1843
|
+
else
|
|
1844
|
+
suggestions.push('Aggiungi lettere minuscole.');
|
|
1845
|
+
// Uppercase letters
|
|
1846
|
+
if (/[A-Z]/.test(password))
|
|
1847
|
+
score++;
|
|
1848
|
+
else
|
|
1849
|
+
suggestions.push('Aggiungi lettere maiuscole.');
|
|
1850
|
+
// Numbers
|
|
1851
|
+
if (/\d/.test(password))
|
|
1852
|
+
score++;
|
|
1853
|
+
else
|
|
1854
|
+
suggestions.push('Aggiungi numeri.');
|
|
1855
|
+
// Special characters
|
|
1856
|
+
if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password))
|
|
1857
|
+
score++;
|
|
1858
|
+
else
|
|
1859
|
+
suggestions.push('Aggiungi caratteri speciali (!@#$%^&*).');
|
|
1860
|
+
// Common patterns
|
|
1861
|
+
if (/(.)\1{2,}/.test(password)) {
|
|
1862
|
+
score = Math.max(0, score - 1);
|
|
1863
|
+
suggestions.push('Evita di ripetere lo stesso carattere.');
|
|
1886
1864
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1865
|
+
if (/123|abc|qwe/i.test(password)) {
|
|
1866
|
+
score = Math.max(0, score - 1);
|
|
1867
|
+
suggestions.push('Evita sequenze comuni (123, abc, qwe).');
|
|
1868
|
+
}
|
|
1869
|
+
// Label and color
|
|
1870
|
+
let label;
|
|
1871
|
+
let color;
|
|
1872
|
+
let isValid;
|
|
1873
|
+
if (score <= 2) {
|
|
1874
|
+
label = 'Molto debole';
|
|
1875
|
+
color = '#f44336';
|
|
1876
|
+
isValid = false;
|
|
1877
|
+
}
|
|
1878
|
+
else if (score <= 3) {
|
|
1879
|
+
label = 'Debole';
|
|
1880
|
+
color = '#ff9800';
|
|
1881
|
+
isValid = false;
|
|
1882
|
+
}
|
|
1883
|
+
else if (score <= 4) {
|
|
1884
|
+
label = 'Media';
|
|
1885
|
+
color = '#ffc107';
|
|
1886
|
+
isValid = true;
|
|
1890
1887
|
}
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
}
|
|
1888
|
+
else if (score <= 5) {
|
|
1889
|
+
label = 'Forte';
|
|
1890
|
+
color = '#8bc34a';
|
|
1891
|
+
isValid = true;
|
|
1896
1892
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
return this.clone(value);
|
|
1904
|
-
}
|
|
1905
|
-
return null;
|
|
1906
|
-
}
|
|
1907
|
-
/**
|
|
1908
|
-
* Formats a `Date` using the given date-fns display format string.
|
|
1909
|
-
* Throws an `Error` when `date` is not valid.
|
|
1910
|
-
* @param date - The date to format.
|
|
1911
|
-
* @param displayFormat - A date-fns format string (e.g. `'P'`, `'LLL uuuu'`).
|
|
1912
|
-
*/
|
|
1913
|
-
format(date, displayFormat) {
|
|
1914
|
-
if (!this.isValid(date)) {
|
|
1915
|
-
throw Error('DateFnsAdapter: Cannot format invalid date.');
|
|
1893
|
+
else {
|
|
1894
|
+
label = 'Molto forte';
|
|
1895
|
+
color = '#4caf50';
|
|
1896
|
+
isValid = true;
|
|
1897
|
+
}
|
|
1898
|
+
return { score, label, color, suggestions, isValid };
|
|
1916
1899
|
}
|
|
1917
|
-
|
|
1900
|
+
else
|
|
1901
|
+
return {
|
|
1902
|
+
score: 0,
|
|
1903
|
+
isValid: false,
|
|
1904
|
+
suggestions: []
|
|
1905
|
+
};
|
|
1918
1906
|
}
|
|
1919
1907
|
/**
|
|
1920
|
-
*
|
|
1921
|
-
* @
|
|
1922
|
-
* @param years - Number of years to add (can be negative).
|
|
1908
|
+
* Check if current browser supports touch
|
|
1909
|
+
* @returns : true if the display is touchable
|
|
1923
1910
|
*/
|
|
1924
|
-
|
|
1925
|
-
return
|
|
1911
|
+
static isTouchable() {
|
|
1912
|
+
return (this.isBrowser() && ("ontouchstart" in window ||
|
|
1913
|
+
navigator.maxTouchPoints > 0));
|
|
1926
1914
|
}
|
|
1927
1915
|
/**
|
|
1928
|
-
*
|
|
1929
|
-
*
|
|
1930
|
-
* @
|
|
1916
|
+
* This check will prevent 'window' logic to be executed
|
|
1917
|
+
* while executing the server rendering
|
|
1918
|
+
* @returns : true if using the browser
|
|
1931
1919
|
*/
|
|
1932
|
-
|
|
1933
|
-
return
|
|
1920
|
+
static isBrowser() {
|
|
1921
|
+
return typeof (window) !== 'undefined';
|
|
1934
1922
|
}
|
|
1935
1923
|
/**
|
|
1936
|
-
*
|
|
1937
|
-
* @param
|
|
1938
|
-
* @
|
|
1924
|
+
* Convert folders in a tree of Node object.
|
|
1925
|
+
* @param folders : the subfolders group or null to root
|
|
1926
|
+
* @returns : a node list
|
|
1939
1927
|
*/
|
|
1940
|
-
|
|
1941
|
-
return
|
|
1928
|
+
static toNodes(folders) {
|
|
1929
|
+
return this._toNodes(folders, undefined);
|
|
1942
1930
|
}
|
|
1943
1931
|
/**
|
|
1944
|
-
*
|
|
1945
|
-
* @param
|
|
1932
|
+
* Convert folders in a tree of Node object.
|
|
1933
|
+
* @param folders : the children group or null to root
|
|
1934
|
+
* @param parent : the parent node
|
|
1935
|
+
* @returns : a node list
|
|
1946
1936
|
*/
|
|
1947
|
-
|
|
1948
|
-
|
|
1937
|
+
static _toNodes(folders, parent) {
|
|
1938
|
+
const nodes = [];
|
|
1939
|
+
folders.forEach((n) => {
|
|
1940
|
+
const node = {
|
|
1941
|
+
id: n.id,
|
|
1942
|
+
name: n.name,
|
|
1943
|
+
count: n.count,
|
|
1944
|
+
parent: parent,
|
|
1945
|
+
children: undefined,
|
|
1946
|
+
bag: n,
|
|
1947
|
+
};
|
|
1948
|
+
const children = n.children ?? n.subFolders ?? [];
|
|
1949
|
+
if (children.length > 0) {
|
|
1950
|
+
node.children = this._toNodes(children, node);
|
|
1951
|
+
}
|
|
1952
|
+
nodes.push(node);
|
|
1953
|
+
});
|
|
1954
|
+
return nodes;
|
|
1949
1955
|
}
|
|
1950
1956
|
/**
|
|
1951
|
-
* Returns
|
|
1952
|
-
*
|
|
1953
|
-
*
|
|
1954
|
-
* @
|
|
1957
|
+
* Returns an array of individual power-of-2 flag values that are set in `value`.
|
|
1958
|
+
* @param value - The bitmask to decompose.
|
|
1959
|
+
* @param max - Upper-bound exponent: checks flags from `1` up to `1 << max` (default: `30`).
|
|
1960
|
+
* @returns Array of set flag values, or an empty array when `value` is `0`.
|
|
1955
1961
|
*/
|
|
1956
|
-
|
|
1957
|
-
if (
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1962
|
+
static getFlags(value, max = 30) {
|
|
1963
|
+
if (value !== 0) {
|
|
1964
|
+
const items = [];
|
|
1965
|
+
const top = Math.min(max, 30); // beyond 30 the shift overflows into the sign bit
|
|
1966
|
+
for (let exp = 0; exp <= top; exp++) {
|
|
1967
|
+
const v = 1 << exp;
|
|
1968
|
+
if ((value & v) === v)
|
|
1969
|
+
items.push(v);
|
|
1964
1970
|
}
|
|
1971
|
+
return items;
|
|
1965
1972
|
}
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
/**
|
|
1969
|
-
* Returns `true` when `obj` is an instance of `Date`.
|
|
1970
|
-
* @param obj - The object to test.
|
|
1971
|
-
*/
|
|
1972
|
-
isDateInstance(obj) {
|
|
1973
|
-
return isDate(obj);
|
|
1974
|
-
}
|
|
1975
|
-
/**
|
|
1976
|
-
* Returns `true` when `date` represents a valid point in time.
|
|
1977
|
-
* @param date - The date to validate.
|
|
1978
|
-
*/
|
|
1979
|
-
isValid(date) {
|
|
1980
|
-
return isValid(date);
|
|
1973
|
+
else
|
|
1974
|
+
return [];
|
|
1981
1975
|
}
|
|
1976
|
+
/** Cache for resolved color luminance results. */
|
|
1977
|
+
static { this.colorLightCache = new Map(); }
|
|
1982
1978
|
/**
|
|
1983
|
-
*
|
|
1979
|
+
* Check if a color is light or dark
|
|
1980
|
+
* @param color : the color
|
|
1981
|
+
* @param minimumLuminance : the lumimance to consider
|
|
1982
|
+
* @returns true if the color is light
|
|
1984
1983
|
*/
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
*
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
{
|
|
2008
|
-
provide: DateAdapter,
|
|
2009
|
-
useClass: DateFnsAdapter,
|
|
2010
|
-
deps: [MAT_DATE_LOCALE],
|
|
2011
|
-
},
|
|
2012
|
-
{ provide: MAT_DATE_FORMATS, useValue: MAT_DATE_FNS_FORMATS }
|
|
2013
|
-
]);
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
/**
|
|
2017
|
-
* Directive that moves browser focus to the host element after the first render cycle.
|
|
2018
|
-
* Apply `autoFocus` to any focusable element to set focus automatically on initialisation.
|
|
2019
|
-
*/
|
|
2020
|
-
class AutoFocusDirective {
|
|
2021
|
-
constructor() {
|
|
2022
|
-
this.elementRef = inject(ElementRef);
|
|
2023
|
-
afterNextRender(() => {
|
|
2024
|
-
this.elementRef.nativeElement?.focus();
|
|
2025
|
-
});
|
|
2026
|
-
}
|
|
2027
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: AutoFocusDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
2028
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: AutoFocusDirective, isStandalone: true, selector: "[autoFocus]", ngImport: i0 }); }
|
|
2029
|
-
}
|
|
2030
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: AutoFocusDirective, decorators: [{
|
|
2031
|
-
type: Directive,
|
|
2032
|
-
args: [{
|
|
2033
|
-
selector: '[autoFocus]',
|
|
2034
|
-
standalone: true
|
|
2035
|
-
}]
|
|
2036
|
-
}], ctorParameters: () => [] });
|
|
2037
|
-
|
|
2038
|
-
class FileInfo {
|
|
2039
|
-
isValid() {
|
|
2040
|
-
return this.valid;
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
|
-
class ValueModel {
|
|
2044
|
-
}
|
|
2045
|
-
class IDModel {
|
|
2046
|
-
}
|
|
2047
|
-
class GroupModel {
|
|
2048
|
-
}
|
|
2049
|
-
class DeleteModel extends GroupModel {
|
|
2050
|
-
}
|
|
2051
|
-
class RelationModel {
|
|
2052
|
-
}
|
|
2053
|
-
class UpdateRelationsModel {
|
|
2054
|
-
}
|
|
2055
|
-
class QueryModel {
|
|
2056
|
-
}
|
|
2057
|
-
class ImportModel {
|
|
2058
|
-
}
|
|
2059
|
-
class DateInterval {
|
|
2060
|
-
get fromAsDate() {
|
|
2061
|
-
if (this.from) {
|
|
2062
|
-
if (!(this.from instanceof Date)) {
|
|
2063
|
-
this.from = new Date(this.from);
|
|
2064
|
-
}
|
|
2065
|
-
if (this.from) {
|
|
2066
|
-
return new Date(this.from.getFullYear(), this.from.getMonth(), this.from.getDate(), 2, 0, 0);
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
return undefined;
|
|
2070
|
-
}
|
|
2071
|
-
get toAsDate() {
|
|
2072
|
-
if (this.to) {
|
|
2073
|
-
if (!(this.to instanceof Date)) {
|
|
2074
|
-
this.to = new Date(this.to);
|
|
2075
|
-
}
|
|
2076
|
-
if (this.to) {
|
|
2077
|
-
return new Date(this.to.getFullYear(), this.to.getMonth(), this.to.getDate(), 2, 0, 0);
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
return undefined;
|
|
2081
|
-
}
|
|
2082
|
-
constructor(from, to) {
|
|
2083
|
-
this.from = from;
|
|
2084
|
-
this.to = to;
|
|
2085
|
-
}
|
|
2086
|
-
clear() {
|
|
2087
|
-
this.from = undefined;
|
|
2088
|
-
this.to = undefined;
|
|
1984
|
+
static isColorLight(color, minimumLuminance = 186) {
|
|
1985
|
+
if (!this.isBrowser())
|
|
1986
|
+
return true; // SSR fallback
|
|
1987
|
+
const cacheKey = `${color}|${minimumLuminance}`;
|
|
1988
|
+
const cached = this.colorLightCache.get(cacheKey);
|
|
1989
|
+
if (cached !== undefined)
|
|
1990
|
+
return cached;
|
|
1991
|
+
const tempDiv = document.createElement('div');
|
|
1992
|
+
tempDiv.style.color = color;
|
|
1993
|
+
document.body.appendChild(tempDiv);
|
|
1994
|
+
const rgb = getComputedStyle(tempDiv).color;
|
|
1995
|
+
document.body.removeChild(tempDiv);
|
|
1996
|
+
const result = rgb.match(/\d+/g);
|
|
1997
|
+
let light = true; // fallback
|
|
1998
|
+
if (result && result.length >= 3) {
|
|
1999
|
+
const r = parseInt(result[0], 10);
|
|
2000
|
+
const g = parseInt(result[1], 10);
|
|
2001
|
+
const b = parseInt(result[2], 10);
|
|
2002
|
+
light = (0.299 * r + 0.587 * g + 0.114 * b) > minimumLuminance;
|
|
2003
|
+
}
|
|
2004
|
+
this.colorLightCache.set(cacheKey, light);
|
|
2005
|
+
return light;
|
|
2089
2006
|
}
|
|
2090
2007
|
}
|
|
2091
2008
|
|
|
@@ -2097,7 +2014,7 @@ class DateInterval {
|
|
|
2097
2014
|
class DateIntervalChangeDirective {
|
|
2098
2015
|
constructor() {
|
|
2099
2016
|
/** The date interval model to update when the input value changes. */
|
|
2100
|
-
this.dateIntervalChange = input(new DateInterval(
|
|
2017
|
+
this.dateIntervalChange = input(new DateInterval(), /* @ts-ignore */
|
|
2101
2018
|
...(ngDevMode ? [{ debugName: "dateIntervalChange" }] : /* istanbul ignore next */ []));
|
|
2102
2019
|
/** When `true`, the directive updates the interval's end date; otherwise the start date. */
|
|
2103
2020
|
this.end = input(false, /* @ts-ignore */
|
|
@@ -2114,19 +2031,27 @@ class DateIntervalChangeDirective {
|
|
|
2114
2031
|
});
|
|
2115
2032
|
}
|
|
2116
2033
|
/**
|
|
2117
|
-
* Handles `
|
|
2118
|
-
*
|
|
2034
|
+
* Handles `keydown` events on the host element.
|
|
2035
|
+
* The space key must be blocked HERE: by the time `keyup` fires the character
|
|
2036
|
+
* has already been inserted into the input, so `preventDefault` on `keyup`
|
|
2037
|
+
* cannot stop it.
|
|
2119
2038
|
* @param e - The keyboard event emitted by the host input.
|
|
2120
2039
|
*/
|
|
2121
|
-
|
|
2040
|
+
onKeydown(e) {
|
|
2122
2041
|
if (e.key === ' ') {
|
|
2123
2042
|
e.preventDefault();
|
|
2124
2043
|
e.stopPropagation();
|
|
2125
2044
|
}
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Handles `keyup` events on the host element and forwards them to the debounce pipeline.
|
|
2048
|
+
* @param e - The keyboard event emitted by the host input.
|
|
2049
|
+
*/
|
|
2050
|
+
onKeyup(e) {
|
|
2126
2051
|
this.subject.next(e);
|
|
2127
2052
|
}
|
|
2128
2053
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DateIntervalChangeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
2129
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: DateIntervalChangeDirective, isStandalone: true, selector: "[dateIntervalChange]", inputs: { dateIntervalChange: { classPropertyName: "dateIntervalChange", publicName: "dateIntervalChange", isSignal: true, isRequired: false, transformFunction: null }, end: { classPropertyName: "end", publicName: "end", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "keyup": "onKeyup($event)" } }, ngImport: i0 }); }
|
|
2054
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: DateIntervalChangeDirective, isStandalone: true, selector: "[dateIntervalChange]", inputs: { dateIntervalChange: { classPropertyName: "dateIntervalChange", publicName: "dateIntervalChange", isSignal: true, isRequired: false, transformFunction: null }, end: { classPropertyName: "end", publicName: "end", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "keydown": "onKeydown($event)", "keyup": "onKeyup($event)" } }, ngImport: i0 }); }
|
|
2130
2055
|
}
|
|
2131
2056
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DateIntervalChangeDirective, decorators: [{
|
|
2132
2057
|
type: Directive,
|
|
@@ -2134,7 +2059,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
2134
2059
|
selector: '[dateIntervalChange]',
|
|
2135
2060
|
standalone: true,
|
|
2136
2061
|
}]
|
|
2137
|
-
}], ctorParameters: () => [], propDecorators: { dateIntervalChange: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateIntervalChange", required: false }] }], end: [{ type: i0.Input, args: [{ isSignal: true, alias: "end", required: false }] }],
|
|
2062
|
+
}], ctorParameters: () => [], propDecorators: { dateIntervalChange: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateIntervalChange", required: false }] }], end: [{ type: i0.Input, args: [{ isSignal: true, alias: "end", required: false }] }], onKeydown: [{
|
|
2063
|
+
type: HostListener,
|
|
2064
|
+
args: ['keydown', ['$event']]
|
|
2065
|
+
}], onKeyup: [{
|
|
2138
2066
|
type: HostListener,
|
|
2139
2067
|
args: ['keyup', ['$event']]
|
|
2140
2068
|
}] } });
|
|
@@ -2163,6 +2091,7 @@ class CopyClipboardDirective {
|
|
|
2163
2091
|
if (SystemUtils.isBrowser()) {
|
|
2164
2092
|
const listener = (clipEvent) => {
|
|
2165
2093
|
clipEvent.clipboardData?.setData('text/html', payload);
|
|
2094
|
+
clipEvent.clipboardData?.setData('text/plain', payload);
|
|
2166
2095
|
clipEvent.preventDefault();
|
|
2167
2096
|
this.copied.emit(payload);
|
|
2168
2097
|
};
|
|
@@ -2230,17 +2159,37 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
2230
2159
|
/**
|
|
2231
2160
|
* Directive that validates that the host control's value equals the value of another control.
|
|
2232
2161
|
* Bind `[equals]="otherControl"`.
|
|
2162
|
+
*
|
|
2163
|
+
* The host control is re-validated whenever the OTHER control's value changes:
|
|
2164
|
+
* without this, typing a new password AFTER the confirmation field was filled
|
|
2165
|
+
* left the form incorrectly valid (the classic password/confirm bug).
|
|
2233
2166
|
*/
|
|
2234
2167
|
class EqualsValidatorDirective {
|
|
2235
2168
|
constructor() {
|
|
2236
2169
|
/** The control whose value must match the host control's value. */
|
|
2237
2170
|
this.equals = input(undefined, /* @ts-ignore */
|
|
2238
2171
|
...(ngDevMode ? [{ debugName: "equals" }] : /* istanbul ignore next */ []));
|
|
2172
|
+
// Re-subscribe whenever the [equals] binding points to a different control,
|
|
2173
|
+
// and trigger host re-validation on every change of the other control.
|
|
2174
|
+
effect(() => {
|
|
2175
|
+
const other = this.equals();
|
|
2176
|
+
this.subscription?.unsubscribe();
|
|
2177
|
+
this.subscription = other?.valueChanges.subscribe(() => this.onValidatorChange?.());
|
|
2178
|
+
});
|
|
2179
|
+
inject(DestroyRef).onDestroy(() => this.subscription?.unsubscribe());
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* Registers the callback Angular invokes to re-run this validator.
|
|
2183
|
+
* @param fn - The revalidation callback provided by the forms API.
|
|
2184
|
+
*/
|
|
2185
|
+
registerOnValidatorChange(fn) {
|
|
2186
|
+
this.onValidatorChange = fn;
|
|
2239
2187
|
}
|
|
2240
2188
|
/**
|
|
2241
2189
|
* Validates that the host control value equals the bound control's value.
|
|
2242
2190
|
* Returns `null` (valid) when no control is bound.
|
|
2243
2191
|
* @param control - The form control to validate.
|
|
2192
|
+
* @returns `null` when valid, `{ equals: ... }` otherwise.
|
|
2244
2193
|
*/
|
|
2245
2194
|
validate(control) {
|
|
2246
2195
|
const eq = this.equals();
|
|
@@ -2270,7 +2219,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
2270
2219
|
],
|
|
2271
2220
|
standalone: true,
|
|
2272
2221
|
}]
|
|
2273
|
-
}], propDecorators: { equals: [{ type: i0.Input, args: [{ isSignal: true, alias: "equals", required: false }] }] } });
|
|
2222
|
+
}], ctorParameters: () => [], propDecorators: { equals: [{ type: i0.Input, args: [{ isSignal: true, alias: "equals", required: false }] }] } });
|
|
2274
2223
|
|
|
2275
2224
|
/**
|
|
2276
2225
|
* Directive that validates a file size against configurable minimum and maximum bounds.
|
|
@@ -2454,18 +2403,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
2454
2403
|
/**
|
|
2455
2404
|
* Directive that validates that the host control's value is different from another control's value.
|
|
2456
2405
|
* Bind `[notEqual]="otherControl"`.
|
|
2406
|
+
*
|
|
2407
|
+
* The host control is re-validated whenever the OTHER control's value changes,
|
|
2408
|
+
* so editing either field keeps both error states consistent.
|
|
2457
2409
|
*/
|
|
2458
2410
|
class NotEqualValidatorDirective {
|
|
2459
2411
|
constructor() {
|
|
2460
2412
|
/** The control whose value must differ from the host control's value. */
|
|
2461
2413
|
this.notEqual = input(undefined, /* @ts-ignore */
|
|
2462
2414
|
...(ngDevMode ? [{ debugName: "notEqual" }] : /* istanbul ignore next */ []));
|
|
2415
|
+
effect(() => {
|
|
2416
|
+
const other = this.notEqual();
|
|
2417
|
+
this.subscription?.unsubscribe();
|
|
2418
|
+
this.subscription = other?.valueChanges.subscribe(() => this.onValidatorChange?.());
|
|
2419
|
+
});
|
|
2420
|
+
inject(DestroyRef).onDestroy(() => this.subscription?.unsubscribe());
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Registers the callback Angular invokes to re-run this validator.
|
|
2424
|
+
* @param fn - The revalidation callback provided by the forms API.
|
|
2425
|
+
*/
|
|
2426
|
+
registerOnValidatorChange(fn) {
|
|
2427
|
+
this.onValidatorChange = fn;
|
|
2463
2428
|
}
|
|
2464
2429
|
/**
|
|
2465
2430
|
* Validates that the host control value is not equal to the bound control's value.
|
|
2466
2431
|
* Also clears the `notequal` error on the other control when the host becomes valid.
|
|
2467
2432
|
* Returns `null` (valid) when no control is bound.
|
|
2468
2433
|
* @param control - The form control to validate.
|
|
2434
|
+
* @returns `null` when valid, `{ notequal: true }` otherwise.
|
|
2469
2435
|
*/
|
|
2470
2436
|
validate(control) {
|
|
2471
2437
|
const notEqual = this.notEqual();
|
|
@@ -2477,7 +2443,8 @@ class NotEqualValidatorDirective {
|
|
|
2477
2443
|
control.markAsTouched();
|
|
2478
2444
|
}
|
|
2479
2445
|
else if (notEqual.hasError('notequal')) {
|
|
2480
|
-
|
|
2446
|
+
const { notequal: _removed, ...rest } = notEqual.errors ?? {};
|
|
2447
|
+
notEqual.setErrors(Object.keys(rest).length > 0 ? rest : null);
|
|
2481
2448
|
notEqual.updateValueAndValidity({ onlySelf: true, emitEvent: false });
|
|
2482
2449
|
notEqual.markAsTouched();
|
|
2483
2450
|
notEqual.markAsDirty();
|
|
@@ -2506,7 +2473,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
2506
2473
|
],
|
|
2507
2474
|
standalone: true,
|
|
2508
2475
|
}]
|
|
2509
|
-
}], propDecorators: { notEqual: [{ type: i0.Input, args: [{ isSignal: true, alias: "notEqual", required: false }] }] } });
|
|
2476
|
+
}], ctorParameters: () => [], propDecorators: { notEqual: [{ type: i0.Input, args: [{ isSignal: true, alias: "notEqual", required: false }] }] } });
|
|
2510
2477
|
|
|
2511
2478
|
/**
|
|
2512
2479
|
* Directive that validates that a control value is not a future date.
|
|
@@ -2522,8 +2489,11 @@ class NotFutureValidatorDirective {
|
|
|
2522
2489
|
const input = control.value;
|
|
2523
2490
|
if (!input || input.length === 0)
|
|
2524
2491
|
return null;
|
|
2492
|
+
const parsed = SystemUtils.parseDate(input);
|
|
2493
|
+
if (!parsed)
|
|
2494
|
+
return { notFuture: "Non valido." };
|
|
2525
2495
|
const today = endOfDay(new Date());
|
|
2526
|
-
const d = endOfDay(
|
|
2496
|
+
const d = endOfDay(parsed);
|
|
2527
2497
|
return d <= today ? null : { notFuture: "Non valido." };
|
|
2528
2498
|
}
|
|
2529
2499
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: NotFutureValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
@@ -2635,7 +2605,10 @@ class SqlDateValidatorDirective {
|
|
|
2635
2605
|
const input = control.value;
|
|
2636
2606
|
if (!input || input.length === 0)
|
|
2637
2607
|
return null;
|
|
2638
|
-
const
|
|
2608
|
+
const parsed = SystemUtils.parseDate(input);
|
|
2609
|
+
if (!parsed)
|
|
2610
|
+
return { sqlDate: "Non valido." };
|
|
2611
|
+
const d = endOfDay(parsed);
|
|
2639
2612
|
return d.getFullYear() > 1750 ? null : { sqlDate: "Non valido." };
|
|
2640
2613
|
}
|
|
2641
2614
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SqlDateValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
@@ -2681,13 +2654,13 @@ class TimeValidatorDirective {
|
|
|
2681
2654
|
const p = value.split(':');
|
|
2682
2655
|
if (p.length !== 2)
|
|
2683
2656
|
return -1;
|
|
2684
|
-
const hh = parseInt(p[0]);
|
|
2685
|
-
if (hh < 0 || hh > 23)
|
|
2657
|
+
const hh = parseInt(p[0], 10);
|
|
2658
|
+
if (isNaN(hh) || hh < 0 || hh > 23)
|
|
2686
2659
|
return -1;
|
|
2687
|
-
const mm = parseInt(p[1]);
|
|
2688
|
-
if (mm < 0 || mm > 59)
|
|
2660
|
+
const mm = parseInt(p[1], 10);
|
|
2661
|
+
if (isNaN(mm) || mm < 0 || mm > 59)
|
|
2689
2662
|
return -1;
|
|
2690
|
-
return
|
|
2663
|
+
return hh * 100 + mm;
|
|
2691
2664
|
}
|
|
2692
2665
|
/**
|
|
2693
2666
|
* Validates that the control value is a valid time string and, when slots are configured,
|
|
@@ -2735,155 +2708,432 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
2735
2708
|
],
|
|
2736
2709
|
standalone: true,
|
|
2737
2710
|
}]
|
|
2738
|
-
}], propDecorators: { slots: [{ type: i0.Input, args: [{ isSignal: true, alias: "slots", required: false }] }] } });
|
|
2711
|
+
}], propDecorators: { slots: [{ type: i0.Input, args: [{ isSignal: true, alias: "slots", required: false }] }] } });
|
|
2712
|
+
|
|
2713
|
+
/**
|
|
2714
|
+
* Directive that validates a control value as a well-formed URL.
|
|
2715
|
+
* Apply `url` to a text input that expects a URL.
|
|
2716
|
+
*/
|
|
2717
|
+
class UrlValidatorDirective {
|
|
2718
|
+
/**
|
|
2719
|
+
* Validates that the control value is a well-formed URL.
|
|
2720
|
+
* Returns `null` (valid) when the control is empty.
|
|
2721
|
+
* @param control - The form control to validate.
|
|
2722
|
+
*/
|
|
2723
|
+
validate(control) {
|
|
2724
|
+
const input = control.value;
|
|
2725
|
+
if (!input || input.length === 0)
|
|
2726
|
+
return null;
|
|
2727
|
+
return SystemUtils.parseUrl(input) ? null : { url: "Non valido." };
|
|
2728
|
+
}
|
|
2729
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: UrlValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
2730
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: UrlValidatorDirective, isStandalone: true, selector: "[url]", providers: [
|
|
2731
|
+
{
|
|
2732
|
+
provide: NG_VALIDATORS,
|
|
2733
|
+
useExisting: forwardRef(() => UrlValidatorDirective),
|
|
2734
|
+
multi: true,
|
|
2735
|
+
},
|
|
2736
|
+
], ngImport: i0 }); }
|
|
2737
|
+
}
|
|
2738
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: UrlValidatorDirective, decorators: [{
|
|
2739
|
+
type: Directive,
|
|
2740
|
+
args: [{
|
|
2741
|
+
selector: "[url]",
|
|
2742
|
+
providers: [
|
|
2743
|
+
{
|
|
2744
|
+
provide: NG_VALIDATORS,
|
|
2745
|
+
useExisting: forwardRef(() => UrlValidatorDirective),
|
|
2746
|
+
multi: true,
|
|
2747
|
+
},
|
|
2748
|
+
],
|
|
2749
|
+
standalone: true,
|
|
2750
|
+
}]
|
|
2751
|
+
}] });
|
|
2752
|
+
|
|
2753
|
+
/**
|
|
2754
|
+
* Directive that validates a control using the host object's `isValid()` method
|
|
2755
|
+
* or a boolean expression passed via `[validIf]`.
|
|
2756
|
+
*/
|
|
2757
|
+
class ValidIfDirective {
|
|
2758
|
+
constructor() {
|
|
2759
|
+
/** When `true`, the control is considered valid regardless of the bound value. */
|
|
2760
|
+
this.validIf = input(false, /* @ts-ignore */
|
|
2761
|
+
...(ngDevMode ? [{ debugName: "validIf" }] : /* istanbul ignore next */ []));
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* Validates the control value against a boolean flag or the value's own `isValid()` method.
|
|
2765
|
+
* @param control - The form control to validate.
|
|
2766
|
+
*/
|
|
2767
|
+
validate(control) {
|
|
2768
|
+
let isValid = false;
|
|
2769
|
+
const c = control.value ? control.value : null;
|
|
2770
|
+
if (!c) {
|
|
2771
|
+
isValid = this.validIf() === true;
|
|
2772
|
+
}
|
|
2773
|
+
else {
|
|
2774
|
+
try {
|
|
2775
|
+
isValid = c.isValid();
|
|
2776
|
+
}
|
|
2777
|
+
catch { }
|
|
2778
|
+
}
|
|
2779
|
+
return isValid ? null : { validIf: "Non valido." };
|
|
2780
|
+
}
|
|
2781
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ValidIfDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
2782
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: ValidIfDirective, isStandalone: true, selector: "[validIf]", inputs: { validIf: { classPropertyName: "validIf", publicName: "validIf", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
2783
|
+
{
|
|
2784
|
+
provide: NG_VALIDATORS,
|
|
2785
|
+
useExisting: forwardRef(() => ValidIfDirective),
|
|
2786
|
+
multi: true,
|
|
2787
|
+
},
|
|
2788
|
+
], ngImport: i0 }); }
|
|
2789
|
+
}
|
|
2790
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ValidIfDirective, decorators: [{
|
|
2791
|
+
type: Directive,
|
|
2792
|
+
args: [{
|
|
2793
|
+
selector: "[validIf]",
|
|
2794
|
+
providers: [
|
|
2795
|
+
{
|
|
2796
|
+
provide: NG_VALIDATORS,
|
|
2797
|
+
useExisting: forwardRef(() => ValidIfDirective),
|
|
2798
|
+
multi: true,
|
|
2799
|
+
},
|
|
2800
|
+
],
|
|
2801
|
+
standalone: true,
|
|
2802
|
+
}]
|
|
2803
|
+
}], propDecorators: { validIf: [{ type: i0.Input, args: [{ isSignal: true, alias: "validIf", required: false }] }] } });
|
|
2804
|
+
|
|
2805
|
+
/**
|
|
2806
|
+
* Directive that delegates validation to an externally provided validator function.
|
|
2807
|
+
* Bind `[validator]="myFn"` where `myFn` is `(c: AbstractControl) => ValidationErrors | null`.
|
|
2808
|
+
*/
|
|
2809
|
+
class ValidatorDirective {
|
|
2810
|
+
constructor() {
|
|
2811
|
+
/** The custom validator function to apply. */
|
|
2812
|
+
this.validator = input(undefined, /* @ts-ignore */
|
|
2813
|
+
...(ngDevMode ? [{ debugName: "validator" }] : /* istanbul ignore next */ []));
|
|
2814
|
+
}
|
|
2815
|
+
/**
|
|
2816
|
+
* Invokes the provided validator function against the given control.
|
|
2817
|
+
* Returns `null` (valid) when no function is bound.
|
|
2818
|
+
* @param control - The form control to validate.
|
|
2819
|
+
*/
|
|
2820
|
+
validate(control) {
|
|
2821
|
+
const fn = this.validator();
|
|
2822
|
+
return fn ? fn(control) : null;
|
|
2823
|
+
}
|
|
2824
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
2825
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: ValidatorDirective, isStandalone: true, selector: "[validator]", inputs: { validator: { classPropertyName: "validator", publicName: "validator", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: NG_VALIDATORS, useExisting: ValidatorDirective, multi: true }], ngImport: i0 }); }
|
|
2826
|
+
}
|
|
2827
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ValidatorDirective, decorators: [{
|
|
2828
|
+
type: Directive,
|
|
2829
|
+
args: [{
|
|
2830
|
+
selector: '[validator]',
|
|
2831
|
+
providers: [{ provide: NG_VALIDATORS, useExisting: ValidatorDirective, multi: true }],
|
|
2832
|
+
standalone: true,
|
|
2833
|
+
}]
|
|
2834
|
+
}], propDecorators: { validator: [{ type: i0.Input, args: [{ isSignal: true, alias: "validator", required: false }] }] } });
|
|
2835
|
+
|
|
2836
|
+
/**
|
|
2837
|
+
* Pipe that converts plain-text newlines (`\r\n`, `\r`, `\n`) to HTML `<br>` tags
|
|
2838
|
+
* and marks the result as trusted HTML so Angular does not escape it.
|
|
2839
|
+
*
|
|
2840
|
+
* Usage: `{{ text | formatHtml }}`
|
|
2841
|
+
*/
|
|
2842
|
+
class FormatHtmlPipe {
|
|
2843
|
+
constructor() {
|
|
2844
|
+
this.sanitizer = inject(DomSanitizer);
|
|
2845
|
+
}
|
|
2846
|
+
/**
|
|
2847
|
+
* Transforms a plain-text string into sanitized HTML by replacing newline characters
|
|
2848
|
+
* with `<br>` tags.
|
|
2849
|
+
* @param value - The input string to transform. Treated as an empty string when `undefined`.
|
|
2850
|
+
* @returns A `SafeHtml` value that can be rendered with `[innerHTML]`.
|
|
2851
|
+
*/
|
|
2852
|
+
transform(value) {
|
|
2853
|
+
const escaped = (value ?? '')
|
|
2854
|
+
.replaceAll('&', '&')
|
|
2855
|
+
.replaceAll('<', '<')
|
|
2856
|
+
.replaceAll('>', '>')
|
|
2857
|
+
.replaceAll('"', '"');
|
|
2858
|
+
return this.sanitizer.bypassSecurityTrustHtml(escaped.replaceAll(/(?:\r\n|\r|\n)/g, '<br>'));
|
|
2859
|
+
}
|
|
2860
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatHtmlPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
2861
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: FormatHtmlPipe, isStandalone: true, name: "formatHtml" }); }
|
|
2862
|
+
}
|
|
2863
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatHtmlPipe, decorators: [{
|
|
2864
|
+
type: Pipe,
|
|
2865
|
+
args: [{
|
|
2866
|
+
name: 'formatHtml',
|
|
2867
|
+
standalone: true
|
|
2868
|
+
}]
|
|
2869
|
+
}] });
|
|
2870
|
+
|
|
2871
|
+
/**
|
|
2872
|
+
* Pipe that converts a Markdown string to sanitized HTML using `SystemUtils.markdownToHtml`.
|
|
2873
|
+
*
|
|
2874
|
+
* Usage: `{{ text | formatMarkdown }}`
|
|
2875
|
+
*/
|
|
2876
|
+
class FormatMarkdownPipe {
|
|
2877
|
+
constructor() {
|
|
2878
|
+
this.sanitizer = inject(DomSanitizer);
|
|
2879
|
+
}
|
|
2880
|
+
/**
|
|
2881
|
+
* Transforms a Markdown string into sanitized HTML.
|
|
2882
|
+
* @param value - The Markdown input to convert. Treated as an empty string when `undefined`.
|
|
2883
|
+
* @returns A `SafeHtml` value that can be rendered with `[innerHTML]`.
|
|
2884
|
+
*/
|
|
2885
|
+
transform(value) {
|
|
2886
|
+
return this.sanitizer.bypassSecurityTrustHtml(SystemUtils.markdownToHtml(value ?? ''));
|
|
2887
|
+
}
|
|
2888
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatMarkdownPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
2889
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: FormatMarkdownPipe, isStandalone: true, name: "formatMarkdown" }); }
|
|
2890
|
+
}
|
|
2891
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatMarkdownPipe, decorators: [{
|
|
2892
|
+
type: Pipe,
|
|
2893
|
+
args: [{
|
|
2894
|
+
name: 'formatMarkdown',
|
|
2895
|
+
standalone: true
|
|
2896
|
+
}]
|
|
2897
|
+
}] });
|
|
2898
|
+
|
|
2899
|
+
/**
|
|
2900
|
+
* General-purpose formatting pipe that converts a raw value to a locale-aware string
|
|
2901
|
+
* based on the specified format type.
|
|
2902
|
+
*
|
|
2903
|
+
* Supported types: `'date'` / `'D'`, `'currency'` / `'C'`, `'number'` / `'N'`,
|
|
2904
|
+
* `'number0'` / `'N0'`, `'percentage'` / `'P'`.
|
|
2905
|
+
*
|
|
2906
|
+
* Usage: `{{ value | format:'currency' }}`
|
|
2907
|
+
*/
|
|
2908
|
+
class FormatPipe {
|
|
2909
|
+
/** Shared formatters: building an `Intl.NumberFormat` per call is expensive. */
|
|
2910
|
+
static { this.currencyFormat = new Intl.NumberFormat('it-IT', { style: 'currency', currency: 'EUR' }); }
|
|
2911
|
+
static { this.numberFormat = new Intl.NumberFormat('it-IT', { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 2 }); }
|
|
2912
|
+
static { this.number0Format = new Intl.NumberFormat('it-IT', { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 0 }); }
|
|
2913
|
+
static { this.percentFormat = new Intl.NumberFormat('it-IT', { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 0 }); }
|
|
2914
|
+
/**
|
|
2915
|
+
* Formats a value according to the specified type and optional pattern.
|
|
2916
|
+
* Returns `undefined` when the value is `null` or `undefined`, or when the type is unrecognised.
|
|
2917
|
+
* @param value - The raw value to format.
|
|
2918
|
+
* @param type - The format type identifier (default: `'date'`).
|
|
2919
|
+
* @param pattern - The date pattern used when `type` is `'date'` (default: `'dd/MM/yyyy'`).
|
|
2920
|
+
* @returns A formatted string, or `undefined` when the value cannot be formatted.
|
|
2921
|
+
*/
|
|
2922
|
+
transform(value, type = 'date', pattern = 'dd/MM/yyyy') {
|
|
2923
|
+
if (value === undefined || value === null)
|
|
2924
|
+
return undefined;
|
|
2925
|
+
switch (type) {
|
|
2926
|
+
case 'D':
|
|
2927
|
+
case 'date': {
|
|
2928
|
+
const d = SystemUtils.parseDate(value, it);
|
|
2929
|
+
if (d)
|
|
2930
|
+
return format(d, pattern, { locale: it });
|
|
2931
|
+
break;
|
|
2932
|
+
}
|
|
2933
|
+
case 'C':
|
|
2934
|
+
case 'currency':
|
|
2935
|
+
return FormatPipe.currencyFormat.format(value);
|
|
2936
|
+
case 'N':
|
|
2937
|
+
case 'number':
|
|
2938
|
+
return FormatPipe.numberFormat.format(value);
|
|
2939
|
+
case 'N0':
|
|
2940
|
+
case 'number0':
|
|
2941
|
+
return FormatPipe.number0Format.format(value);
|
|
2942
|
+
case 'P':
|
|
2943
|
+
case 'percentage':
|
|
2944
|
+
return FormatPipe.percentFormat.format(value);
|
|
2945
|
+
}
|
|
2946
|
+
return undefined;
|
|
2947
|
+
}
|
|
2948
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
2949
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: FormatPipe, isStandalone: true, name: "format" }); }
|
|
2950
|
+
}
|
|
2951
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatPipe, decorators: [{
|
|
2952
|
+
type: Pipe,
|
|
2953
|
+
args: [{
|
|
2954
|
+
name: 'format',
|
|
2955
|
+
standalone: true
|
|
2956
|
+
}]
|
|
2957
|
+
}] });
|
|
2739
2958
|
|
|
2740
2959
|
/**
|
|
2741
|
-
*
|
|
2742
|
-
*
|
|
2960
|
+
* Pipe that applies a global regex replacement on a string and returns the result
|
|
2961
|
+
* as sanitized HTML. When `regexValue` is `'\n'` and no `replaceValue` is given,
|
|
2962
|
+
* newlines are replaced with `<br>` tags.
|
|
2963
|
+
*
|
|
2964
|
+
* Usage: `{{ text | replace:'\n':'' }}`
|
|
2743
2965
|
*/
|
|
2744
|
-
class
|
|
2966
|
+
class ReplacePipe {
|
|
2967
|
+
constructor() {
|
|
2968
|
+
this.sanitizer = inject(DomSanitizer);
|
|
2969
|
+
}
|
|
2745
2970
|
/**
|
|
2746
|
-
*
|
|
2747
|
-
* Returns `
|
|
2748
|
-
* @param
|
|
2971
|
+
* Replaces all occurrences of `regexValue` in `value` with `replaceValue`.
|
|
2972
|
+
* Returns `undefined` when `value` is empty or `undefined`.
|
|
2973
|
+
* @param value - The source string to process.
|
|
2974
|
+
* @param regexValue - The regex pattern string to match (applied with the global flag).
|
|
2975
|
+
* @param replaceValue - The replacement string. Defaults to `'<br>'` when `regexValue` is `'\n'` and this is falsy.
|
|
2976
|
+
* @returns A `SafeHtml` value with all matches replaced, or `undefined` when the input is empty.
|
|
2749
2977
|
*/
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
return
|
|
2978
|
+
transform(value, regexValue, replaceValue) {
|
|
2979
|
+
if (!value)
|
|
2980
|
+
return undefined;
|
|
2981
|
+
const replacement = (regexValue === '\n' && !replaceValue) ? '<br>' : (replaceValue ?? '');
|
|
2982
|
+
return this.sanitizer.bypassSecurityTrustHtml(value.replace(new RegExp(regexValue, 'g'), replacement));
|
|
2755
2983
|
}
|
|
2756
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type:
|
|
2757
|
-
static { this.ɵ
|
|
2758
|
-
{
|
|
2759
|
-
provide: NG_VALIDATORS,
|
|
2760
|
-
useExisting: forwardRef(() => UrlValidatorDirective),
|
|
2761
|
-
multi: true,
|
|
2762
|
-
},
|
|
2763
|
-
], ngImport: i0 }); }
|
|
2984
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ReplacePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
2985
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: ReplacePipe, isStandalone: true, name: "replace" }); }
|
|
2764
2986
|
}
|
|
2765
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type:
|
|
2766
|
-
type:
|
|
2987
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ReplacePipe, decorators: [{
|
|
2988
|
+
type: Pipe,
|
|
2767
2989
|
args: [{
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
{
|
|
2771
|
-
provide: NG_VALIDATORS,
|
|
2772
|
-
useExisting: forwardRef(() => UrlValidatorDirective),
|
|
2773
|
-
multi: true,
|
|
2774
|
-
},
|
|
2775
|
-
],
|
|
2776
|
-
standalone: true,
|
|
2990
|
+
name: 'replace',
|
|
2991
|
+
standalone: true
|
|
2777
2992
|
}]
|
|
2778
2993
|
}] });
|
|
2779
2994
|
|
|
2780
2995
|
/**
|
|
2781
|
-
*
|
|
2782
|
-
*
|
|
2996
|
+
* Pipe that marks an HTML string as trusted so Angular does not escape it when
|
|
2997
|
+
* bound via `[innerHTML]`.
|
|
2998
|
+
*
|
|
2999
|
+
* Usage: `<div [innerHTML]="html | safeHtml"></div>`
|
|
2783
3000
|
*/
|
|
2784
|
-
class
|
|
3001
|
+
class SafeHtmlPipe {
|
|
2785
3002
|
constructor() {
|
|
2786
|
-
|
|
2787
|
-
this.validIf = input(false, /* @ts-ignore */
|
|
2788
|
-
...(ngDevMode ? [{ debugName: "validIf" }] : /* istanbul ignore next */ []));
|
|
3003
|
+
this.sanitizer = inject(DomSanitizer);
|
|
2789
3004
|
}
|
|
2790
3005
|
/**
|
|
2791
|
-
*
|
|
2792
|
-
* @param
|
|
3006
|
+
* Bypasses Angular's HTML sanitization and returns a `SafeHtml` instance.
|
|
3007
|
+
* @param value - The raw HTML string to trust. Treated as an empty string when `undefined`.
|
|
3008
|
+
* @returns A `SafeHtml` value that can be bound to `[innerHTML]` without escaping.
|
|
2793
3009
|
*/
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
const c = control.value ? control.value : null;
|
|
2797
|
-
if (!c) {
|
|
2798
|
-
isValid = this.validIf() === true;
|
|
2799
|
-
}
|
|
2800
|
-
else {
|
|
2801
|
-
try {
|
|
2802
|
-
isValid = c.isValid();
|
|
2803
|
-
}
|
|
2804
|
-
catch { }
|
|
2805
|
-
}
|
|
2806
|
-
return isValid ? null : { validIf: "Non valido." };
|
|
3010
|
+
transform(value) {
|
|
3011
|
+
return this.sanitizer.bypassSecurityTrustHtml(value ?? '');
|
|
2807
3012
|
}
|
|
2808
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type:
|
|
2809
|
-
static { this.ɵ
|
|
2810
|
-
{
|
|
2811
|
-
provide: NG_VALIDATORS,
|
|
2812
|
-
useExisting: forwardRef(() => ValidIfDirective),
|
|
2813
|
-
multi: true,
|
|
2814
|
-
},
|
|
2815
|
-
], ngImport: i0 }); }
|
|
3013
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SafeHtmlPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
3014
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: SafeHtmlPipe, isStandalone: true, name: "safeHtml" }); }
|
|
2816
3015
|
}
|
|
2817
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type:
|
|
2818
|
-
type:
|
|
3016
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SafeHtmlPipe, decorators: [{
|
|
3017
|
+
type: Pipe,
|
|
2819
3018
|
args: [{
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
{
|
|
2823
|
-
provide: NG_VALIDATORS,
|
|
2824
|
-
useExisting: forwardRef(() => ValidIfDirective),
|
|
2825
|
-
multi: true,
|
|
2826
|
-
},
|
|
2827
|
-
],
|
|
2828
|
-
standalone: true,
|
|
3019
|
+
name: 'safeHtml',
|
|
3020
|
+
standalone: true
|
|
2829
3021
|
}]
|
|
2830
|
-
}]
|
|
3022
|
+
}] });
|
|
2831
3023
|
|
|
2832
3024
|
/**
|
|
2833
|
-
*
|
|
2834
|
-
*
|
|
3025
|
+
* Pipe that marks a URL string as a trusted resource URL so Angular does not block it
|
|
3026
|
+
* when bound to attributes such as `[src]` or `[href]` on iframes, objects, or embeds.
|
|
3027
|
+
*
|
|
3028
|
+
* Usage: `<iframe [src]="url | safeUrl"></iframe>`
|
|
2835
3029
|
*/
|
|
2836
|
-
class
|
|
3030
|
+
class SafeUrlPipe {
|
|
2837
3031
|
constructor() {
|
|
2838
|
-
|
|
2839
|
-
this.validator = input(undefined, /* @ts-ignore */
|
|
2840
|
-
...(ngDevMode ? [{ debugName: "validator" }] : /* istanbul ignore next */ []));
|
|
3032
|
+
this.sanitizer = inject(DomSanitizer);
|
|
2841
3033
|
}
|
|
2842
3034
|
/**
|
|
2843
|
-
*
|
|
2844
|
-
*
|
|
2845
|
-
* @
|
|
3035
|
+
* Bypasses Angular's resource-URL sanitization and returns a `SafeResourceUrl` instance.
|
|
3036
|
+
* @param value - The URL string to trust. Treated as an empty string when `undefined`.
|
|
3037
|
+
* @returns A `SafeResourceUrl` that can be bound to resource URL attributes without blocking.
|
|
2846
3038
|
*/
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
return fn ? fn(control) : null;
|
|
3039
|
+
transform(value) {
|
|
3040
|
+
return this.sanitizer.bypassSecurityTrustResourceUrl(value ?? '');
|
|
2850
3041
|
}
|
|
2851
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type:
|
|
2852
|
-
static { this.ɵ
|
|
3042
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SafeUrlPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
3043
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: SafeUrlPipe, isStandalone: true, name: "safeUrl" }); }
|
|
2853
3044
|
}
|
|
2854
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type:
|
|
2855
|
-
type:
|
|
3045
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SafeUrlPipe, decorators: [{
|
|
3046
|
+
type: Pipe,
|
|
2856
3047
|
args: [{
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
standalone: true,
|
|
3048
|
+
name: 'safeUrl',
|
|
3049
|
+
standalone: true
|
|
2860
3050
|
}]
|
|
2861
|
-
}]
|
|
3051
|
+
}] });
|
|
2862
3052
|
|
|
2863
3053
|
/**
|
|
2864
|
-
*
|
|
3054
|
+
* Impure pipe that filters an array using a caller-provided predicate function.
|
|
3055
|
+
* Because the pipe is impure it re-evaluates on every change-detection cycle,
|
|
3056
|
+
* which is necessary when the predicate's captured state changes.
|
|
2865
3057
|
*
|
|
2866
|
-
* Usage:
|
|
3058
|
+
* Usage: `*ngFor="let item of items | callback:myFilter"`
|
|
2867
3059
|
*/
|
|
2868
|
-
class
|
|
2869
|
-
|
|
2870
|
-
|
|
3060
|
+
class SearchCallbackPipe {
|
|
3061
|
+
/**
|
|
3062
|
+
* Filters `items` by applying `callback` to each element.
|
|
3063
|
+
* Returns the original array unchanged when either argument is falsy.
|
|
3064
|
+
* @param items - The source array to filter. May be `undefined`.
|
|
3065
|
+
* @param callback - A predicate function that returns `true` for items to keep.
|
|
3066
|
+
* @returns A new filtered array, the original array when no callback is provided,
|
|
3067
|
+
* or `undefined` when `items` is `undefined`.
|
|
3068
|
+
*/
|
|
3069
|
+
transform(items, callback) {
|
|
3070
|
+
if (!items || !callback)
|
|
3071
|
+
return items;
|
|
3072
|
+
return items.filter(item => callback(item));
|
|
2871
3073
|
}
|
|
3074
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SearchCallbackPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
3075
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: SearchCallbackPipe, isStandalone: true, name: "callback", pure: false }); }
|
|
3076
|
+
}
|
|
3077
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SearchCallbackPipe, decorators: [{
|
|
3078
|
+
type: Pipe,
|
|
3079
|
+
args: [{
|
|
3080
|
+
name: 'callback',
|
|
3081
|
+
pure: false,
|
|
3082
|
+
standalone: true
|
|
3083
|
+
}]
|
|
3084
|
+
}] });
|
|
3085
|
+
|
|
3086
|
+
/**
|
|
3087
|
+
* Pure pipe that filters an array of searchable items against a text query.
|
|
3088
|
+
* Note: being pure, it re-runs only when the array REFERENCE or the query
|
|
3089
|
+
* changes; mutate-in-place updates of the array are not detected.
|
|
3090
|
+
*
|
|
3091
|
+
* Each item is matched either via its `searchBag.name` property (when present)
|
|
3092
|
+
* or by converting the item itself to a lowercase string. The optional `metadata`
|
|
3093
|
+
* argument is updated in-place with the total item count and the filtered count,
|
|
3094
|
+
* making it usable in the template alongside `*ngFor`.
|
|
3095
|
+
*
|
|
3096
|
+
* Usage:
|
|
3097
|
+
* ```html
|
|
3098
|
+
* <div *ngFor="let item of items | search:filterText:meta">...</div>
|
|
3099
|
+
* <div>Showing {{ meta.count }} of {{ meta.total }}</div>
|
|
3100
|
+
* ```
|
|
3101
|
+
*/
|
|
3102
|
+
class SearchFilterPipe {
|
|
2872
3103
|
/**
|
|
2873
|
-
*
|
|
2874
|
-
*
|
|
2875
|
-
* @
|
|
3104
|
+
* Filters `items` by performing a case-insensitive substring match against `value`.
|
|
3105
|
+
* When `items` or `value` is falsy the original array is returned unfiltered.
|
|
3106
|
+
* @param items - The source array to filter. May be `undefined`.
|
|
3107
|
+
* @param value - The search text to match against each item. May be `undefined`.
|
|
3108
|
+
* @param metadata - Optional object that is updated with `total` and `count` after filtering.
|
|
3109
|
+
* @returns The filtered array, the original array when no filter text is given,
|
|
3110
|
+
* or `undefined` when `items` is `undefined`.
|
|
2876
3111
|
*/
|
|
2877
|
-
transform(value) {
|
|
2878
|
-
|
|
3112
|
+
transform(items, value, metadata) {
|
|
3113
|
+
metadata ??= { total: 0, count: 0 };
|
|
3114
|
+
if (!items || !value) {
|
|
3115
|
+
metadata.total = items?.length ?? 0;
|
|
3116
|
+
metadata.count = metadata.total;
|
|
3117
|
+
return items;
|
|
3118
|
+
}
|
|
3119
|
+
const query = value.toLowerCase();
|
|
3120
|
+
const result = items.filter(item => {
|
|
3121
|
+
if (!item)
|
|
3122
|
+
return false;
|
|
3123
|
+
const text = item.searchBag?.name ?? (typeof item === 'string' ? item : undefined);
|
|
3124
|
+
return text?.toLowerCase().includes(query) ?? false;
|
|
3125
|
+
});
|
|
3126
|
+
metadata.total = items.length;
|
|
3127
|
+
metadata.count = result.length;
|
|
3128
|
+
return result;
|
|
2879
3129
|
}
|
|
2880
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type:
|
|
2881
|
-
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type:
|
|
3130
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SearchFilterPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
3131
|
+
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: SearchFilterPipe, isStandalone: true, name: "search" }); }
|
|
2882
3132
|
}
|
|
2883
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type:
|
|
3133
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SearchFilterPipe, decorators: [{
|
|
2884
3134
|
type: Pipe,
|
|
2885
3135
|
args: [{
|
|
2886
|
-
name: '
|
|
3136
|
+
name: 'search',
|
|
2887
3137
|
standalone: true
|
|
2888
3138
|
}]
|
|
2889
3139
|
}] });
|
|
@@ -2895,7 +3145,8 @@ const Breakpoints = {
|
|
|
2895
3145
|
SmallMedium: '(min-width: 600px) and (max-width: 1059.98px)',
|
|
2896
3146
|
Medium: '(min-width: 960px) and (max-width: 1279.98px)',
|
|
2897
3147
|
MediumLarge: '(min-width: 960px) and (max-width: 1459.98px)',
|
|
2898
|
-
Large: '(min-width: 1460px) and (max-width: 1919.98px)'
|
|
3148
|
+
Large: '(min-width: 1460px) and (max-width: 1919.98px)',
|
|
3149
|
+
XLarge: '(min-width: 1920px)'
|
|
2899
3150
|
};
|
|
2900
3151
|
|
|
2901
3152
|
const UtilsMessages = {
|
|
@@ -2927,20 +3178,32 @@ class SelectableModel {
|
|
|
2927
3178
|
}
|
|
2928
3179
|
/**
|
|
2929
3180
|
* @param allowMultiSelect - When `true` (default), multiple items can be selected simultaneously.
|
|
2930
|
-
* @param lookupFieldName - Name of the
|
|
3181
|
+
* @param lookupFieldName - Name of the field used as the unique key when searching the internal list
|
|
3182
|
+
* (default: `'id'`). Keys of `T` are autocompleted; plain strings remain accepted.
|
|
2931
3183
|
*/
|
|
2932
3184
|
constructor(allowMultiSelect = true, lookupFieldName = 'id') {
|
|
2933
3185
|
/**
|
|
2934
3186
|
* Emits whenever the selection changes.
|
|
2935
3187
|
* Carries the affected item, or `undefined` when the whole selection is cleared.
|
|
3188
|
+
*
|
|
3189
|
+
* Note: kept as `EventEmitter` (instead of a plain RxJS `Subject`) for
|
|
3190
|
+
* backward compatibility with existing consumers; `EventEmitter` extends
|
|
3191
|
+
* `Subject`, so `.subscribe()`, `pipe(...)` and `takeUntilDestroyed` all
|
|
3192
|
+
* work as expected.
|
|
2936
3193
|
*/
|
|
2937
3194
|
this.changed = new EventEmitter();
|
|
2938
3195
|
this._all = signal([], /* @ts-ignore */
|
|
2939
3196
|
...(ngDevMode ? [{ debugName: "_all" }] : /* istanbul ignore next */ []));
|
|
2940
|
-
/** Signal that is `true` when at least one item is
|
|
3197
|
+
/** Signal that is `true` when at least one item is tracked in the internal list. */
|
|
2941
3198
|
this.hasValue = computed(() => this._all().length > 0, /* @ts-ignore */
|
|
2942
3199
|
...(ngDevMode ? [{ debugName: "hasValue" }] : /* istanbul ignore next */ []));
|
|
2943
|
-
|
|
3200
|
+
/** Signal with the number of items currently tracked in the internal list. */
|
|
3201
|
+
this.count = computed(() => this._all().length, /* @ts-ignore */
|
|
3202
|
+
...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
|
|
3203
|
+
this._current = new SelectionModel(allowMultiSelect, [], true,
|
|
3204
|
+
// Compare by lookup key so different instances of the same logical item
|
|
3205
|
+
// (e.g. after a data reload) are treated as the same selection entry.
|
|
3206
|
+
(a, b) => a[lookupFieldName] === b[lookupFieldName]);
|
|
2944
3207
|
this._lookupFieldName = lookupFieldName;
|
|
2945
3208
|
}
|
|
2946
3209
|
/**
|
|
@@ -3071,6 +3334,27 @@ class BroadcastChannelManager {
|
|
|
3071
3334
|
get currentBus() {
|
|
3072
3335
|
return this.channel?.name;
|
|
3073
3336
|
}
|
|
3337
|
+
/**
|
|
3338
|
+
* Observable that emits EVERY message bag received on the channel,
|
|
3339
|
+
* regardless of `messageId`. Completed by {@link dispose}.
|
|
3340
|
+
* Prefer {@link observe} to listen for a single message type.
|
|
3341
|
+
*/
|
|
3342
|
+
get messages() {
|
|
3343
|
+
return this.subject.asObservable();
|
|
3344
|
+
}
|
|
3345
|
+
/**
|
|
3346
|
+
* Returns a typed Observable that emits only the messages with the given `messageId`.
|
|
3347
|
+
*
|
|
3348
|
+
* Unlike {@link subscribe} (one callback per id, replaced on re-registration),
|
|
3349
|
+
* this supports any number of concurrent subscribers and integrates with the
|
|
3350
|
+
* RxJS lifecycle (`takeUntilDestroyed`, `async` pipe, ...). Completed by {@link dispose}.
|
|
3351
|
+
*
|
|
3352
|
+
* @param messageId - The message type identifier to listen for.
|
|
3353
|
+
* @returns An Observable of the matching typed message bags.
|
|
3354
|
+
*/
|
|
3355
|
+
observe(messageId) {
|
|
3356
|
+
return this.subject.pipe(filter(bag => bag.messageId === messageId), map(bag => bag));
|
|
3357
|
+
}
|
|
3074
3358
|
/**
|
|
3075
3359
|
* Opens a `BroadcastChannel` with the given name (when the API is available).
|
|
3076
3360
|
* @param bus - The channel name to open (default: `'ARS-CHANNEL'`).
|
|
@@ -3224,6 +3508,16 @@ class BroadcastService {
|
|
|
3224
3508
|
unsubscribeChannelMessage(id) {
|
|
3225
3509
|
this.channel.unsubscribe(id);
|
|
3226
3510
|
}
|
|
3511
|
+
/**
|
|
3512
|
+
* Returns a typed Observable of the cross-tab channel messages with the given `id`.
|
|
3513
|
+
* Supports multiple concurrent subscribers and the RxJS lifecycle operators,
|
|
3514
|
+
* unlike `subscribeChannelMessage` (single callback per id).
|
|
3515
|
+
* @param id - The message type identifier to listen for.
|
|
3516
|
+
* @returns An Observable of the matching typed message bags.
|
|
3517
|
+
*/
|
|
3518
|
+
observeChannelMessage(id) {
|
|
3519
|
+
return this.channel.observe(id);
|
|
3520
|
+
}
|
|
3227
3521
|
/**
|
|
3228
3522
|
* Returns an `Observable` that emits every in-process message published via `sendMessage`.
|
|
3229
3523
|
* @returns An observable stream of `BroadcastMessageInfo` objects.
|
|
@@ -3231,6 +3525,14 @@ class BroadcastService {
|
|
|
3231
3525
|
getMessage() {
|
|
3232
3526
|
return this.subject.asObservable();
|
|
3233
3527
|
}
|
|
3528
|
+
/**
|
|
3529
|
+
* Returns a typed `Observable` of the in-process messages with the given `id`.
|
|
3530
|
+
* @param id - The message type identifier to listen for.
|
|
3531
|
+
* @returns An Observable emitting the typed `data` payload of each matching message.
|
|
3532
|
+
*/
|
|
3533
|
+
observeMessage(id) {
|
|
3534
|
+
return this.subject.pipe(filter$1(info => info.id === id), map$1(info => info.data));
|
|
3535
|
+
}
|
|
3234
3536
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: BroadcastService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
|
|
3235
3537
|
static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.1", ngImport: i0, type: BroadcastService }); }
|
|
3236
3538
|
}
|
|
@@ -3324,7 +3626,7 @@ class ScreenService {
|
|
|
3324
3626
|
* Modern Chromium-based Edge reports a different user-agent and will return `false`.
|
|
3325
3627
|
*/
|
|
3326
3628
|
get isIEOrEdge() {
|
|
3327
|
-
return /msie\s|trident\/|edge\//i.test(window.navigator.userAgent);
|
|
3629
|
+
return SystemUtils.isBrowser() && /msie\s|trident\/|edge\//i.test(window.navigator.userAgent);
|
|
3328
3630
|
}
|
|
3329
3631
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ScreenService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
|
|
3330
3632
|
static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.1", ngImport: i0, type: ScreenService }); }
|
|
@@ -3379,7 +3681,9 @@ class ThemeService {
|
|
|
3379
3681
|
constructor() {
|
|
3380
3682
|
this.broadcastChannel = new BroadcastChannelManager('THEME-SERVICE');
|
|
3381
3683
|
this.broadcastMessage = '$theme-changed';
|
|
3382
|
-
this.prefersColorSchemeMediaQueryList = window.matchMedia
|
|
3684
|
+
this.prefersColorSchemeMediaQueryList = typeof window !== 'undefined' && typeof window.matchMedia === 'function'
|
|
3685
|
+
? window.matchMedia('(prefers-color-scheme: dark)')
|
|
3686
|
+
: undefined;
|
|
3383
3687
|
this.themeChanged = new BehaviorSubject('auto');
|
|
3384
3688
|
this.renderFactory2 = inject(RendererFactory2);
|
|
3385
3689
|
this.renderer = this.renderFactory2.createRenderer(null, null);
|
|
@@ -3412,10 +3716,12 @@ class ThemeService {
|
|
|
3412
3716
|
* @param theme - The initial theme to apply (defaults to the value stored in `localStorage`).
|
|
3413
3717
|
*/
|
|
3414
3718
|
initialize(theme = this.getPreferredTheme()) {
|
|
3415
|
-
this.prefersColorSchemeMediaQueryList
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3719
|
+
if (this.prefersColorSchemeMediaQueryList) {
|
|
3720
|
+
this.prefersColorSchemeMediaQueryList.onchange = () => {
|
|
3721
|
+
if (this.auto())
|
|
3722
|
+
this.setTheme(this.theme());
|
|
3723
|
+
};
|
|
3724
|
+
}
|
|
3419
3725
|
this.broadcastChannel.subscribe({
|
|
3420
3726
|
messageId: this.broadcastMessage,
|
|
3421
3727
|
action: (bag) => {
|
|
@@ -3441,10 +3747,11 @@ class ThemeService {
|
|
|
3441
3747
|
* @returns `'light'` or `'dark'` — never `'auto'`.
|
|
3442
3748
|
*/
|
|
3443
3749
|
getTheme() {
|
|
3444
|
-
|
|
3445
|
-
|
|
3750
|
+
const current = this.theme();
|
|
3751
|
+
if (current === 'auto') {
|
|
3752
|
+
return this.prefersColorSchemeMediaQueryList?.matches ? 'dark' : 'light';
|
|
3446
3753
|
}
|
|
3447
|
-
return
|
|
3754
|
+
return current;
|
|
3448
3755
|
}
|
|
3449
3756
|
/**
|
|
3450
3757
|
* Reads the theme stored in `localStorage` and validates it.
|
|
@@ -3452,6 +3759,8 @@ class ThemeService {
|
|
|
3452
3759
|
* @returns The persisted `ThemeType`, or `'auto'` as default.
|
|
3453
3760
|
*/
|
|
3454
3761
|
getPreferredTheme() {
|
|
3762
|
+
if (typeof localStorage === 'undefined')
|
|
3763
|
+
return 'auto';
|
|
3455
3764
|
const stored = localStorage.getItem('preferred-theme');
|
|
3456
3765
|
return stored && this.isTheme(stored) ? stored : 'auto';
|
|
3457
3766
|
}
|
|
@@ -3472,9 +3781,11 @@ class ThemeService {
|
|
|
3472
3781
|
this.renderer.removeClass(document.body, 'light');
|
|
3473
3782
|
this.renderer.removeClass(document.body, 'dark');
|
|
3474
3783
|
this.theme.set(theme);
|
|
3475
|
-
localStorage
|
|
3784
|
+
if (typeof localStorage !== 'undefined') {
|
|
3785
|
+
localStorage.setItem('preferred-theme', theme);
|
|
3786
|
+
}
|
|
3476
3787
|
const effectiveClass = this.auto()
|
|
3477
|
-
? (this.prefersColorSchemeMediaQueryList
|
|
3788
|
+
? (this.prefersColorSchemeMediaQueryList?.matches ? 'dark' : 'light')
|
|
3478
3789
|
: theme;
|
|
3479
3790
|
this.renderer.addClass(document.body, effectiveClass);
|
|
3480
3791
|
this.themeChanged.next(this.getTheme());
|
|
@@ -3488,12 +3799,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
3488
3799
|
}], ctorParameters: () => [] });
|
|
3489
3800
|
|
|
3490
3801
|
/*
|
|
3491
|
-
* Public API Surface of
|
|
3802
|
+
* Public API Surface of scm-utils
|
|
3492
3803
|
*/
|
|
3493
3804
|
|
|
3494
3805
|
/**
|
|
3495
3806
|
* Generated bundle index. Do not edit.
|
|
3496
3807
|
*/
|
|
3497
3808
|
|
|
3498
|
-
export { AutoFocusDirective, Breakpoints, BroadcastChannelManager, BroadcastService, CopyClipboardDirective, DateFnsAdapter, DateFormat, DateInterval, DateIntervalChangeDirective, DeleteModel, EmailsValidatorDirective, EnvironmentService, EqualsValidatorDirective, FileInfo, FileSizeValidatorDirective, FormatHtmlPipe, FormatMarkdownPipe, FormatPipe, GroupModel, GuidValidatorDirective, IDModel, ImportModel, MAT_DATE_FNS_FORMATS, MaxTermsValidatorDirective, NotEmptyValidatorDirective, NotEqualValidatorDirective, NotFutureValidatorDirective, PasswordValidatorDirective, QueryModel, RelationModel, RemoveFocusDirective, ReplacePipe, SafeHtmlPipe, SafeUrlPipe, ScreenService, SearchCallbackPipe, SearchFilterPipe, SelectableModel, SplashService, SqlDateValidatorDirective, SystemUtils, ThemeService, TimeValidatorDirective, UpdateRelationsModel, UrlValidatorDirective, UtilsMessages, ValidIfDirective, ValidatorDirective, ValueModel,
|
|
3809
|
+
export { AutoFocusDirective, Breakpoints, BroadcastChannelManager, BroadcastService, CopyClipboardDirective, DateFnsAdapter, DateFormat, DateInterval, DateIntervalChangeDirective, DeleteModel, EmailsValidatorDirective, EnvironmentService, EqualsValidatorDirective, FileInfo, FileSizeValidatorDirective, FormatHtmlPipe, FormatMarkdownPipe, FormatPipe, GroupModel, GuidValidatorDirective, IDModel, ImportModel, MAT_DATE_FNS_FORMATS, MaxTermsValidatorDirective, NotEmptyValidatorDirective, NotEqualValidatorDirective, NotFutureValidatorDirective, PasswordValidatorDirective, QueryModel, RelationModel, RemoveFocusDirective, ReplacePipe, SafeHtmlPipe, SafeUrlPipe, ScreenService, SearchCallbackPipe, SearchFilterPipe, SelectableModel, SplashService, SqlDateValidatorDirective, SystemUtils, ThemeService, TimeValidatorDirective, UpdateRelationsModel, UrlValidatorDirective, UtilsMessages, ValidIfDirective, ValidatorDirective, ValueModel, provideSCMDateFns };
|
|
3499
3810
|
//# sourceMappingURL=arsedizioni-ars-utils-core.mjs.map
|