@fabio.buscaroli/scm-utils 22.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +10 -0
  2. package/fesm2022/abv-scm-utils-core.mjs +3293 -0
  3. package/fesm2022/abv-scm-utils-core.mjs.map +1 -0
  4. package/fesm2022/abv-scm-utils-tinymce.mjs +230 -0
  5. package/fesm2022/abv-scm-utils-tinymce.mjs.map +1 -0
  6. package/fesm2022/abv-scm-utils-ui.application.mjs +3396 -0
  7. package/fesm2022/abv-scm-utils-ui.application.mjs.map +1 -0
  8. package/fesm2022/abv-scm-utils-ui.mjs +3060 -0
  9. package/fesm2022/abv-scm-utils-ui.mjs.map +1 -0
  10. package/fesm2022/abv-scm-utils-ui.oauth.mjs +278 -0
  11. package/fesm2022/abv-scm-utils-ui.oauth.mjs.map +1 -0
  12. package/fesm2022/abv-scm-utils.mjs +13 -0
  13. package/fesm2022/abv-scm-utils.mjs.map +1 -0
  14. package/fesm2022/fabio.buscaroli-scm-utils-core.mjs +3293 -0
  15. package/fesm2022/fabio.buscaroli-scm-utils-core.mjs.map +1 -0
  16. package/fesm2022/fabio.buscaroli-scm-utils-tinymce.mjs +230 -0
  17. package/fesm2022/fabio.buscaroli-scm-utils-tinymce.mjs.map +1 -0
  18. package/fesm2022/fabio.buscaroli-scm-utils-ui.application.mjs +3396 -0
  19. package/fesm2022/fabio.buscaroli-scm-utils-ui.application.mjs.map +1 -0
  20. package/fesm2022/fabio.buscaroli-scm-utils-ui.mjs +3060 -0
  21. package/fesm2022/fabio.buscaroli-scm-utils-ui.mjs.map +1 -0
  22. package/fesm2022/fabio.buscaroli-scm-utils-ui.oauth.mjs +278 -0
  23. package/fesm2022/fabio.buscaroli-scm-utils-ui.oauth.mjs.map +1 -0
  24. package/fesm2022/fabio.buscaroli-scm-utils.mjs +13 -0
  25. package/fesm2022/fabio.buscaroli-scm-utils.mjs.map +1 -0
  26. package/package.json +58 -0
  27. package/styles/ui.colors.scss +77 -0
  28. package/styles/ui.scss +350 -0
  29. package/tinymce/README.md +12 -0
  30. package/tinymce/langs/it.js +466 -0
  31. package/types/abv-scm-utils-core.d.ts +1536 -0
  32. package/types/abv-scm-utils-tinymce.d.ts +59 -0
  33. package/types/abv-scm-utils-ui.application.d.ts +1504 -0
  34. package/types/abv-scm-utils-ui.d.ts +1393 -0
  35. package/types/abv-scm-utils-ui.oauth.d.ts +68 -0
  36. package/types/abv-scm-utils.d.ts +4 -0
  37. package/types/fabio.buscaroli-scm-utils-core.d.ts +1536 -0
  38. package/types/fabio.buscaroli-scm-utils-tinymce.d.ts +59 -0
  39. package/types/fabio.buscaroli-scm-utils-ui.application.d.ts +1504 -0
  40. package/types/fabio.buscaroli-scm-utils-ui.d.ts +1393 -0
  41. package/types/fabio.buscaroli-scm-utils-ui.oauth.d.ts +68 -0
  42. package/types/fabio.buscaroli-scm-utils.d.ts +4 -0
@@ -0,0 +1,3293 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Pipe, inject, makeEnvironmentProviders, Injectable, ElementRef, afterNextRender, Directive, input, DestroyRef, HostListener, output, forwardRef, EventEmitter, signal, computed, Service, RendererFactory2 } from '@angular/core';
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';
7
+ import { DateAdapter, MAT_DATE_LOCALE, MAT_DATE_FORMATS } from '@angular/material/core';
8
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
9
+ import { Subject, BehaviorSubject } from 'rxjs';
10
+ import { debounceTime } from 'rxjs/operators';
11
+ import { NG_VALIDATORS } from '@angular/forms';
12
+ import { SelectionModel } from '@angular/cdk/collections';
13
+
14
+ /**
15
+ * Zero-dependency Markdown to HTML converter.
16
+ *
17
+ * Design goals:
18
+ * - Single line-based pass over blocks (no repeated full-string regex passes).
19
+ * - Inline formatting never touches code spans / code blocks (stash & restore).
20
+ * - No HTML entity escaping by default in normal text: the source passes as-is.
21
+ * Exception: the content of code spans / code blocks is ALWAYS escaped, so
22
+ * things like List<string> render correctly (the browser displays the
23
+ * original characters; entities never reach the user or the clipboard).
24
+ * Opt-in full escaping via { escapeHtml: true } for untrusted input.
25
+ * - URL sanitization on links/images (javascript:, vbscript:, data: are dropped).
26
+ * - Bounded result cache for repeated renders (Angular change detection friendly).
27
+ *
28
+ * Supported syntax: headings, paragraphs, hard/soft breaks, hr, blockquotes
29
+ * (nested), fenced code blocks, inline code, bold/italic/strikethrough, links
30
+ * (with title), images, autolinked bare URLs, ordered/unordered lists (nested,
31
+ * task lists, ordered start offset), GFM tables with alignment.
32
+ *
33
+ * Known simplifications (documented, by design):
34
+ * - A blank line terminates a list.
35
+ * - Setext headings (=== / ---) are not supported, use # syntax.
36
+ * - Reference-style links [text][ref] are not supported.
37
+ */
38
+ class MarkdownUtils {
39
+ // #region Cache
40
+ static { this.cache = new Map(); }
41
+ static { this.CACHE_MAX = 200; }
42
+ /** Clears the internal result cache. */
43
+ static clearCache() {
44
+ this.cache.clear();
45
+ }
46
+ // #endregion
47
+ // #region Public API
48
+ /**
49
+ * Convert markdown to HTML.
50
+ * @param markdown : the markdown source
51
+ * @param options : conversion options
52
+ * @returns : the HTML string ('' when input is falsy)
53
+ */
54
+ static toHtml(markdown, options) {
55
+ if (!markdown)
56
+ return '';
57
+ const opts = { escapeHtml: false, breaks: true, ...options };
58
+ const key = `${opts.escapeHtml ? 1 : 0}${opts.breaks ? 1 : 0}|${markdown}`;
59
+ const hit = this.cache.get(key);
60
+ if (hit !== undefined)
61
+ return hit;
62
+ // Normalize line endings and fix glued sentence punctuation ("dettaglio!Ecco")
63
+ const source = markdown
64
+ .replace(/\r\n?/g, '\n')
65
+ .replace(/([A-Za-zÀ-ÿ][!?])([A-Za-zÀ-ÿ])/g, '$1 $2');
66
+ const html = this.parseBlocks(source.split('\n'), opts).trim();
67
+ if (this.cache.size >= this.CACHE_MAX) {
68
+ this.cache.delete(this.cache.keys().next().value); // FIFO eviction
69
+ }
70
+ this.cache.set(key, html);
71
+ return html;
72
+ }
73
+ // #endregion
74
+ // #region Block parser (single pass)
75
+ static parseBlocks(lines, opts) {
76
+ const out = [];
77
+ let paragraph = [];
78
+ const flush = () => {
79
+ if (paragraph.length > 0) {
80
+ const sep = opts.breaks ? '<br>' : ' ';
81
+ out.push(`<p>${paragraph.map(l => this.inline(l, opts)).join(sep)}</p>`);
82
+ paragraph = [];
83
+ }
84
+ };
85
+ for (let i = 0; i < lines.length; i++) {
86
+ const line = lines[i];
87
+ const trimmed = line.trim();
88
+ // --- Fenced code block -------------------------------------------------
89
+ const fence = trimmed.match(/^```([\w+-]+)?\s*$/);
90
+ if (fence) {
91
+ flush();
92
+ const lang = fence[1] ? ` class="language-${fence[1]}"` : '';
93
+ const code = [];
94
+ i++;
95
+ while (i < lines.length && !/^```\s*$/.test(lines[i].trim())) {
96
+ code.push(lines[i]);
97
+ i++;
98
+ }
99
+ out.push(`<pre><code${lang}>${this.escape(code.join('\n'))}</code></pre>`);
100
+ continue;
101
+ }
102
+ // --- Blank line --------------------------------------------------------
103
+ if (!trimmed) {
104
+ flush();
105
+ continue;
106
+ }
107
+ // --- ATX heading -------------------------------------------------------
108
+ const heading = trimmed.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
109
+ if (heading) {
110
+ flush();
111
+ const level = heading[1].length;
112
+ out.push(`<h${level}>${this.inline(heading[2], opts)}</h${level}>`);
113
+ continue;
114
+ }
115
+ // --- Horizontal rule ---------------------------------------------------
116
+ if (/^(?:-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
117
+ flush();
118
+ out.push('<hr>');
119
+ continue;
120
+ }
121
+ // --- Blockquote (consecutive lines, one level stripped, recursive) ------
122
+ if (/^>\s?/.test(trimmed)) {
123
+ flush();
124
+ const quoted = [];
125
+ while (i < lines.length && /^>\s?/.test(lines[i].trim())) {
126
+ quoted.push(lines[i].trim().replace(/^>\s?/, ''));
127
+ i++;
128
+ }
129
+ i--;
130
+ out.push(`<blockquote>${this.parseBlocks(quoted, opts)}</blockquote>`);
131
+ continue;
132
+ }
133
+ // --- Table (header row + separator row) ---------------------------------
134
+ if (trimmed.includes('|') &&
135
+ i + 1 < lines.length &&
136
+ lines[i + 1].includes('-') &&
137
+ /^\|?[\s:\-|]+\|?$/.test(lines[i + 1].trim())) {
138
+ flush();
139
+ const aligns = this.tableAligns(lines[i + 1].trim());
140
+ const head = this.tableRow(trimmed, 'th', aligns, opts);
141
+ const body = [];
142
+ i += 2;
143
+ while (i < lines.length && lines[i].trim().includes('|')) {
144
+ body.push(this.tableRow(lines[i].trim(), 'td', aligns, opts));
145
+ i++;
146
+ }
147
+ i--;
148
+ out.push(`<table>\n<thead>\n${head}\n</thead>` +
149
+ (body.length > 0 ? `\n<tbody>\n${body.join('\n')}\n</tbody>` : '') +
150
+ `\n</table>`);
151
+ continue;
152
+ }
153
+ // --- List ---------------------------------------------------------------
154
+ if (MarkdownUtils.listItemRe.test(line)) {
155
+ flush();
156
+ const [html, last] = this.parseList(lines, i, opts);
157
+ out.push(html);
158
+ i = last;
159
+ continue;
160
+ }
161
+ // --- Plain text: accumulate into current paragraph ----------------------
162
+ paragraph.push(trimmed);
163
+ }
164
+ flush();
165
+ return out.join('\n');
166
+ }
167
+ // #endregion
168
+ // #region Lists
169
+ /** Matches "- item", "* item", "+ item", "1. item", "1) item" with leading indent. */
170
+ static { this.listItemRe = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/; }
171
+ /**
172
+ * Gathers the contiguous list block starting at `start`, builds it (with
173
+ * nesting) and returns [html, indexOfLastConsumedLine].
174
+ */
175
+ static parseList(lines, start, opts) {
176
+ const block = [];
177
+ let i = start;
178
+ while (i < lines.length) {
179
+ const l = lines[i];
180
+ if (!l.trim())
181
+ break; // blank line ends the list
182
+ if (this.listItemRe.test(l) || /^\s{2,}\S/.test(l)) {
183
+ block.push(l);
184
+ i++;
185
+ }
186
+ else
187
+ break;
188
+ }
189
+ return [this.buildList(block, opts), i - 1];
190
+ }
191
+ static buildList(block, opts) {
192
+ const first = block[0].match(this.listItemRe);
193
+ const baseIndent = first[1].length;
194
+ const ordered = /^\d/.test(first[2]);
195
+ const startNum = ordered ? parseInt(first[2], 10) : 1;
196
+ const items = [];
197
+ for (const l of block) {
198
+ const m = l.match(this.listItemRe);
199
+ if (m && m[1].length <= baseIndent) {
200
+ items.push({ text: m[3], childLines: [] });
201
+ }
202
+ else if (items.length > 0) {
203
+ // Continuation or nested content: strip one nesting level of indent
204
+ items[items.length - 1].childLines.push(l.length > baseIndent + 2 ? l.slice(baseIndent + 2) : l.trim());
205
+ }
206
+ }
207
+ const lis = items.map(item => {
208
+ let inner;
209
+ // GFM task list: - [ ] / - [x]
210
+ const task = item.text.match(/^\[([ xX])\]\s+(.*)$/);
211
+ if (task) {
212
+ const checked = task[1] !== ' ' ? ' checked' : '';
213
+ inner = `<input type="checkbox" disabled${checked}> ${this.inline(task[2], opts)}`;
214
+ }
215
+ else {
216
+ inner = this.inline(item.text, opts);
217
+ }
218
+ if (item.childLines.length > 0) {
219
+ inner += '\n' + this.parseBlocks(item.childLines, opts);
220
+ }
221
+ return `<li>${inner}</li>`;
222
+ });
223
+ const tag = ordered ? 'ol' : 'ul';
224
+ const startAttr = ordered && startNum > 1 ? ` start="${startNum}"` : '';
225
+ return `<${tag}${startAttr}>\n${lis.join('\n')}\n</${tag}>`;
226
+ }
227
+ // #endregion
228
+ // #region Tables
229
+ static tableAligns(separator) {
230
+ return separator
231
+ .replace(/^\||\|$/g, '')
232
+ .split('|')
233
+ .map(cell => {
234
+ const t = cell.trim();
235
+ if (t.startsWith(':') && t.endsWith(':'))
236
+ return 'center';
237
+ if (t.endsWith(':'))
238
+ return 'right';
239
+ if (t.startsWith(':'))
240
+ return 'left';
241
+ return undefined;
242
+ });
243
+ }
244
+ static tableRow(row, tag, aligns, opts) {
245
+ const cells = row.replace(/^\||\|$/g, '').split('|').map(c => c.trim());
246
+ const cellsHtml = cells
247
+ .map((cell, j) => {
248
+ const align = aligns[j] ? ` align="${aligns[j]}"` : '';
249
+ return `<${tag}${align}>${this.inline(cell, opts)}</${tag}>`;
250
+ })
251
+ .join('');
252
+ return `<tr>${cellsHtml}</tr>`;
253
+ }
254
+ // #endregion
255
+ // #region Inline formatting
256
+ /**
257
+ * Applies inline markdown to a single text segment.
258
+ * Generated HTML (code, links, images, autolinks) is stashed behind \u0000
259
+ * placeholders so later regex passes can never corrupt it.
260
+ */
261
+ static inline(text, opts) {
262
+ const store = [];
263
+ const stash = (html) => `\u0000${store.push(html) - 1}\u0000`;
264
+ // 1. Inline code: stash first so later passes can't touch it.
265
+ // Content is ALWAYS escaped so <, >, & inside code survive in the DOM
266
+ // (the browser renders entities back to the original characters).
267
+ let s = text.replace(/`([^`]+)`/g, (_m, code) => stash(`<code>${this.escape(code)}</code>`));
268
+ // 2. Escape raw HTML as entities (opt-in only)
269
+ if (opts.escapeHtml)
270
+ s = this.escape(s);
271
+ // 3. Images: ![alt](url "title")
272
+ s = s.replace(/!\[([^\]]*)\]\(\s*([^)\s]+)(?:\s+(?:"|&quot;)(.+?)(?:"|&quot;))?\s*\)/g, (_m, alt, src, title) => {
273
+ const url = this.safeUrl(src);
274
+ if (!url)
275
+ return alt;
276
+ const t = title ? ` title="${title}"` : '';
277
+ return stash(`<img src="${url}" alt="${alt}"${t} loading="lazy">`);
278
+ });
279
+ // 4. Links: [text](url "title")
280
+ s = s.replace(/\[([^\]]+)\]\(\s*([^)\s]+)(?:\s+(?:"|&quot;)(.+?)(?:"|&quot;))?\s*\)/g, (_m, txt, href, title) => {
281
+ const url = this.safeUrl(href);
282
+ if (!url)
283
+ return txt;
284
+ const t = title ? ` title="${title}"` : '';
285
+ return stash(`<a href="${url}"${t} target="_blank" rel="noopener noreferrer">${txt}</a>`);
286
+ });
287
+ // 5. Autolink remaining bare URLs (markdown links are already stashed)
288
+ s = s.replace(/https?:\/\/[^\s<>"']+/g, url => stash(`<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`));
289
+ // 6. Bold / italic / strikethrough.
290
+ // Delimiters must hug the text; underscore emphasis only at word
291
+ // boundaries so identifiers like snake_case are left untouched.
292
+ s = s.replace(/\*\*\*(?=\S)([\s\S]*?\S)\*\*\*/g, '<strong><em>$1</em></strong>');
293
+ s = s.replace(/(?<![\w*])___(?=\S)([\s\S]*?\S)___(?![\w*])/g, '<strong><em>$1</em></strong>');
294
+ s = s.replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, '<strong>$1</strong>');
295
+ s = s.replace(/(?<![\w*])__(?=\S)([\s\S]*?\S)__(?![\w*])/g, '<strong>$1</strong>');
296
+ s = s.replace(/\*(?=\S)([^*\n]*?\S)\*/g, '<em>$1</em>');
297
+ s = s.replace(/(?<![\w_])_(?=\S)([^_\n]*?\S)_(?![\w_])/g, '<em>$1</em>');
298
+ s = s.replace(/~~(?=\S)([\s\S]*?\S)~~/g, '<del>$1</del>');
299
+ // 7. Restore stashed HTML
300
+ return s.replace(/\u0000(\d+)\u0000/g, (_m, idx) => store[Number(idx)] ?? '');
301
+ }
302
+ // #endregion
303
+ // #region Security helpers
304
+ /** Escapes &, <, > and " for safe HTML interpolation. */
305
+ static escape(s) {
306
+ return s
307
+ .replace(/&/g, '&amp;')
308
+ .replace(/</g, '&lt;')
309
+ .replace(/>/g, '&gt;')
310
+ .replace(/"/g, '&quot;');
311
+ }
312
+ /**
313
+ * Returns a sanitized URL or undefined when the scheme is dangerous.
314
+ * Blocks javascript:, vbscript: and data: (also when obfuscated with
315
+ * whitespace/control characters, e.g. "java\tscript:").
316
+ */
317
+ static safeUrl(url) {
318
+ const compact = url.replace(/[\s\u0000-\u001f]/g, '');
319
+ if (/^(javascript|vbscript|data):/i.test(compact))
320
+ return undefined;
321
+ return url.replace(/"/g, '%22');
322
+ }
323
+ // #endregion
324
+ // #region Clipboard
325
+ /**
326
+ * Copies markdown content to the clipboard in two flavors:
327
+ * - text/html : the rendered HTML (rich paste into Word, Outlook, Gmail...)
328
+ * - text/plain : the original markdown source (paste into editors, IDEs...)
329
+ * @param markdown : the markdown source
330
+ * @param options : conversion options for the HTML flavor
331
+ * @returns : true on success
332
+ */
333
+ static async copyToClipboard(markdown, options) {
334
+ if (!markdown || typeof navigator === 'undefined' || !navigator.clipboard)
335
+ return false;
336
+ try {
337
+ if (typeof ClipboardItem !== 'undefined') {
338
+ const html = this.toHtml(markdown, options);
339
+ await navigator.clipboard.write([
340
+ new ClipboardItem({
341
+ 'text/html': new Blob([html], { type: 'text/html' }),
342
+ 'text/plain': new Blob([markdown], { type: 'text/plain' }),
343
+ }),
344
+ ]);
345
+ }
346
+ else {
347
+ // Older engines: plain text only
348
+ await navigator.clipboard.writeText(markdown);
349
+ }
350
+ return true;
351
+ }
352
+ catch {
353
+ // Clipboard API requires a secure context and user activation
354
+ return false;
355
+ }
356
+ }
357
+ /**
358
+ * Copies plain text (e.g. the content of a single code block) to the clipboard.
359
+ * @param text : the text to copy
360
+ * @returns : true on success
361
+ */
362
+ static async copyText(text) {
363
+ if (!text || typeof navigator === 'undefined' || !navigator.clipboard)
364
+ return false;
365
+ try {
366
+ await navigator.clipboard.writeText(text);
367
+ return true;
368
+ }
369
+ catch {
370
+ return false;
371
+ }
372
+ }
373
+ /**
374
+ * Extracts the visible plain text from a rendered element
375
+ * (what the user sees, entities already decoded by the browser).
376
+ * @param element : the element hosting the rendered markdown
377
+ * @returns : the plain text
378
+ */
379
+ static elementToText(element) {
380
+ return element.innerText ?? element.textContent ?? '';
381
+ }
382
+ }
383
+
384
+ var DateFormat;
385
+ (function (DateFormat) {
386
+ DateFormat[DateFormat["Short"] = 1] = "Short";
387
+ DateFormat[DateFormat["Long"] = 2] = "Long";
388
+ DateFormat[DateFormat["LongWithShortMonth"] = 3] = "LongWithShortMonth";
389
+ DateFormat[DateFormat["LongWithWeekDay"] = 4] = "LongWithWeekDay";
390
+ DateFormat[DateFormat["LongWithShortWeekDay"] = 5] = "LongWithShortWeekDay";
391
+ DateFormat[DateFormat["MonthAndYear"] = 6] = "MonthAndYear";
392
+ DateFormat[DateFormat["LongMonthAndYear"] = 7] = "LongMonthAndYear";
393
+ DateFormat[DateFormat["WeekDay"] = 8] = "WeekDay";
394
+ DateFormat[DateFormat["LongWeekDay"] = 9] = "LongWeekDay";
395
+ DateFormat[DateFormat["DayAndMonth"] = 10] = "DayAndMonth";
396
+ DateFormat[DateFormat["ShortUS"] = 11] = "ShortUS";
397
+ DateFormat[DateFormat["ShortISO8601"] = 12] = "ShortISO8601"; // yyyy-mm-dd
398
+ })(DateFormat || (DateFormat = {}));
399
+ class SystemUtils {
400
+ /** Shared collator for locale-aware, case-insensitive string comparison. */
401
+ static { this.collator = new Intl.Collator('it', { sensitivity: 'base', numeric: true }); }
402
+ /**
403
+ * Array find by key
404
+ * @param array : the array to scan
405
+ * @param key : key name
406
+ * @param value : the value to search for
407
+ * @returns : the property value or null
408
+ */
409
+ static arrayFindByKey(array, key, value) {
410
+ if (!array)
411
+ return undefined;
412
+ for (const item of array) {
413
+ if (item[key] === value) {
414
+ return item;
415
+ }
416
+ }
417
+ return undefined;
418
+ }
419
+ /**
420
+ * Array find index by key
421
+ * @param array : the array to scan
422
+ * @param key : the key name
423
+ * @param value : the value to search for
424
+ * @returns : the array index or -1 if not found
425
+ */
426
+ static arrayFindIndexByKey(array, key, value) {
427
+ if (!array)
428
+ return -1;
429
+ for (let i = 0; i < array.length; i++) {
430
+ if (array[i][key] === value) {
431
+ return i;
432
+ }
433
+ }
434
+ return -1;
435
+ }
436
+ /**
437
+ * Get a value from and array made of name|value items
438
+ * @param array : the array to scan
439
+ * @param value : the value to search for
440
+ * @returns : the property value or null if not found
441
+ */
442
+ static arrayGetValue(array, value) {
443
+ if (!array)
444
+ return undefined;
445
+ for (const item of array) {
446
+ const i = item;
447
+ if (i['value'] === value) {
448
+ return i['name'] ?? i['id'];
449
+ }
450
+ }
451
+ return undefined;
452
+ }
453
+ /**
454
+ * Convert items to nodes into a tree structure
455
+ * @param items : list of nodes
456
+ * @param parent : parent node
457
+ * @returns : an array of INode objects
458
+ */
459
+ static arrayToNodes(items, parent) {
460
+ const nodes = [];
461
+ items.forEach(n => {
462
+ const node = {
463
+ id: n.id.toString(),
464
+ name: n.name,
465
+ count: n.count ? n.count : 0,
466
+ children: undefined,
467
+ parent,
468
+ bag: n,
469
+ };
470
+ nodes.push(node);
471
+ node.children =
472
+ n.children && n.children.length > 0
473
+ ? this.arrayToNodes(n.children, node)
474
+ : [];
475
+ });
476
+ return nodes;
477
+ }
478
+ /**
479
+ * Comparator factory for sorting arrays of objects by a given property key.
480
+ * @param key - Name of the property to sort by.
481
+ * @param order - Sort direction: `'asc'` (default) or `'desc'`.
482
+ * @returns A comparator function that returns a negative, zero, or positive number.
483
+ */
484
+ static arraySortCompare(key, order = 'asc') {
485
+ const dir = order === 'desc' ? -1 : 1;
486
+ return (a, b) => {
487
+ const varA = a[key];
488
+ const varB = b[key];
489
+ if (varA === varB)
490
+ return 0;
491
+ if (varA === undefined || varA === null)
492
+ return -1 * dir;
493
+ if (varB === undefined || varB === null)
494
+ return 1 * dir;
495
+ if (typeof varA === 'string' && typeof varB === 'string') {
496
+ return SystemUtils.collator.compare(varA, varB) * dir;
497
+ }
498
+ return (varA > varB ? 1 : varA < varB ? -1 : 0) * dir;
499
+ };
500
+ }
501
+ /**
502
+ * Format weight
503
+ * @param gr : grams
504
+ * @returns : the formatted string
505
+ */
506
+ static formatWeight(gr) {
507
+ if (gr > 1000000)
508
+ return `${(gr / 1000000).toFixed(2)} t`;
509
+ else if (gr > 100000)
510
+ return `${(gr / 100000).toFixed(2)} q`;
511
+ else if (gr > 1000)
512
+ return `${(gr / 1000).toFixed(2)} kg`;
513
+ else
514
+ return `${gr} gr`;
515
+ }
516
+ /**
517
+ * Format file size
518
+ * @param bytes : number of bytes
519
+ * @returns : the formatted string
520
+ */
521
+ static formatFileSize(bytes) {
522
+ if (bytes > 1024000)
523
+ return `${(bytes / 1024000).toFixed(1)} MB`;
524
+ if (bytes > 1024)
525
+ return `${(bytes / 1024).toFixed(1)} Kb`;
526
+ return `${bytes} byte`;
527
+ }
528
+ /**
529
+ * Compare two strings (case-insensitive, locale-aware, accent-insensitive).
530
+ * @param a : string a
531
+ * @param b : string b
532
+ * @returns : 0 if equals, 1 if bigger, -1 if lower
533
+ */
534
+ static compareString(a, b) {
535
+ return this.collator.compare(a ?? '', b ?? '');
536
+ }
537
+ /**
538
+ * Capitalize a string
539
+ * @param s : the string to capitalize
540
+ * @returns : the capitalized string
541
+ */
542
+ static capitalize(s) {
543
+ if (!s)
544
+ return undefined;
545
+ let b = "";
546
+ let cap = true;
547
+ for (const char of s) {
548
+ if (char === " ") {
549
+ b += char;
550
+ cap = true;
551
+ }
552
+ else if (cap) {
553
+ b += char.toUpperCase();
554
+ cap = false;
555
+ }
556
+ else {
557
+ b += char.toLowerCase();
558
+ }
559
+ }
560
+ return b;
561
+ }
562
+ /**
563
+ * Truncate a string at the last word boundary before `max`.
564
+ * @param s : the string to truncate
565
+ * @param max : the max number of chars
566
+ * @returns : the truncated string
567
+ */
568
+ static truncate(s, max = 500) {
569
+ if (!s)
570
+ return undefined;
571
+ if (s.length < max)
572
+ return s;
573
+ const i = s.lastIndexOf(' ', max - 1);
574
+ return i > 0 ? s.slice(0, i) : s;
575
+ }
576
+ /**
577
+ * Join a list of strings
578
+ * @param items : the list of strings
579
+ * @param sep : the separator string
580
+ * @param max : the maximum resulting string
581
+ * @returns : the joined string
582
+ */
583
+ static join(items, sep = " ", max = 350) {
584
+ if (!items || items.length === 0)
585
+ return undefined;
586
+ if (items.length > 1) {
587
+ let l = 0;
588
+ let s = "";
589
+ while (s.length < max && items.length > l) {
590
+ if (l > 0) {
591
+ s += sep;
592
+ }
593
+ s += items[l++];
594
+ }
595
+ if (items.length > l) {
596
+ s += "...";
597
+ }
598
+ return s;
599
+ }
600
+ return items[0];
601
+ }
602
+ /**
603
+ * Normalize a string by converting it to lowercase and removing extra spaces, with special handling for acronyms and camelCase.
604
+ * @param s : the string to normalize
605
+ * @returns : The normalized string, or `undefined` if the input is falsy.
606
+ */
607
+ static normalizeDisplay(s) {
608
+ if (!s)
609
+ return s;
610
+ return s
611
+ .split(' ')
612
+ .map((word, wordIndex) => {
613
+ if (!word)
614
+ return word;
615
+ // If the word is all uppercase and contains at least one letter, we assume it's an acronym and leave it as is
616
+ if (word === word.toUpperCase() && /[A-Z]/.test(word)) {
617
+ return word;
618
+ }
619
+ // Otherwise, convert to lowercase letter by letter
620
+ const chars = word.split('');
621
+ return chars
622
+ .map((char, charIndex) => {
623
+ if (charIndex === 0 && wordIndex === 0) {
624
+ // First letter of the first word:
625
+ // remains uppercase only if the first 2 letters are both uppercase
626
+ const secondChar = chars[1] ?? '';
627
+ const keepUppercase = char === char.toUpperCase() &&
628
+ secondChar === secondChar.toUpperCase() &&
629
+ /[A-Z]/.test(char) &&
630
+ /[A-Z]/.test(secondChar);
631
+ return keepUppercase ? char : char.toLowerCase();
632
+ }
633
+ return char.toLowerCase();
634
+ })
635
+ .join('');
636
+ })
637
+ .join(' ');
638
+ }
639
+ /**
640
+ * Wraps bare URLs in the given string with `<a>` anchor tags.
641
+ * @param s - The plain-text or HTML string to process.
642
+ * @returns The string with URLs replaced by clickable links, or `''` when `s` is falsy.
643
+ */
644
+ static replaceAsHtml(s) {
645
+ if (!s)
646
+ return '';
647
+ return s.replace(/https?:\/\/[^\s<]+/gi, v => `<a href="${v}" target="_blank" rel="noopener">${v}</a>`);
648
+ }
649
+ /**
650
+ * Convert markdown to html
651
+ * @param markdown : the markdown data
652
+ * @param escapeHtml : true to escape HTML. Default is false
653
+ * @returns the html
654
+ */
655
+ static markdownToHtml(markdown, escapeHtml = false) {
656
+ return MarkdownUtils.toHtml(markdown, { escapeHtml: escapeHtml });
657
+ }
658
+ /**
659
+ * Compare two names
660
+ * @param a : name a
661
+ * @param b : name b
662
+ * @returns : true if a equals b
663
+ */
664
+ static compareNames(a, b) {
665
+ if (a)
666
+ a = a.trim();
667
+ if (b)
668
+ b = b.trim();
669
+ if (a && b && a.length !== b.length)
670
+ return false;
671
+ if (this.compareString(a, b) === 0)
672
+ return true;
673
+ const p1 = (a ?? '').split(' ');
674
+ const p2 = (b ?? '').split(' ');
675
+ if (p1.length !== p2.length)
676
+ return false;
677
+ let matches = p1.length;
678
+ for (const s1 of p1) {
679
+ for (const s2 of p2) {
680
+ if (this.compareString(s1, s2) === 0) {
681
+ matches--;
682
+ break;
683
+ }
684
+ }
685
+ }
686
+ return matches === 0;
687
+ }
688
+ /**
689
+ * Cipher a text
690
+ * @param text : the text
691
+ * @param key : the key
692
+ * @param reverse : true to decode, false to encode
693
+ * @returns :the ciphered text
694
+ */
695
+ static cipher(text, key, reverse = false) {
696
+ if (!text || !key)
697
+ return undefined;
698
+ // Surrogate pair limit
699
+ const bound = 0x10000;
700
+ const keyLen = key.length;
701
+ let result = '';
702
+ for (let i = 0; i < text.length; i++) {
703
+ let rotation = key.charCodeAt(i % keyLen);
704
+ if (reverse)
705
+ rotation = -rotation;
706
+ result += String.fromCharCode((text.charCodeAt(i) + rotation + bound) % bound);
707
+ }
708
+ return result;
709
+ }
710
+ /**
711
+ * Clone an object (deep copy).
712
+ * Uses native structuredClone: handles Date, Map, Set, typed arrays and circular references.
713
+ * @param obj : the object to clone
714
+ * @returns : a new object
715
+ */
716
+ static clone(obj) {
717
+ if (!obj)
718
+ return {};
719
+ return structuredClone(obj);
720
+ }
721
+ /**
722
+ * Creates a deep clone of an object.
723
+ * @param obj - The source object to clone.
724
+ * @param dest - Optional pre-allocated destination object to merge the clone into.
725
+ * @returns A deep copy of `obj`.
726
+ */
727
+ static deepClone(obj, dest) {
728
+ const cloned = structuredClone(obj);
729
+ if (dest) {
730
+ Object.assign(dest, cloned);
731
+ return dest;
732
+ }
733
+ return cloned;
734
+ }
735
+ /**
736
+ * Returns `true` when `value` is a syntactically valid UUID string.
737
+ * @param value - The string to validate.
738
+ */
739
+ static parseUUID(value) {
740
+ const regex = /^({|()?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}(}|))?$/;
741
+ return !!value && value.length > 0 && regex.test(value);
742
+ }
743
+ /**
744
+ * Returns `true` when `value` is a valid, non-empty (non-zero) UUID.
745
+ * @param value - The string to validate.
746
+ */
747
+ static parseUUIDNotEmpty(value) {
748
+ const regex = /^({|()?0{8}-(0{4}-){3}0{12}(}|))?$/;
749
+ return this.parseUUID(value) && !regex.test(value ?? '');
750
+ }
751
+ /**
752
+ * Return an empty UUID
753
+ * @returns : the empty UUID
754
+ */
755
+ static emptyUUID() { return "00000000-0000-0000-0000-000000000000"; }
756
+ ;
757
+ /**
758
+ * Create a new UUID
759
+ * @returns : the string UUID
760
+ */
761
+ static generateUUID() {
762
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
763
+ return crypto.randomUUID();
764
+ }
765
+ // Fallback (non-secure contexts)
766
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
767
+ const r = (Math.random() * 16) | 0;
768
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
769
+ return v.toString(16);
770
+ });
771
+ }
772
+ /**
773
+ * Reconstruct a standard UUID (with dashes) from a 32-char hex string without dashes.
774
+ * @param value : 32-character hex string
775
+ * @returns : the formatted UUID or the original string if it doesn't match the expected format
776
+ */
777
+ static inflateUUID(value) {
778
+ const s = value.trim();
779
+ if (s.length !== 32)
780
+ return s;
781
+ return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20)}`;
782
+ }
783
+ /** Precompiled validation patterns. */
784
+ 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; }
785
+ static { this.urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)\/?$/i; }
786
+ /**
787
+ * Parse a text and return true if it is a valid email (or empty)
788
+ * @param value : email
789
+ * @returns : true if the email is valid or empty
790
+ */
791
+ static parseEmail(value) {
792
+ if (!value)
793
+ return true;
794
+ return this.emailRegex.test(value.trim().toLowerCase());
795
+ }
796
+ /**
797
+ * Parse a text containing one or more email addresses separated by `;` or `,`
798
+ * and return true if all of them are valid (or the text is empty).
799
+ * Empty items caused by double or trailing separators are ignored.
800
+ * @param value : the email list to parse (e.g. "a@b.it; c@d.it")
801
+ * @returns : true if all emails are valid or the value is empty
802
+ */
803
+ static parseEmails(value) {
804
+ if (!value || value.trim().length === 0)
805
+ return true;
806
+ const items = value
807
+ .split(/[;,]/)
808
+ .map(e => e.trim().toLowerCase())
809
+ .filter(e => e.length > 0);
810
+ if (items.length === 0)
811
+ return false; // only separators, no addresses
812
+ return items.every(e => this.emailRegex.test(e));
813
+ }
814
+ /**
815
+ * Parse a text and return true if it is a valid url (or empty)
816
+ * @param value : the url to parse
817
+ * @returns : true if the url is valid or empty
818
+ */
819
+ static parseUrl(value) {
820
+ if (!value)
821
+ return true;
822
+ return this.urlRegex.test(value.trim().toLowerCase());
823
+ }
824
+ /**
825
+ * Get date parts from a string value
826
+ * @param value : the string to parse
827
+ * @returns : an array of numbers with year, month, day
828
+ */
829
+ static getDateParts(value) {
830
+ if (!value)
831
+ return undefined;
832
+ let parts = [];
833
+ if (value.indexOf("-") !== -1) {
834
+ const p = value.split("-");
835
+ if (p.length !== 3)
836
+ return undefined;
837
+ parts = [parseInt(p[0]), parseInt(p[1]) - 1, parseInt(p[2])];
838
+ }
839
+ else if (value.indexOf("/") !== -1) {
840
+ const p = value.split("/");
841
+ if (p.length !== 3)
842
+ return undefined;
843
+ parts = [parseInt(p[2]), parseInt(p[1]) - 1, parseInt(p[0])];
844
+ }
845
+ return parts;
846
+ }
847
+ /**
848
+ * Parse a date
849
+ * @param value : the value to check
850
+ * @param locale : the locale to use
851
+ * @returns : a valid Date object or null
852
+ */
853
+ static parseDate(value, locale = it) {
854
+ // No value at all
855
+ if (!value)
856
+ return undefined;
857
+ // A Date value
858
+ if (value instanceof Date && value.getTime())
859
+ return value;
860
+ // Parse known formats using date-fns
861
+ let d = parseISO(value);
862
+ if (d && d.getTime() && d.getFullYear() > 1750)
863
+ return d;
864
+ d = parse(value, 'dd/MM/yyyy', new Date(), { locale: locale });
865
+ if (d && d.getTime() && d.getFullYear() > 1750)
866
+ return d;
867
+ d = parse(value, 'yyyy-MM-dd', new Date(), { locale: locale });
868
+ if (d && d.getTime() && d.getFullYear() > 1750)
869
+ return d;
870
+ // Parse values manually
871
+ const parts = this.getDateParts(value);
872
+ if (!parts)
873
+ return undefined;
874
+ if (parts[0] < 100)
875
+ parts[0] += 2000;
876
+ if (isNaN(parts[0]) || parts[0] < 0 || parts[0] < 1750)
877
+ return undefined;
878
+ if (isNaN(parts[1]) || parts[1] < 0)
879
+ return undefined;
880
+ if (isNaN(parts[2]) || parts[2] < 0)
881
+ return undefined;
882
+ if (parts[1] > 11)
883
+ return undefined;
884
+ if (parts[1] === 1 && parts[2] > 29)
885
+ return undefined;
886
+ else if (parts[1] !== 1 && parts[2] > 31)
887
+ return undefined;
888
+ return new TZDate(parts[0], parts[1], parts[2], 12);
889
+ }
890
+ /**
891
+ * Format a date
892
+ * @param value : the date or string to format
893
+ * @param fmt : the DateFormat to use or the string pattern
894
+ * @param locale : the locale to use (default is IT)
895
+ * @returns : the formatted string
896
+ */
897
+ static formatDate(value, fmt = DateFormat.Short, locale = it) {
898
+ // No value at all
899
+ if (!value)
900
+ return '';
901
+ // A string
902
+ if (typeof value === 'string' || value instanceof String)
903
+ value = this.parseDate(value);
904
+ // Not a date
905
+ if (!(value instanceof Date && value.getTime()))
906
+ return '';
907
+ // Format
908
+ switch (fmt) {
909
+ case DateFormat.Short: return format(value, "dd/MM/yyyy", { locale: locale });
910
+ case DateFormat.Long: return format(value, "d MMMM yyyy", { locale: locale });
911
+ case DateFormat.LongWithShortMonth: return format(value, "d MMM yyyy", { locale: locale });
912
+ case DateFormat.LongWithWeekDay: return format(value, "EEEE, d MMMM yyyy", { locale: locale });
913
+ case DateFormat.LongWithShortWeekDay: return format(value, "EEE, d MMMM yyyy", { locale: locale });
914
+ case DateFormat.MonthAndYear: return format(value, "MMM yyyy", { locale: locale });
915
+ case DateFormat.LongMonthAndYear: return format(value, "MMMM yyyy", { locale: locale });
916
+ case DateFormat.WeekDay: return format(value, "EEE, d", { locale: locale });
917
+ case DateFormat.LongWeekDay: return format(value, "EEEE, d", { locale: locale });
918
+ case DateFormat.DayAndMonth: return format(value, "d MMMM", { locale: locale });
919
+ case DateFormat.ShortUS: return format(value, "MM/dd/yyyy", { locale: locale });
920
+ case DateFormat.ShortISO8601: return format(value, "yyyy-MM-dd", { locale: locale });
921
+ default:
922
+ return format(value, fmt, { locale: locale });
923
+ }
924
+ }
925
+ /**
926
+ * Return an italian local date
927
+ * @param value : the date
928
+ * @returns : the new date
929
+ */
930
+ static toLocalDate(value) {
931
+ // No value at all
932
+ if (!value)
933
+ return undefined;
934
+ // A string
935
+ if (typeof value === 'string' || value instanceof String)
936
+ value = this.parseDate(value);
937
+ // Not a date
938
+ if (!(value instanceof Date && value.getTime()))
939
+ return undefined;
940
+ // Update date
941
+ return new TZDate(value, "Europe/Rome");
942
+ }
943
+ /**
944
+ * Update a DateInterval object according to a string
945
+ * @param value : string value
946
+ * @param interval : DateInterval value to update
947
+ * @param end : true if must be updated the first or the end value
948
+ * @param copy : copy the same value (works only if not end element)
949
+ */
950
+ static changeDateInterval(value, interval, end = false, copy = false) {
951
+ if (value) {
952
+ let year = -1;
953
+ if (value.length === 4 && (year = parseInt(value)) > 1750) {
954
+ if (!end) {
955
+ interval.from = new TZDate(new Date(year, 0, 1), 'Europe/Rome');
956
+ interval.to = new TZDate(new Date(year, 11, 31), 'Europe/Rome');
957
+ }
958
+ else
959
+ interval.to = new Date(year, 11, 31);
960
+ }
961
+ else {
962
+ let parts = this.getDateParts(value);
963
+ if (!parts || isNaN(parts[0]) || parts[0] < 0 || parts[0] < 1750)
964
+ return;
965
+ const d = new TZDate(new Date(parts[0], parts[1], parts[2]), 'Europe/Rome');
966
+ if (end)
967
+ interval.to = d;
968
+ else if (copy) {
969
+ interval.from = d;
970
+ interval.to = d;
971
+ }
972
+ else
973
+ interval.from = d;
974
+ }
975
+ }
976
+ }
977
+ /**
978
+ * Formats a number using `Intl.NumberFormat`.
979
+ * @param value - The number to format.
980
+ * @param decimals - Maximum decimal places (default: `2`).
981
+ * @param locale - BCP 47 locale tag (default: `'it-IT'`).
982
+ * @returns The formatted number string.
983
+ */
984
+ static formatNumber(value, decimals = 2, locale = 'it-IT') {
985
+ return Intl.NumberFormat(locale, { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: decimals }).format(value);
986
+ }
987
+ /**
988
+ * Formats a number as a currency string using `Intl.NumberFormat`.
989
+ * @param value - The number to format.
990
+ * @param currency - ISO 4217 currency code (default: `'EUR'`).
991
+ * @param decimals - Maximum decimal places (default: `2`).
992
+ * @param locale - BCP 47 locale tag (default: `'it-IT'`).
993
+ * @returns The formatted currency string.
994
+ */
995
+ static formatCurrency(value, currency = 'EUR', decimals = 2, locale = 'it-IT') {
996
+ return Intl.NumberFormat(locale, { style: 'currency', currency, minimumFractionDigits: 0, maximumFractionDigits: decimals }).format(value);
997
+ }
998
+ /**
999
+ * Percent-encodes a string for safe inclusion in a URL.
1000
+ * @param value - The string to encode.
1001
+ * @returns The encoded string, or `undefined` when `value` is empty.
1002
+ */
1003
+ static urlEncode(value) {
1004
+ return value.length > 0
1005
+ ? encodeURIComponent(value)
1006
+ : undefined;
1007
+ }
1008
+ /**
1009
+ * Decodes a percent-encoded URL string, treating `+` as a space.
1010
+ * @param value - The encoded string to decode.
1011
+ * @returns The decoded string, or `undefined` when `value` is empty or absent.
1012
+ */
1013
+ static urlDecode(value) {
1014
+ return value && value.length > 0
1015
+ ? decodeURIComponent(value.replace(/\+/g, '%20'))
1016
+ : undefined;
1017
+ }
1018
+ /**
1019
+ * Reads a query string parameter value from the current page URL.
1020
+ * @param name - The parameter name to look up.
1021
+ * @returns The decoded parameter value, or `undefined` when absent or running server-side.
1022
+ */
1023
+ static getQueryStringValueByName(name) {
1024
+ if (!this.isBrowser())
1025
+ return undefined;
1026
+ const v = new URLSearchParams(window.location.search).get(name);
1027
+ return v && v !== 'null' ? v : undefined;
1028
+ }
1029
+ /**
1030
+ * Generate a password
1031
+ * @returns : the password string
1032
+ */
1033
+ static generatePassword() {
1034
+ const random = "$" + Math.random().toString(36).slice(-11);
1035
+ let password = "";
1036
+ let hasUpperCase = false;
1037
+ for (const rnd of random) {
1038
+ if (!hasUpperCase && "abcdefghjilmnopqrstuvywxz".includes(rnd)) {
1039
+ password += rnd.toUpperCase();
1040
+ hasUpperCase = true;
1041
+ }
1042
+ else {
1043
+ password += rnd;
1044
+ }
1045
+ }
1046
+ if (!hasUpperCase)
1047
+ password = password.substring(0, 11) + "M";
1048
+ return password;
1049
+ }
1050
+ /**
1051
+ * Calculate password strength
1052
+ * @param password: the password to evaluate
1053
+ * @returns the password strength info
1054
+ */
1055
+ static calculatePasswordStrength(password) {
1056
+ if (password && password.length > 0) {
1057
+ let score = 0;
1058
+ const suggestions = [];
1059
+ // Length
1060
+ if (password.length >= 10)
1061
+ score++;
1062
+ else
1063
+ suggestions.push('Usa almeno 10 caratteri.');
1064
+ if (password.length >= 12)
1065
+ score++;
1066
+ else if (password.length >= 10)
1067
+ suggestions.push('Considera di usare più di 12 caratteri.');
1068
+ // Lowercase letters
1069
+ if (/[a-z]/.test(password))
1070
+ score++;
1071
+ else
1072
+ suggestions.push('Aggiungi lettere minuscole.');
1073
+ // Uppercase letters
1074
+ if (/[A-Z]/.test(password))
1075
+ score++;
1076
+ else
1077
+ suggestions.push('Aggiungi lettere maiuscole.');
1078
+ // Numbers
1079
+ if (/\d/.test(password))
1080
+ score++;
1081
+ else
1082
+ suggestions.push('Aggiungi numeri.');
1083
+ // Special characters
1084
+ if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password))
1085
+ score++;
1086
+ else
1087
+ suggestions.push('Aggiungi caratteri speciali (!@#$%^&*).');
1088
+ // Common patterns
1089
+ if (/(.)\1{2,}/.test(password)) {
1090
+ score = Math.max(0, score - 1);
1091
+ suggestions.push('Evita di ripetere lo stesso carattere.');
1092
+ }
1093
+ if (/123|abc|qwe/i.test(password)) {
1094
+ score = Math.max(0, score - 1);
1095
+ suggestions.push('Evita sequenze comuni (123, abc, qwe).');
1096
+ }
1097
+ // Label and color
1098
+ let label;
1099
+ let color;
1100
+ let isValid;
1101
+ if (score <= 2) {
1102
+ label = 'Molto debole';
1103
+ color = '#f44336';
1104
+ isValid = false;
1105
+ }
1106
+ else if (score <= 3) {
1107
+ label = 'Debole';
1108
+ color = '#ff9800';
1109
+ isValid = false;
1110
+ }
1111
+ else if (score <= 4) {
1112
+ label = 'Media';
1113
+ color = '#ffc107';
1114
+ isValid = true;
1115
+ }
1116
+ else if (score <= 5) {
1117
+ label = 'Forte';
1118
+ color = '#8bc34a';
1119
+ isValid = true;
1120
+ }
1121
+ else {
1122
+ label = 'Molto forte';
1123
+ color = '#4caf50';
1124
+ isValid = true;
1125
+ }
1126
+ return { score, label, color, suggestions, isValid };
1127
+ }
1128
+ else
1129
+ return {
1130
+ score: 0,
1131
+ isValid: false,
1132
+ suggestions: []
1133
+ };
1134
+ }
1135
+ /**
1136
+ * Check if current browser supports touch
1137
+ * @returns : true if the display is touchable
1138
+ */
1139
+ static isTouchable() {
1140
+ return (this.isBrowser() && ("ontouchstart" in window ||
1141
+ navigator.maxTouchPoints > 0));
1142
+ }
1143
+ /**
1144
+ * This check will prevent 'window' logic to be executed
1145
+ * while executing the server rendering
1146
+ * @returns : true if using the browser
1147
+ */
1148
+ static isBrowser() {
1149
+ return typeof (window) !== 'undefined';
1150
+ }
1151
+ /**
1152
+ * Convert folders in a tree of Node object.
1153
+ * @param folders : the subfolders group or null to root
1154
+ * @returns : a node list
1155
+ */
1156
+ static toNodes(folders) {
1157
+ return this._toNodes(folders, undefined);
1158
+ }
1159
+ /**
1160
+ * Convert folders in a tree of Node object.
1161
+ * @param folders : the children group or null to root
1162
+ * @param parent : the parent node
1163
+ * @returns : a node list
1164
+ */
1165
+ static _toNodes(folders, parent) {
1166
+ const nodes = [];
1167
+ folders.forEach((n) => {
1168
+ const node = {
1169
+ id: n.id,
1170
+ name: n.name,
1171
+ count: n.count,
1172
+ parent: parent,
1173
+ children: undefined,
1174
+ bag: n,
1175
+ };
1176
+ const children = n.children ?? n.subFolders ?? [];
1177
+ if (children.length > 0) {
1178
+ node.children = this._toNodes(children, node);
1179
+ }
1180
+ nodes.push(node);
1181
+ });
1182
+ return nodes;
1183
+ }
1184
+ /**
1185
+ * Returns an array of individual power-of-2 flag values that are set in `value`.
1186
+ * @param value - The bitmask to decompose.
1187
+ * @param max - Upper-bound exponent: checks flags from `1` up to `1 << max` (default: `30`).
1188
+ * @returns Array of set flag values, or an empty array when `value` is `0`.
1189
+ */
1190
+ static getFlags(value, max = 30) {
1191
+ if (value !== 0) {
1192
+ const items = [];
1193
+ let v = 1;
1194
+ const m = 1 << max;
1195
+ while (v < m) {
1196
+ if ((value & v) === v)
1197
+ items.push(v);
1198
+ v = v << 1;
1199
+ }
1200
+ return items;
1201
+ }
1202
+ else
1203
+ return [];
1204
+ }
1205
+ /** Cache for resolved color luminance results. */
1206
+ static { this.colorLightCache = new Map(); }
1207
+ /**
1208
+ * Check if a color is light or dark
1209
+ * @param color : the color
1210
+ * @param minimumLuminance : the lumimance to consider
1211
+ * @returns true if the color is light
1212
+ */
1213
+ static isColorLight(color, minimumLuminance = 186) {
1214
+ if (!this.isBrowser())
1215
+ return true; // SSR fallback
1216
+ const cacheKey = `${color}|${minimumLuminance}`;
1217
+ const cached = this.colorLightCache.get(cacheKey);
1218
+ if (cached !== undefined)
1219
+ return cached;
1220
+ const tempDiv = document.createElement('div');
1221
+ tempDiv.style.color = color;
1222
+ document.body.appendChild(tempDiv);
1223
+ const rgb = getComputedStyle(tempDiv).color;
1224
+ document.body.removeChild(tempDiv);
1225
+ const result = rgb.match(/\d+/g);
1226
+ let light = true; // fallback
1227
+ if (result && result.length >= 3) {
1228
+ const r = parseInt(result[0], 10);
1229
+ const g = parseInt(result[1], 10);
1230
+ const b = parseInt(result[2], 10);
1231
+ light = (0.299 * r + 0.587 * g + 0.114 * b) > minimumLuminance;
1232
+ }
1233
+ this.colorLightCache.set(cacheKey, light);
1234
+ return light;
1235
+ }
1236
+ }
1237
+
1238
+ /**
1239
+ * General-purpose formatting pipe that converts a raw value to a locale-aware string
1240
+ * based on the specified format type.
1241
+ *
1242
+ * Supported types: `'date'` / `'D'`, `'currency'` / `'C'`, `'number'` / `'N'`,
1243
+ * `'number0'` / `'N0'`, `'percentage'` / `'P'`.
1244
+ *
1245
+ * Usage: `{{ value | format:'currency' }}`
1246
+ */
1247
+ class FormatPipe {
1248
+ /**
1249
+ * Formats a value according to the specified type and optional pattern.
1250
+ * Returns `undefined` when the value is `null` or `undefined`, or when the type is unrecognised.
1251
+ * @param value - The raw value to format.
1252
+ * @param type - The format type identifier (default: `'date'`).
1253
+ * @param pattern - The date pattern used when `type` is `'date'` (default: `'dd/MM/yyyy'`).
1254
+ * @returns A formatted string, or `undefined` when the value cannot be formatted.
1255
+ */
1256
+ transform(value, type = 'date', pattern = 'dd/MM/yyyy') {
1257
+ if (value === undefined || value === null)
1258
+ return undefined;
1259
+ switch (type) {
1260
+ case 'D':
1261
+ case 'date': {
1262
+ const d = SystemUtils.parseDate(value, it);
1263
+ if (d)
1264
+ return format(d, pattern, { locale: it });
1265
+ break;
1266
+ }
1267
+ case 'C':
1268
+ case 'currency':
1269
+ return new Intl.NumberFormat('it-IT', { style: 'currency', currency: 'EUR' }).format(value);
1270
+ case 'N':
1271
+ case 'number':
1272
+ return new Intl.NumberFormat('it-IT', { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(value);
1273
+ case 'N0':
1274
+ case 'number0':
1275
+ return new Intl.NumberFormat('it-IT', { style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(value);
1276
+ case 'P':
1277
+ case 'percentage':
1278
+ return new Intl.NumberFormat('it-IT', { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(value);
1279
+ }
1280
+ return undefined;
1281
+ }
1282
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1283
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: FormatPipe, isStandalone: true, name: "format" }); }
1284
+ }
1285
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatPipe, decorators: [{
1286
+ type: Pipe,
1287
+ args: [{
1288
+ name: 'format',
1289
+ standalone: true
1290
+ }]
1291
+ }] });
1292
+
1293
+ /**
1294
+ * Pipe that applies a global regex replacement on a string and returns the result
1295
+ * as sanitized HTML. When `regexValue` is `'\n'` and no `replaceValue` is given,
1296
+ * newlines are replaced with `<br>` tags.
1297
+ *
1298
+ * Usage: `{{ text | replace:'\n':'' }}`
1299
+ */
1300
+ class ReplacePipe {
1301
+ constructor() {
1302
+ this.sanitizer = inject(DomSanitizer);
1303
+ }
1304
+ /**
1305
+ * Replaces all occurrences of `regexValue` in `value` with `replaceValue`.
1306
+ * Returns `undefined` when `value` is empty or `undefined`.
1307
+ * @param value - The source string to process.
1308
+ * @param regexValue - The regex pattern string to match (applied with the global flag).
1309
+ * @param replaceValue - The replacement string. Defaults to `'<br>'` when `regexValue` is `'\n'` and this is falsy.
1310
+ * @returns A `SafeHtml` value with all matches replaced, or `undefined` when the input is empty.
1311
+ */
1312
+ transform(value, regexValue, replaceValue) {
1313
+ if (!value)
1314
+ return undefined;
1315
+ const replacement = (regexValue === '\n' && !replaceValue) ? '<br>' : replaceValue;
1316
+ return this.sanitizer.bypassSecurityTrustHtml(value.replace(new RegExp(regexValue, 'g'), replacement));
1317
+ }
1318
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ReplacePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1319
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: ReplacePipe, isStandalone: true, name: "replace" }); }
1320
+ }
1321
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ReplacePipe, decorators: [{
1322
+ type: Pipe,
1323
+ args: [{
1324
+ name: 'replace',
1325
+ standalone: true
1326
+ }]
1327
+ }] });
1328
+
1329
+ /**
1330
+ * Pipe that marks an HTML string as trusted so Angular does not escape it when
1331
+ * bound via `[innerHTML]`.
1332
+ *
1333
+ * Usage: `<div [innerHTML]="html | safeHtml"></div>`
1334
+ */
1335
+ class SafeHtmlPipe {
1336
+ constructor() {
1337
+ this.sanitizer = inject(DomSanitizer);
1338
+ }
1339
+ /**
1340
+ * Bypasses Angular's HTML sanitization and returns a `SafeHtml` instance.
1341
+ * @param value - The raw HTML string to trust. Treated as an empty string when `undefined`.
1342
+ * @returns A `SafeHtml` value that can be bound to `[innerHTML]` without escaping.
1343
+ */
1344
+ transform(value) {
1345
+ return this.sanitizer.bypassSecurityTrustHtml(value ?? '');
1346
+ }
1347
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SafeHtmlPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1348
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: SafeHtmlPipe, isStandalone: true, name: "safeHtml" }); }
1349
+ }
1350
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SafeHtmlPipe, decorators: [{
1351
+ type: Pipe,
1352
+ args: [{
1353
+ name: 'safeHtml',
1354
+ standalone: true
1355
+ }]
1356
+ }] });
1357
+
1358
+ /**
1359
+ * Pipe that marks a URL string as a trusted resource URL so Angular does not block it
1360
+ * when bound to attributes such as `[src]` or `[href]` on iframes, objects, or embeds.
1361
+ *
1362
+ * Usage: `<iframe [src]="url | safeUrl"></iframe>`
1363
+ */
1364
+ class SafeUrlPipe {
1365
+ constructor() {
1366
+ this.sanitizer = inject(DomSanitizer);
1367
+ }
1368
+ /**
1369
+ * Bypasses Angular's resource-URL sanitization and returns a `SafeResourceUrl` instance.
1370
+ * @param value - The URL string to trust. Treated as an empty string when `undefined`.
1371
+ * @returns A `SafeResourceUrl` that can be bound to resource URL attributes without blocking.
1372
+ */
1373
+ transform(value) {
1374
+ return this.sanitizer.bypassSecurityTrustResourceUrl(value ?? '');
1375
+ }
1376
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SafeUrlPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1377
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: SafeUrlPipe, isStandalone: true, name: "safeUrl" }); }
1378
+ }
1379
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SafeUrlPipe, decorators: [{
1380
+ type: Pipe,
1381
+ args: [{
1382
+ name: 'safeUrl',
1383
+ standalone: true
1384
+ }]
1385
+ }] });
1386
+
1387
+ /**
1388
+ * Impure pipe that filters an array using a caller-provided predicate function.
1389
+ * Because the pipe is impure it re-evaluates on every change-detection cycle,
1390
+ * which is necessary when the predicate's captured state changes.
1391
+ *
1392
+ * Usage: `*ngFor="let item of items | callback:myFilter"`
1393
+ */
1394
+ class SearchCallbackPipe {
1395
+ /**
1396
+ * Filters `items` by applying `callback` to each element.
1397
+ * Returns the original array unchanged when either argument is falsy.
1398
+ * @param items - The source array to filter. May be `undefined`.
1399
+ * @param callback - A predicate function that returns `true` for items to keep.
1400
+ * @returns A new filtered array, the original array when no callback is provided,
1401
+ * or `undefined` when `items` is `undefined`.
1402
+ */
1403
+ transform(items, callback) {
1404
+ if (!items || !callback)
1405
+ return items;
1406
+ return items.filter(item => callback(item));
1407
+ }
1408
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SearchCallbackPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1409
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: SearchCallbackPipe, isStandalone: true, name: "callback", pure: false }); }
1410
+ }
1411
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SearchCallbackPipe, decorators: [{
1412
+ type: Pipe,
1413
+ args: [{
1414
+ name: 'callback',
1415
+ pure: false,
1416
+ standalone: true
1417
+ }]
1418
+ }] });
1419
+
1420
+ /**
1421
+ * Impure pipe that filters an array of searchable items against a text query.
1422
+ *
1423
+ * Each item is matched either via its `searchBag.name` property (when present)
1424
+ * or by converting the item itself to a lowercase string. The optional `metadata`
1425
+ * argument is updated in-place with the total item count and the filtered count,
1426
+ * making it usable in the template alongside `*ngFor`.
1427
+ *
1428
+ * Usage:
1429
+ * ```html
1430
+ * <div *ngFor="let item of items | search:filterText:meta">...</div>
1431
+ * <div>Showing {{ meta.count }} of {{ meta.total }}</div>
1432
+ * ```
1433
+ */
1434
+ class SearchFilterPipe {
1435
+ /**
1436
+ * Filters `items` by performing a case-insensitive substring match against `value`.
1437
+ * When `items` or `value` is falsy the original array is returned unfiltered.
1438
+ * @param items - The source array to filter. May be `undefined`.
1439
+ * @param value - The search text to match against each item. May be `undefined`.
1440
+ * @param metadata - Optional object that is updated with `total` and `count` after filtering.
1441
+ * @returns The filtered array, the original array when no filter text is given,
1442
+ * or `undefined` when `items` is `undefined`.
1443
+ */
1444
+ transform(items, value, metadata) {
1445
+ metadata ??= { total: 0, count: 0 };
1446
+ if (!items || !value)
1447
+ return items;
1448
+ const query = value.toLowerCase();
1449
+ const result = items.filter(item => {
1450
+ if (!item)
1451
+ return false;
1452
+ const text = item.searchBag?.name ?? (typeof item === 'string' ? item : undefined);
1453
+ return text?.toLowerCase().includes(query) ?? false;
1454
+ });
1455
+ metadata.total = items.length;
1456
+ metadata.count = result.length;
1457
+ return result;
1458
+ }
1459
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SearchFilterPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1460
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: SearchFilterPipe, isStandalone: true, name: "search" }); }
1461
+ }
1462
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SearchFilterPipe, decorators: [{
1463
+ type: Pipe,
1464
+ args: [{
1465
+ name: 'search',
1466
+ standalone: true
1467
+ }]
1468
+ }] });
1469
+
1470
+ /**
1471
+ * Pipe that converts plain-text newlines (`\r\n`, `\r`, `\n`) to HTML `<br>` tags
1472
+ * and marks the result as trusted HTML so Angular does not escape it.
1473
+ *
1474
+ * Usage: `{{ text | formatHtml }}`
1475
+ */
1476
+ class FormatHtmlPipe {
1477
+ constructor() {
1478
+ this.sanitizer = inject(DomSanitizer);
1479
+ }
1480
+ /**
1481
+ * Transforms a plain-text string into sanitized HTML by replacing newline characters
1482
+ * with `<br>` tags.
1483
+ * @param value - The input string to transform. Treated as an empty string when `undefined`.
1484
+ * @returns A `SafeHtml` value that can be rendered with `[innerHTML]`.
1485
+ */
1486
+ transform(value) {
1487
+ return this.sanitizer.bypassSecurityTrustHtml((value ?? '').replaceAll(/(?:\r\n|\r|\n)/g, '<br>'));
1488
+ }
1489
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatHtmlPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1490
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: FormatHtmlPipe, isStandalone: true, name: "formatHtml" }); }
1491
+ }
1492
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatHtmlPipe, decorators: [{
1493
+ type: Pipe,
1494
+ args: [{
1495
+ name: 'formatHtml',
1496
+ standalone: true
1497
+ }]
1498
+ }] });
1499
+
1500
+ /**
1501
+ * Standalone providers for the scm-utils "core" layer.
1502
+ *
1503
+ * Registers the core pipes as injectable services so they can be used
1504
+ * via `inject()` in services, guards, and resolvers — not only in templates.
1505
+ * Components that use these pipes only in templates should import them
1506
+ * directly via `imports: [FormatPipe, SafeHtmlPipe, ...]` instead.
1507
+ *
1508
+ * @example
1509
+ * bootstrapApplication(AppComponent, {
1510
+ * providers: [provideSCMCore()]
1511
+ * });
1512
+ */
1513
+ function provideSCMCore() {
1514
+ return makeEnvironmentProviders([
1515
+ SearchFilterPipe,
1516
+ SearchCallbackPipe,
1517
+ SafeHtmlPipe,
1518
+ SafeUrlPipe,
1519
+ ReplacePipe,
1520
+ FormatPipe,
1521
+ FormatHtmlPipe
1522
+ ]);
1523
+ }
1524
+
1525
+ /**
1526
+ * Creates an array of the given length, filling each slot with the result of `valueFunction`.
1527
+ * @param length - Number of elements to create.
1528
+ * @param valueFunction - Factory called with each index to produce the element value.
1529
+ * @returns Typed array of `length` elements.
1530
+ */
1531
+ function range(length, valueFunction) {
1532
+ const valuesArray = Array(length);
1533
+ for (let i = 0; i < length; i++) {
1534
+ valuesArray[i] = valueFunction(i);
1535
+ }
1536
+ return valuesArray;
1537
+ }
1538
+ // date-fns doesn't have a way to read/print month names or days of the week directly,
1539
+ // so we get them by formatting a date with a format that produces the desired month/day.
1540
+ const MONTH_FORMATS = {
1541
+ long: 'LLLL',
1542
+ short: 'LLL',
1543
+ narrow: 'LLLLL',
1544
+ };
1545
+ const DAY_OF_WEEK_FORMATS = {
1546
+ long: 'EEEE',
1547
+ short: 'EEE',
1548
+ narrow: 'EEEEE',
1549
+ };
1550
+ const MAT_DATE_FNS_FORMATS = {
1551
+ parse: {
1552
+ dateInput: 'P',
1553
+ },
1554
+ display: {
1555
+ dateInput: 'P',
1556
+ monthYearLabel: 'LLL uuuu',
1557
+ dateA11yLabel: 'PP',
1558
+ monthYearA11yLabel: 'LLLL uuuu',
1559
+ },
1560
+ };
1561
+ /**
1562
+ * date-fns adapter that integrates Angular Material's date picker with the date-fns library,
1563
+ * applying `Europe/Rome` timezone for all parsed and created dates.
1564
+ */
1565
+ class DateFnsAdapter extends DateAdapter {
1566
+ constructor() {
1567
+ super();
1568
+ const matDateLocale = inject(MAT_DATE_LOCALE, { optional: true });
1569
+ if (matDateLocale) {
1570
+ this.setLocale(matDateLocale);
1571
+ }
1572
+ }
1573
+ /**
1574
+ * Returns the year component of the given date.
1575
+ * @param date - The source date.
1576
+ */
1577
+ getYear(date) {
1578
+ return getYear(date);
1579
+ }
1580
+ /**
1581
+ * Returns the zero-based month index of the given date (0 = January).
1582
+ * @param date - The source date.
1583
+ */
1584
+ getMonth(date) {
1585
+ return getMonth(date);
1586
+ }
1587
+ /**
1588
+ * Returns the day-of-month of the given date (1-based).
1589
+ * @param date - The source date.
1590
+ */
1591
+ getDate(date) {
1592
+ return getDate(date);
1593
+ }
1594
+ /**
1595
+ * Returns the day-of-week of the given date (0 = Sunday).
1596
+ * @param date - The source date.
1597
+ */
1598
+ getDayOfWeek(date) {
1599
+ return getDay(date);
1600
+ }
1601
+ /**
1602
+ * Returns an array of 12 month name strings formatted for the active locale.
1603
+ * @param style - One of `'long'`, `'short'`, or `'narrow'`.
1604
+ */
1605
+ getMonthNames(style) {
1606
+ const pattern = MONTH_FORMATS[style];
1607
+ return range(12, i => this.format(new Date(2017, i, 1), pattern));
1608
+ }
1609
+ /**
1610
+ * Returns an array of 31 day-of-month label strings formatted using `Intl.DateTimeFormat`
1611
+ * when available, falling back to plain numeric strings.
1612
+ */
1613
+ getDateNames() {
1614
+ const dtf = typeof Intl !== 'undefined'
1615
+ ? new Intl.DateTimeFormat(this.locale?.code, {
1616
+ day: 'numeric',
1617
+ timeZone: 'utc',
1618
+ })
1619
+ : null;
1620
+ return range(31, i => {
1621
+ if (dtf) {
1622
+ // date-fns doesn't appear to support this functionality.
1623
+ // Fall back to `Intl` on supported browsers.
1624
+ const date = new Date();
1625
+ date.setUTCFullYear(2017, 0, i + 1);
1626
+ date.setUTCHours(0, 0, 0, 0);
1627
+ return dtf.format(date).replace(/[\u200e\u200f]/g, '');
1628
+ }
1629
+ return String(i + 1);
1630
+ });
1631
+ }
1632
+ /**
1633
+ * Returns an array of 7 day-of-week name strings formatted for the active locale.
1634
+ * @param style - One of `'long'`, `'short'`, or `'narrow'`.
1635
+ */
1636
+ getDayOfWeekNames(style) {
1637
+ const pattern = DAY_OF_WEEK_FORMATS[style];
1638
+ return range(7, i => this.format(new Date(2017, 0, i + 1), pattern));
1639
+ }
1640
+ /**
1641
+ * Returns the four-digit year string for the given date.
1642
+ * @param date - The source date.
1643
+ */
1644
+ getYearName(date) {
1645
+ return this.format(date, 'y');
1646
+ }
1647
+ /**
1648
+ * Returns the first day of the week for the active locale (0 = Sunday, 1 = Monday, …).
1649
+ */
1650
+ getFirstDayOfWeek() {
1651
+ return this.locale?.options?.weekStartsOn ?? 0;
1652
+ }
1653
+ /**
1654
+ * Returns the number of days in the month of the given date.
1655
+ * @param date - The source date.
1656
+ */
1657
+ getNumDaysInMonth(date) {
1658
+ return getDaysInMonth(date);
1659
+ }
1660
+ /**
1661
+ * Creates an independent copy of the given date.
1662
+ * @param date - The date to clone.
1663
+ */
1664
+ clone(date) {
1665
+ return new Date(date.getTime());
1666
+ }
1667
+ /**
1668
+ * Creates a `Date` in the `Europe/Rome` timezone for the given year, month, and day.
1669
+ * Throws an `Error` when any component is out of range.
1670
+ * @param year - Full four-digit year.
1671
+ * @param month - Zero-based month index (0 = January, 11 = December).
1672
+ * @param date - Day-of-month (1-based).
1673
+ */
1674
+ createDate(year, month, date) {
1675
+ if (month < 0 || month > 11) {
1676
+ throw Error(`Invalid month index "${month}". Month index has to be between 0 and 11.`);
1677
+ }
1678
+ if (date < 1) {
1679
+ throw Error(`Invalid date "${date}". Date has to be greater than 0.`);
1680
+ }
1681
+ // Passing the year to the constructor causes year numbers <100 to be converted to 19xx.
1682
+ // To work around this we use `setFullYear` and `setHours` instead.
1683
+ const result = new Date();
1684
+ result.setFullYear(year, month, date);
1685
+ result.setHours(0, 0, 0, 0);
1686
+ const result2 = new TZDate(result, 'Europe/Rome');
1687
+ if (result2.getMonth() !== month) {
1688
+ throw Error(`Invalid date "${date}" for month with index "${month}".`);
1689
+ }
1690
+ return result2;
1691
+ }
1692
+ /**
1693
+ * Returns today's date in the local timezone.
1694
+ */
1695
+ today() {
1696
+ return new Date();
1697
+ }
1698
+ /**
1699
+ * Parses a value into a `Date`.
1700
+ * - Strings are first attempted as ISO 8601, then matched against each format in `parseFormat`.
1701
+ * - Numbers are treated as Unix timestamps (milliseconds).
1702
+ * - Existing `Date` instances are cloned.
1703
+ * @param value - The value to parse.
1704
+ * @param parseFormat - A format string or an array of format strings (date-fns tokens).
1705
+ * @returns A valid `Date` in `Europe/Rome`, an invalid sentinel, or `null` for unrecognised input.
1706
+ */
1707
+ parse(value, parseFormat) {
1708
+ if (typeof value === 'string' && value.length > 0) {
1709
+ const iso8601Date = parseISO(value);
1710
+ if (this.isValid(iso8601Date)) {
1711
+ return new TZDate(iso8601Date, 'Europe/Rome');
1712
+ }
1713
+ const formats = Array.isArray(parseFormat) ? parseFormat : [parseFormat];
1714
+ if (!formats.length) {
1715
+ throw Error('Formats array must not be empty.');
1716
+ }
1717
+ for (const currentFormat of formats) {
1718
+ const fromFormat = parse(value, currentFormat, new Date(), { locale: this.locale });
1719
+ if (this.isValid(fromFormat)) {
1720
+ return new TZDate(fromFormat, 'Europe/Rome');
1721
+ }
1722
+ }
1723
+ return this.invalid();
1724
+ }
1725
+ else if (typeof value === 'number') {
1726
+ return new Date(value);
1727
+ }
1728
+ else if (value instanceof Date) {
1729
+ return this.clone(value);
1730
+ }
1731
+ return null;
1732
+ }
1733
+ /**
1734
+ * Formats a `Date` using the given date-fns display format string.
1735
+ * Throws an `Error` when `date` is not valid.
1736
+ * @param date - The date to format.
1737
+ * @param displayFormat - A date-fns format string (e.g. `'P'`, `'LLL uuuu'`).
1738
+ */
1739
+ format(date, displayFormat) {
1740
+ if (!this.isValid(date)) {
1741
+ throw Error('DateFnsAdapter: Cannot format invalid date.');
1742
+ }
1743
+ return format(date, displayFormat, { locale: this.locale });
1744
+ }
1745
+ /**
1746
+ * Adds the given number of whole years to a date.
1747
+ * @param date - The base date.
1748
+ * @param years - Number of years to add (can be negative).
1749
+ */
1750
+ addCalendarYears(date, years) {
1751
+ return addYears(date, years);
1752
+ }
1753
+ /**
1754
+ * Adds the given number of whole months to a date.
1755
+ * @param date - The base date.
1756
+ * @param months - Number of months to add (can be negative).
1757
+ */
1758
+ addCalendarMonths(date, months) {
1759
+ return addMonths(date, months);
1760
+ }
1761
+ /**
1762
+ * Adds the given number of whole days to a date.
1763
+ * @param date - The base date.
1764
+ * @param days - Number of days to add (can be negative).
1765
+ */
1766
+ addCalendarDays(date, days) {
1767
+ return addDays(date, days);
1768
+ }
1769
+ /**
1770
+ * Serialises a date to an ISO 8601 date string (`yyyy-MM-dd`).
1771
+ * @param date - The date to serialise.
1772
+ */
1773
+ toIso8601(date) {
1774
+ return formatISO(date, { representation: 'date' });
1775
+ }
1776
+ /**
1777
+ * Returns the given value when it is a valid `Date`, or `null` for an empty string.
1778
+ * Deserialises valid ISO 8601 strings into `Date` instances.
1779
+ * Delegates all other values to the base-class implementation.
1780
+ * @param value - The raw value to deserialise.
1781
+ */
1782
+ deserialize(value) {
1783
+ if (typeof value === 'string') {
1784
+ if (!value) {
1785
+ return null;
1786
+ }
1787
+ const date = parseISO(value);
1788
+ if (this.isValid(date)) {
1789
+ return date;
1790
+ }
1791
+ }
1792
+ return super.deserialize(value);
1793
+ }
1794
+ /**
1795
+ * Returns `true` when `obj` is an instance of `Date`.
1796
+ * @param obj - The object to test.
1797
+ */
1798
+ isDateInstance(obj) {
1799
+ return isDate(obj);
1800
+ }
1801
+ /**
1802
+ * Returns `true` when `date` represents a valid point in time.
1803
+ * @param date - The date to validate.
1804
+ */
1805
+ isValid(date) {
1806
+ return isValid(date);
1807
+ }
1808
+ /**
1809
+ * Returns a sentinel `Date` that represents an invalid date (`new Date(NaN)`).
1810
+ */
1811
+ invalid() {
1812
+ return new Date(NaN);
1813
+ }
1814
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DateFnsAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1815
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DateFnsAdapter }); }
1816
+ }
1817
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DateFnsAdapter, decorators: [{
1818
+ type: Injectable
1819
+ }], ctorParameters: () => [] });
1820
+ /**
1821
+ * Standalone providers for the scm-utils date-fns adapter.
1822
+ *
1823
+ * Configures Angular Material to use {@link DateFnsAdapter} (Europe/Rome timezone)
1824
+ * and the matching {@link MAT_DATE_FNS_FORMATS}.
1825
+ *
1826
+ * @example
1827
+ * bootstrapApplication(AppComponent, {
1828
+ * providers: [provideSCMDateFns()]
1829
+ * });
1830
+ */
1831
+ function provideSCMDateFns() {
1832
+ return makeEnvironmentProviders([
1833
+ {
1834
+ provide: DateAdapter,
1835
+ useClass: DateFnsAdapter,
1836
+ deps: [MAT_DATE_LOCALE],
1837
+ },
1838
+ { provide: MAT_DATE_FORMATS, useValue: MAT_DATE_FNS_FORMATS }
1839
+ ]);
1840
+ }
1841
+
1842
+ /**
1843
+ * Directive that moves browser focus to the host element after the first render cycle.
1844
+ * Apply `autoFocus` to any focusable element to set focus automatically on initialisation.
1845
+ */
1846
+ class AutoFocusDirective {
1847
+ constructor() {
1848
+ this.elementRef = inject(ElementRef);
1849
+ afterNextRender(() => {
1850
+ this.elementRef.nativeElement?.focus();
1851
+ });
1852
+ }
1853
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: AutoFocusDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1854
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: AutoFocusDirective, isStandalone: true, selector: "[autoFocus]", ngImport: i0 }); }
1855
+ }
1856
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: AutoFocusDirective, decorators: [{
1857
+ type: Directive,
1858
+ args: [{
1859
+ selector: '[autoFocus]',
1860
+ standalone: true
1861
+ }]
1862
+ }], ctorParameters: () => [] });
1863
+
1864
+ class FileInfo {
1865
+ isValid() {
1866
+ return this.valid;
1867
+ }
1868
+ }
1869
+ class ValueModel {
1870
+ }
1871
+ class IDModel {
1872
+ }
1873
+ class GroupModel {
1874
+ }
1875
+ class DeleteModel extends GroupModel {
1876
+ }
1877
+ class RelationModel {
1878
+ }
1879
+ class UpdateRelationsModel {
1880
+ }
1881
+ class QueryModel {
1882
+ }
1883
+ class ImportModel {
1884
+ }
1885
+ class DateInterval {
1886
+ get fromAsDate() {
1887
+ if (this.from) {
1888
+ if (!(this.from instanceof Date)) {
1889
+ this.from = new Date(this.from);
1890
+ }
1891
+ if (this.from) {
1892
+ return new Date(this.from.getFullYear(), this.from.getMonth(), this.from.getDate(), 2, 0, 0);
1893
+ }
1894
+ }
1895
+ return undefined;
1896
+ }
1897
+ get toAsDate() {
1898
+ if (this.to) {
1899
+ if (!(this.to instanceof Date)) {
1900
+ this.to = new Date(this.to);
1901
+ }
1902
+ if (this.to) {
1903
+ return new Date(this.to.getFullYear(), this.to.getMonth(), this.to.getDate(), 2, 0, 0);
1904
+ }
1905
+ }
1906
+ return undefined;
1907
+ }
1908
+ constructor(from, to) {
1909
+ this.from = from;
1910
+ this.to = to;
1911
+ }
1912
+ clear() {
1913
+ this.from = undefined;
1914
+ this.to = undefined;
1915
+ }
1916
+ }
1917
+
1918
+ /**
1919
+ * Directive that listens to `keyup` events on a date input and debounces changes
1920
+ * into a {@link DateInterval} model, converting shorthand strings (e.g. "d/m") to dates.
1921
+ * Apply `[dateIntervalChange]="interval"` to the host `<input>` element.
1922
+ */
1923
+ class DateIntervalChangeDirective {
1924
+ constructor() {
1925
+ /** The date interval model to update when the input value changes. */
1926
+ this.dateIntervalChange = input(new DateInterval(null, null), /* @ts-ignore */
1927
+ ...(ngDevMode ? [{ debugName: "dateIntervalChange" }] : /* istanbul ignore next */ []));
1928
+ /** When `true`, the directive updates the interval's end date; otherwise the start date. */
1929
+ this.end = input(false, /* @ts-ignore */
1930
+ ...(ngDevMode ? [{ debugName: "end" }] : /* istanbul ignore next */ []));
1931
+ this.subject = new Subject();
1932
+ this.destroyRef = inject(DestroyRef);
1933
+ this.subject
1934
+ .pipe(debounceTime(100), takeUntilDestroyed(this.destroyRef))
1935
+ .subscribe((e) => {
1936
+ const value = e.target?.value;
1937
+ if (value !== undefined) {
1938
+ SystemUtils.changeDateInterval(value, this.dateIntervalChange(), this.end(), e.key === ' ');
1939
+ }
1940
+ });
1941
+ }
1942
+ /**
1943
+ * Handles `keyup` events on the host element.
1944
+ * Prevents default browser behaviour for the space key and forwards the event to the debounce pipeline.
1945
+ * @param e - The keyboard event emitted by the host input.
1946
+ */
1947
+ onKeyup(e) {
1948
+ if (e.key === ' ') {
1949
+ e.preventDefault();
1950
+ e.stopPropagation();
1951
+ }
1952
+ this.subject.next(e);
1953
+ }
1954
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DateIntervalChangeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1955
+ 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 }); }
1956
+ }
1957
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DateIntervalChangeDirective, decorators: [{
1958
+ type: Directive,
1959
+ args: [{
1960
+ selector: '[dateIntervalChange]',
1961
+ standalone: true,
1962
+ }]
1963
+ }], ctorParameters: () => [], propDecorators: { dateIntervalChange: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateIntervalChange", required: false }] }], end: [{ type: i0.Input, args: [{ isSignal: true, alias: "end", required: false }] }], onKeyup: [{
1964
+ type: HostListener,
1965
+ args: ['keyup', ['$event']]
1966
+ }] } });
1967
+
1968
+ /**
1969
+ * Directive that copies a string payload to the clipboard when the host element is clicked.
1970
+ * Bind `[copyClipboard]="text"` to provide the content to copy and listen to `(copied)` for confirmation.
1971
+ */
1972
+ class CopyClipboardDirective {
1973
+ constructor() {
1974
+ /** The text to copy to the clipboard. Bound via the `copyClipboard` attribute. */
1975
+ this.payload = input(undefined, { ...(ngDevMode ? { debugName: "payload" } : /* istanbul ignore next */ {}), alias: 'copyClipboard' });
1976
+ /** Emits the copied text after a successful copy operation. */
1977
+ this.copied = output({ alias: 'copied' });
1978
+ }
1979
+ /**
1980
+ * Handles click events on the host element and copies the payload to the clipboard.
1981
+ * Emits `copied` with the copied text on success.
1982
+ * @param e - The mouse click event.
1983
+ */
1984
+ onClick(e) {
1985
+ e.preventDefault();
1986
+ const payload = this.payload();
1987
+ if (!payload)
1988
+ return;
1989
+ if (SystemUtils.isBrowser()) {
1990
+ const listener = (clipEvent) => {
1991
+ clipEvent.clipboardData?.setData('text/html', payload);
1992
+ clipEvent.preventDefault();
1993
+ this.copied.emit(payload);
1994
+ };
1995
+ document.addEventListener('copy', listener, false);
1996
+ document.execCommand('copy');
1997
+ document.removeEventListener('copy', listener, false);
1998
+ }
1999
+ }
2000
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: CopyClipboardDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2001
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: CopyClipboardDirective, isStandalone: true, selector: "[copyClipboard]", inputs: { payload: { classPropertyName: "payload", publicName: "copyClipboard", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { copied: "copied" }, host: { listeners: { "click": "onClick($event)" } }, ngImport: i0 }); }
2002
+ }
2003
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: CopyClipboardDirective, decorators: [{
2004
+ type: Directive,
2005
+ args: [{
2006
+ selector: '[copyClipboard]',
2007
+ standalone: true
2008
+ }]
2009
+ }], propDecorators: { payload: [{ type: i0.Input, args: [{ isSignal: true, alias: "copyClipboard", required: false }] }], copied: [{ type: i0.Output, args: ["copied"] }], onClick: [{
2010
+ type: HostListener,
2011
+ args: ['click', ['$event']]
2012
+ }] } });
2013
+
2014
+ /**
2015
+ * Directive that validates a semicolon-separated list of email addresses.
2016
+ * Apply `emails` to a text input containing one or more addresses separated by `;`.
2017
+ */
2018
+ class EmailsValidatorDirective {
2019
+ /**
2020
+ * Validates each address in a semicolon-separated email list.
2021
+ * Returns `null` when the control is empty.
2022
+ * @param control - The form control to validate.
2023
+ */
2024
+ validate(control) {
2025
+ const input = control.value;
2026
+ if (!input || input.length === 0)
2027
+ return null;
2028
+ const parts = input.replaceAll(/\r\n/g, '').split(';');
2029
+ const isValid = parts.every(part => part.length === 0 || !!SystemUtils.parseEmail(part));
2030
+ return isValid ? null : { emails: "Elenco non valido." };
2031
+ }
2032
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: EmailsValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2033
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: EmailsValidatorDirective, isStandalone: true, selector: "[emails]", providers: [
2034
+ {
2035
+ provide: NG_VALIDATORS,
2036
+ useExisting: forwardRef(() => EmailsValidatorDirective),
2037
+ multi: true,
2038
+ },
2039
+ ], ngImport: i0 }); }
2040
+ }
2041
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: EmailsValidatorDirective, decorators: [{
2042
+ type: Directive,
2043
+ args: [{
2044
+ selector: "[emails]",
2045
+ providers: [
2046
+ {
2047
+ provide: NG_VALIDATORS,
2048
+ useExisting: forwardRef(() => EmailsValidatorDirective),
2049
+ multi: true,
2050
+ },
2051
+ ],
2052
+ standalone: true,
2053
+ }]
2054
+ }] });
2055
+
2056
+ /**
2057
+ * Directive that validates that the host control's value equals the value of another control.
2058
+ * Bind `[equals]="otherControl"`.
2059
+ */
2060
+ class EqualsValidatorDirective {
2061
+ constructor() {
2062
+ /** The control whose value must match the host control's value. */
2063
+ this.equals = input(undefined, /* @ts-ignore */
2064
+ ...(ngDevMode ? [{ debugName: "equals" }] : /* istanbul ignore next */ []));
2065
+ }
2066
+ /**
2067
+ * Validates that the host control value equals the bound control's value.
2068
+ * Returns `null` (valid) when no control is bound.
2069
+ * @param control - The form control to validate.
2070
+ */
2071
+ validate(control) {
2072
+ const eq = this.equals();
2073
+ if (!eq)
2074
+ return null;
2075
+ return eq.value === control.value ? null : { equals: "Non valido." };
2076
+ }
2077
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: EqualsValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2078
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: EqualsValidatorDirective, isStandalone: true, selector: "[equals]", inputs: { equals: { classPropertyName: "equals", publicName: "equals", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
2079
+ {
2080
+ provide: NG_VALIDATORS,
2081
+ useExisting: forwardRef(() => EqualsValidatorDirective),
2082
+ multi: true,
2083
+ },
2084
+ ], ngImport: i0 }); }
2085
+ }
2086
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: EqualsValidatorDirective, decorators: [{
2087
+ type: Directive,
2088
+ args: [{
2089
+ selector: "[equals]",
2090
+ providers: [
2091
+ {
2092
+ provide: NG_VALIDATORS,
2093
+ useExisting: forwardRef(() => EqualsValidatorDirective),
2094
+ multi: true,
2095
+ },
2096
+ ],
2097
+ standalone: true,
2098
+ }]
2099
+ }], propDecorators: { equals: [{ type: i0.Input, args: [{ isSignal: true, alias: "equals", required: false }] }] } });
2100
+
2101
+ /**
2102
+ * Directive that validates a file size against configurable minimum and maximum bounds.
2103
+ * Bind `[fileSize]` together with `[size]="fileSizeInMb"`, `[maxSizeMb]`, and `[minSizeMb]`.
2104
+ */
2105
+ class FileSizeValidatorDirective {
2106
+ constructor() {
2107
+ /** Maximum allowed file size in megabytes. Defaults to 5. */
2108
+ this.maxSizeMb = input(5, /* @ts-ignore */
2109
+ ...(ngDevMode ? [{ debugName: "maxSizeMb" }] : /* istanbul ignore next */ []));
2110
+ /** Minimum required file size in megabytes. Defaults to 0. */
2111
+ this.minSizeMb = input(0, /* @ts-ignore */
2112
+ ...(ngDevMode ? [{ debugName: "minSizeMb" }] : /* istanbul ignore next */ []));
2113
+ /** The actual file size in megabytes to validate against the bounds. */
2114
+ this.size = input(undefined, /* @ts-ignore */
2115
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2116
+ }
2117
+ /**
2118
+ * Validates that the bound file size falls within the configured min/max range.
2119
+ * Returns `null` when no control value is present.
2120
+ * @param control - The form control to validate.
2121
+ */
2122
+ validate(control) {
2123
+ const input = control.value;
2124
+ if (!input)
2125
+ return null;
2126
+ const s = this.size() ?? 0;
2127
+ const isValid = s <= this.maxSizeMb() && s >= this.minSizeMb();
2128
+ return isValid ? null : { fileSize: "Non valido." };
2129
+ }
2130
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FileSizeValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2131
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: FileSizeValidatorDirective, isStandalone: true, selector: "[fileSize]", inputs: { maxSizeMb: { classPropertyName: "maxSizeMb", publicName: "maxSizeMb", isSignal: true, isRequired: false, transformFunction: null }, minSizeMb: { classPropertyName: "minSizeMb", publicName: "minSizeMb", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
2132
+ {
2133
+ provide: NG_VALIDATORS,
2134
+ useExisting: forwardRef(() => FileSizeValidatorDirective),
2135
+ multi: true,
2136
+ },
2137
+ ], ngImport: i0 }); }
2138
+ }
2139
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FileSizeValidatorDirective, decorators: [{
2140
+ type: Directive,
2141
+ args: [{
2142
+ selector: "[fileSize]",
2143
+ providers: [
2144
+ {
2145
+ provide: NG_VALIDATORS,
2146
+ useExisting: forwardRef(() => FileSizeValidatorDirective),
2147
+ multi: true,
2148
+ },
2149
+ ],
2150
+ standalone: true,
2151
+ }]
2152
+ }], propDecorators: { maxSizeMb: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSizeMb", required: false }] }], minSizeMb: [{ type: i0.Input, args: [{ isSignal: true, alias: "minSizeMb", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
2153
+
2154
+ /**
2155
+ * Directive that validates a control value as a GUID / UUID string.
2156
+ * Apply `guid` to a text input that expects a valid UUID.
2157
+ */
2158
+ class GuidValidatorDirective {
2159
+ /**
2160
+ * Validates that the control value is a well-formed GUID / UUID.
2161
+ * Returns `null` when the control is empty.
2162
+ * @param control - The form control to validate.
2163
+ */
2164
+ validate(control) {
2165
+ const input = control.value;
2166
+ if (!input || input.length === 0)
2167
+ return null;
2168
+ return SystemUtils.parseUUID(input) ? null : { guid: "Non valido." };
2169
+ }
2170
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: GuidValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2171
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: GuidValidatorDirective, isStandalone: true, selector: "[guid]", providers: [
2172
+ {
2173
+ provide: NG_VALIDATORS,
2174
+ useExisting: forwardRef(() => GuidValidatorDirective),
2175
+ multi: true,
2176
+ },
2177
+ ], ngImport: i0 }); }
2178
+ }
2179
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: GuidValidatorDirective, decorators: [{
2180
+ type: Directive,
2181
+ args: [{
2182
+ selector: "[guid]",
2183
+ providers: [
2184
+ {
2185
+ provide: NG_VALIDATORS,
2186
+ useExisting: forwardRef(() => GuidValidatorDirective),
2187
+ multi: true,
2188
+ },
2189
+ ],
2190
+ standalone: true,
2191
+ }]
2192
+ }] });
2193
+
2194
+ /**
2195
+ * Directive that validates that a control value does not exceed a maximum word count.
2196
+ * Bind `[maxTerms]="10"` to allow at most 10 whitespace-separated terms.
2197
+ */
2198
+ class MaxTermsValidatorDirective {
2199
+ constructor() {
2200
+ /** The maximum number of whitespace-separated terms allowed. */
2201
+ this.maxTerms = input(0, /* @ts-ignore */
2202
+ ...(ngDevMode ? [{ debugName: "maxTerms" }] : /* istanbul ignore next */ []));
2203
+ }
2204
+ /**
2205
+ * Validates that the control value contains no more than the configured number of terms.
2206
+ * Returns `null` when the control is empty.
2207
+ * @param control - The form control to validate.
2208
+ */
2209
+ validate(control) {
2210
+ const input = control.value;
2211
+ if (!input)
2212
+ return null;
2213
+ const terms = input.match(/\S+/g)?.length ?? 0;
2214
+ return terms <= this.maxTerms() ? null : { maxTerms: "Non valido." };
2215
+ }
2216
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: MaxTermsValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2217
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: MaxTermsValidatorDirective, isStandalone: true, selector: "[maxTerms]", inputs: { maxTerms: { classPropertyName: "maxTerms", publicName: "maxTerms", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
2218
+ {
2219
+ provide: NG_VALIDATORS,
2220
+ useExisting: forwardRef(() => MaxTermsValidatorDirective),
2221
+ multi: true,
2222
+ },
2223
+ ], ngImport: i0 }); }
2224
+ }
2225
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: MaxTermsValidatorDirective, decorators: [{
2226
+ type: Directive,
2227
+ args: [{
2228
+ selector: "[maxTerms]",
2229
+ providers: [
2230
+ {
2231
+ provide: NG_VALIDATORS,
2232
+ useExisting: forwardRef(() => MaxTermsValidatorDirective),
2233
+ multi: true,
2234
+ },
2235
+ ],
2236
+ standalone: true,
2237
+ }]
2238
+ }], propDecorators: { maxTerms: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxTerms", required: false }] }] } });
2239
+
2240
+ /**
2241
+ * Directive that validates that a string control value is not blank (whitespace-only).
2242
+ * Apply `notEmpty` to a text input where non-blank content is required.
2243
+ */
2244
+ class NotEmptyValidatorDirective {
2245
+ /**
2246
+ * Validates that the control value is a non-blank string.
2247
+ * Returns `null` when the control is empty or not a string.
2248
+ * @param control - The form control to validate.
2249
+ */
2250
+ validate(control) {
2251
+ const input = control?.value;
2252
+ if (!input || typeof input !== 'string' || input.length === 0)
2253
+ return null;
2254
+ return input.trim().length > 0 ? null : { notEmpty: true };
2255
+ }
2256
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: NotEmptyValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2257
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: NotEmptyValidatorDirective, isStandalone: true, selector: "[notEmpty]", providers: [
2258
+ {
2259
+ provide: NG_VALIDATORS,
2260
+ useExisting: forwardRef(() => NotEmptyValidatorDirective),
2261
+ multi: true,
2262
+ },
2263
+ ], ngImport: i0 }); }
2264
+ }
2265
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: NotEmptyValidatorDirective, decorators: [{
2266
+ type: Directive,
2267
+ args: [{
2268
+ selector: "[notEmpty]",
2269
+ providers: [
2270
+ {
2271
+ provide: NG_VALIDATORS,
2272
+ useExisting: forwardRef(() => NotEmptyValidatorDirective),
2273
+ multi: true,
2274
+ },
2275
+ ],
2276
+ standalone: true,
2277
+ }]
2278
+ }] });
2279
+
2280
+ /**
2281
+ * Directive that validates that the host control's value is different from another control's value.
2282
+ * Bind `[notEqual]="otherControl"`.
2283
+ */
2284
+ class NotEqualValidatorDirective {
2285
+ constructor() {
2286
+ /** The control whose value must differ from the host control's value. */
2287
+ this.notEqual = input(undefined, /* @ts-ignore */
2288
+ ...(ngDevMode ? [{ debugName: "notEqual" }] : /* istanbul ignore next */ []));
2289
+ }
2290
+ /**
2291
+ * Validates that the host control value is not equal to the bound control's value.
2292
+ * Also clears the `notequal` error on the other control when the host becomes valid.
2293
+ * Returns `null` (valid) when no control is bound.
2294
+ * @param control - The form control to validate.
2295
+ */
2296
+ validate(control) {
2297
+ const notEqual = this.notEqual();
2298
+ if (!notEqual)
2299
+ return null;
2300
+ const isValid = (!notEqual.value && !control.value) || (notEqual.value !== control.value);
2301
+ const errors = isValid ? null : { notequal: true };
2302
+ if (errors) {
2303
+ control.markAsTouched();
2304
+ }
2305
+ else if (notEqual.hasError('notequal')) {
2306
+ notEqual.setErrors({ notequal: null });
2307
+ notEqual.updateValueAndValidity({ onlySelf: true, emitEvent: false });
2308
+ notEqual.markAsTouched();
2309
+ notEqual.markAsDirty();
2310
+ }
2311
+ return errors;
2312
+ }
2313
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: NotEqualValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2314
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: NotEqualValidatorDirective, isStandalone: true, selector: "[notEqual]", inputs: { notEqual: { classPropertyName: "notEqual", publicName: "notEqual", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
2315
+ {
2316
+ provide: NG_VALIDATORS,
2317
+ useExisting: forwardRef(() => NotEqualValidatorDirective),
2318
+ multi: true,
2319
+ },
2320
+ ], ngImport: i0 }); }
2321
+ }
2322
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: NotEqualValidatorDirective, decorators: [{
2323
+ type: Directive,
2324
+ args: [{
2325
+ selector: "[notEqual]",
2326
+ providers: [
2327
+ {
2328
+ provide: NG_VALIDATORS,
2329
+ useExisting: forwardRef(() => NotEqualValidatorDirective),
2330
+ multi: true,
2331
+ },
2332
+ ],
2333
+ standalone: true,
2334
+ }]
2335
+ }], propDecorators: { notEqual: [{ type: i0.Input, args: [{ isSignal: true, alias: "notEqual", required: false }] }] } });
2336
+
2337
+ /**
2338
+ * Directive that validates that a control value is not a future date.
2339
+ * Apply `notFuture` to a text input that expects a date on or before today.
2340
+ */
2341
+ class NotFutureValidatorDirective {
2342
+ /**
2343
+ * Validates that the control value represents a date that is not in the future.
2344
+ * Returns `null` when the control is empty.
2345
+ * @param control - The form control to validate.
2346
+ */
2347
+ validate(control) {
2348
+ const input = control.value;
2349
+ if (!input || input.length === 0)
2350
+ return null;
2351
+ const today = endOfDay(new Date());
2352
+ const d = endOfDay(SystemUtils.parseDate(input));
2353
+ return d <= today ? null : { notFuture: "Non valido." };
2354
+ }
2355
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: NotFutureValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2356
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: NotFutureValidatorDirective, isStandalone: true, selector: "[notFuture]", providers: [
2357
+ {
2358
+ provide: NG_VALIDATORS,
2359
+ useExisting: forwardRef(() => NotFutureValidatorDirective),
2360
+ multi: true,
2361
+ },
2362
+ ], ngImport: i0 }); }
2363
+ }
2364
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: NotFutureValidatorDirective, decorators: [{
2365
+ type: Directive,
2366
+ args: [{
2367
+ selector: "[notFuture]",
2368
+ providers: [
2369
+ {
2370
+ provide: NG_VALIDATORS,
2371
+ useExisting: forwardRef(() => NotFutureValidatorDirective),
2372
+ multi: true,
2373
+ },
2374
+ ],
2375
+ standalone: true,
2376
+ }]
2377
+ }] });
2378
+
2379
+ /**
2380
+ * Directive that validates a control value as a sufficiently strong password.
2381
+ * Apply `password` to a password input.
2382
+ */
2383
+ class PasswordValidatorDirective {
2384
+ /**
2385
+ * Validates that the control value meets the minimum password-strength requirements.
2386
+ * @param control - The form control to validate.
2387
+ */
2388
+ validate(control) {
2389
+ const input = control.value ?? '';
2390
+ const strength = SystemUtils.calculatePasswordStrength(input);
2391
+ return strength.isValid ? null : { password: "Non valido." };
2392
+ }
2393
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PasswordValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2394
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: PasswordValidatorDirective, isStandalone: true, selector: "[password]", providers: [
2395
+ {
2396
+ provide: NG_VALIDATORS,
2397
+ useExisting: forwardRef(() => PasswordValidatorDirective),
2398
+ multi: true,
2399
+ },
2400
+ ], ngImport: i0 }); }
2401
+ }
2402
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PasswordValidatorDirective, decorators: [{
2403
+ type: Directive,
2404
+ args: [{
2405
+ selector: "[password]",
2406
+ providers: [
2407
+ {
2408
+ provide: NG_VALIDATORS,
2409
+ useExisting: forwardRef(() => PasswordValidatorDirective),
2410
+ multi: true,
2411
+ },
2412
+ ],
2413
+ standalone: true,
2414
+ }]
2415
+ }] });
2416
+
2417
+ /**
2418
+ * Directive that removes focus from the host element after it is clicked,
2419
+ * preventing the browser from keeping a visible focus ring post-interaction.
2420
+ * Apply `removeFocus` to any focusable element (e.g. a button).
2421
+ */
2422
+ class RemoveFocusDirective {
2423
+ constructor() {
2424
+ this.elementRef = inject(ElementRef);
2425
+ }
2426
+ /**
2427
+ * Handles click events on the host element and blurs it on the next event-loop tick
2428
+ * so that the click action completes before focus is removed.
2429
+ */
2430
+ onClick() {
2431
+ const el = this.elementRef.nativeElement;
2432
+ if (el && typeof el.blur === 'function') {
2433
+ setTimeout(() => el.blur(), 0);
2434
+ }
2435
+ }
2436
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: RemoveFocusDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2437
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: RemoveFocusDirective, isStandalone: true, selector: "[removeFocus]", host: { listeners: { "click": "onClick()" } }, ngImport: i0 }); }
2438
+ }
2439
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: RemoveFocusDirective, decorators: [{
2440
+ type: Directive,
2441
+ args: [{
2442
+ selector: '[removeFocus]',
2443
+ standalone: true
2444
+ }]
2445
+ }], propDecorators: { onClick: [{
2446
+ type: HostListener,
2447
+ args: ['click']
2448
+ }] } });
2449
+
2450
+ /**
2451
+ * Directive that validates a control value as a parseable SQL-compatible date string.
2452
+ * Apply `sqlDate` to a text input that expects a date after year 1750.
2453
+ */
2454
+ class SqlDateValidatorDirective {
2455
+ /**
2456
+ * Validates that the control value can be parsed as a date after 1750.
2457
+ * Returns `null` when the control is empty.
2458
+ * @param control - The form control to validate.
2459
+ */
2460
+ validate(control) {
2461
+ const input = control.value;
2462
+ if (!input || input.length === 0)
2463
+ return null;
2464
+ const d = endOfDay(SystemUtils.parseDate(input));
2465
+ return d.getFullYear() > 1750 ? null : { sqlDate: "Non valido." };
2466
+ }
2467
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SqlDateValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2468
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: SqlDateValidatorDirective, isStandalone: true, selector: "[sqlDate]", providers: [
2469
+ {
2470
+ provide: NG_VALIDATORS,
2471
+ useExisting: forwardRef(() => SqlDateValidatorDirective),
2472
+ multi: true,
2473
+ },
2474
+ ], ngImport: i0 }); }
2475
+ }
2476
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SqlDateValidatorDirective, decorators: [{
2477
+ type: Directive,
2478
+ args: [{
2479
+ selector: "[sqlDate]",
2480
+ providers: [
2481
+ {
2482
+ provide: NG_VALIDATORS,
2483
+ useExisting: forwardRef(() => SqlDateValidatorDirective),
2484
+ multi: true,
2485
+ },
2486
+ ],
2487
+ standalone: true,
2488
+ }]
2489
+ }] });
2490
+
2491
+ /**
2492
+ * Directive that validates a time string against optional allowed time slot ranges.
2493
+ * Bind `[time]` and optionally `[slots]="'08:00-12:00|14:00-18:00'"` (pipe-separated ranges).
2494
+ */
2495
+ class TimeValidatorDirective {
2496
+ constructor() {
2497
+ /** Optional pipe-separated list of allowed time ranges, e.g. `"08:00-12:00|14:00-18:00"`. */
2498
+ this.slots = input(undefined, /* @ts-ignore */
2499
+ ...(ngDevMode ? [{ debugName: "slots" }] : /* istanbul ignore next */ []));
2500
+ }
2501
+ /**
2502
+ * Parses a `"HH:MM"` time string into a comparable integer (e.g. `"09:30"` -> `930`).
2503
+ * Returns `-1` when the string is not a valid time.
2504
+ * @param value - The time string to parse.
2505
+ */
2506
+ getTime(value) {
2507
+ const p = value.split(':');
2508
+ if (p.length !== 2)
2509
+ return -1;
2510
+ const hh = parseInt(p[0]);
2511
+ if (hh < 0 || hh > 23)
2512
+ return -1;
2513
+ const mm = parseInt(p[1]);
2514
+ if (mm < 0 || mm > 59)
2515
+ return -1;
2516
+ return parseInt(p[0] + p[1]);
2517
+ }
2518
+ /**
2519
+ * Validates that the control value is a valid time string and, when slots are configured,
2520
+ * that it falls within at least one of the allowed ranges.
2521
+ * Returns `null` when the control is empty.
2522
+ * @param control - The form control to validate.
2523
+ */
2524
+ validate(control) {
2525
+ const input = control.value;
2526
+ if (!input || input.length === 0)
2527
+ return null;
2528
+ const t = this.getTime(input);
2529
+ if (t === -1)
2530
+ return { time: "Non valido." };
2531
+ const slotsValue = this.slots();
2532
+ if (slotsValue) {
2533
+ const isValid = slotsValue.split('|').some(s => {
2534
+ const t1 = this.getTime(s.substring(0, 5));
2535
+ const t2 = this.getTime(s.substring(6));
2536
+ return t1 !== -1 && t2 !== -1 && t1 <= t && t2 >= t;
2537
+ });
2538
+ return isValid ? null : { time: "Non valido." };
2539
+ }
2540
+ return null;
2541
+ }
2542
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: TimeValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2543
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: TimeValidatorDirective, isStandalone: true, selector: "[time]", inputs: { slots: { classPropertyName: "slots", publicName: "slots", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
2544
+ {
2545
+ provide: NG_VALIDATORS,
2546
+ useExisting: forwardRef(() => TimeValidatorDirective),
2547
+ multi: true,
2548
+ },
2549
+ ], ngImport: i0 }); }
2550
+ }
2551
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: TimeValidatorDirective, decorators: [{
2552
+ type: Directive,
2553
+ args: [{
2554
+ selector: "[time]",
2555
+ providers: [
2556
+ {
2557
+ provide: NG_VALIDATORS,
2558
+ useExisting: forwardRef(() => TimeValidatorDirective),
2559
+ multi: true,
2560
+ },
2561
+ ],
2562
+ standalone: true,
2563
+ }]
2564
+ }], propDecorators: { slots: [{ type: i0.Input, args: [{ isSignal: true, alias: "slots", required: false }] }] } });
2565
+
2566
+ /**
2567
+ * Directive that validates a control value as a well-formed URL.
2568
+ * Apply `url` to a text input that expects a URL.
2569
+ */
2570
+ class UrlValidatorDirective {
2571
+ /**
2572
+ * Validates that the control value is a well-formed URL.
2573
+ * Returns `null` (valid) when the control is empty.
2574
+ * @param control - The form control to validate.
2575
+ */
2576
+ validate(control) {
2577
+ const input = control.value;
2578
+ if (!input || input.length === 0)
2579
+ return null;
2580
+ return SystemUtils.parseUrl(input) ? null : { url: "Non valido." };
2581
+ }
2582
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: UrlValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2583
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: UrlValidatorDirective, isStandalone: true, selector: "[url]", providers: [
2584
+ {
2585
+ provide: NG_VALIDATORS,
2586
+ useExisting: forwardRef(() => UrlValidatorDirective),
2587
+ multi: true,
2588
+ },
2589
+ ], ngImport: i0 }); }
2590
+ }
2591
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: UrlValidatorDirective, decorators: [{
2592
+ type: Directive,
2593
+ args: [{
2594
+ selector: "[url]",
2595
+ providers: [
2596
+ {
2597
+ provide: NG_VALIDATORS,
2598
+ useExisting: forwardRef(() => UrlValidatorDirective),
2599
+ multi: true,
2600
+ },
2601
+ ],
2602
+ standalone: true,
2603
+ }]
2604
+ }] });
2605
+
2606
+ /**
2607
+ * Directive that validates a control using the host object's `isValid()` method
2608
+ * or a boolean expression passed via `[validIf]`.
2609
+ */
2610
+ class ValidIfDirective {
2611
+ constructor() {
2612
+ /** When `true`, the control is considered valid regardless of the bound value. */
2613
+ this.validIf = input(false, /* @ts-ignore */
2614
+ ...(ngDevMode ? [{ debugName: "validIf" }] : /* istanbul ignore next */ []));
2615
+ }
2616
+ /**
2617
+ * Validates the control value against a boolean flag or the value's own `isValid()` method.
2618
+ * @param control - The form control to validate.
2619
+ */
2620
+ validate(control) {
2621
+ let isValid = false;
2622
+ const c = control.value ? control.value : null;
2623
+ if (!c) {
2624
+ isValid = this.validIf() === true;
2625
+ }
2626
+ else {
2627
+ try {
2628
+ isValid = c.isValid();
2629
+ }
2630
+ catch { }
2631
+ }
2632
+ return isValid ? null : { validIf: "Non valido." };
2633
+ }
2634
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ValidIfDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2635
+ 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: [
2636
+ {
2637
+ provide: NG_VALIDATORS,
2638
+ useExisting: forwardRef(() => ValidIfDirective),
2639
+ multi: true,
2640
+ },
2641
+ ], ngImport: i0 }); }
2642
+ }
2643
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ValidIfDirective, decorators: [{
2644
+ type: Directive,
2645
+ args: [{
2646
+ selector: "[validIf]",
2647
+ providers: [
2648
+ {
2649
+ provide: NG_VALIDATORS,
2650
+ useExisting: forwardRef(() => ValidIfDirective),
2651
+ multi: true,
2652
+ },
2653
+ ],
2654
+ standalone: true,
2655
+ }]
2656
+ }], propDecorators: { validIf: [{ type: i0.Input, args: [{ isSignal: true, alias: "validIf", required: false }] }] } });
2657
+
2658
+ /**
2659
+ * Directive that delegates validation to an externally provided validator function.
2660
+ * Bind `[validator]="myFn"` where `myFn` is `(c: AbstractControl) => ValidationErrors | null`.
2661
+ */
2662
+ class ValidatorDirective {
2663
+ constructor() {
2664
+ /** The custom validator function to apply. */
2665
+ this.validator = input(undefined, /* @ts-ignore */
2666
+ ...(ngDevMode ? [{ debugName: "validator" }] : /* istanbul ignore next */ []));
2667
+ }
2668
+ /**
2669
+ * Invokes the provided validator function against the given control.
2670
+ * Returns `null` (valid) when no function is bound.
2671
+ * @param control - The form control to validate.
2672
+ */
2673
+ validate(control) {
2674
+ const fn = this.validator();
2675
+ return fn ? fn(control) : null;
2676
+ }
2677
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2678
+ 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 }); }
2679
+ }
2680
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ValidatorDirective, decorators: [{
2681
+ type: Directive,
2682
+ args: [{
2683
+ selector: '[validator]',
2684
+ providers: [{ provide: NG_VALIDATORS, useExisting: ValidatorDirective, multi: true }],
2685
+ standalone: true,
2686
+ }]
2687
+ }], propDecorators: { validator: [{ type: i0.Input, args: [{ isSignal: true, alias: "validator", required: false }] }] } });
2688
+
2689
+ /**
2690
+ * Pipe that converts a Markdown string to sanitized HTML using `SystemUtils.markdownToHtml`.
2691
+ *
2692
+ * Usage: `{{ text | formatMarkdown }}`
2693
+ */
2694
+ class FormatMarkdownPipe {
2695
+ constructor() {
2696
+ this.sanitizer = inject(DomSanitizer);
2697
+ }
2698
+ /**
2699
+ * Transforms a Markdown string into sanitized HTML.
2700
+ * @param value - The Markdown input to convert. Treated as an empty string when `undefined`.
2701
+ * @returns A `SafeHtml` value that can be rendered with `[innerHTML]`.
2702
+ */
2703
+ transform(value) {
2704
+ return this.sanitizer.bypassSecurityTrustHtml(SystemUtils.markdownToHtml(value ?? ''));
2705
+ }
2706
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatMarkdownPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
2707
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: FormatMarkdownPipe, isStandalone: true, name: "formatMarkdown" }); }
2708
+ }
2709
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: FormatMarkdownPipe, decorators: [{
2710
+ type: Pipe,
2711
+ args: [{
2712
+ name: 'formatMarkdown',
2713
+ standalone: true
2714
+ }]
2715
+ }] });
2716
+
2717
+ const Breakpoints = {
2718
+ XXSmall: '(max-width: 349.98px)',
2719
+ XSmall: '(max-width: 599.98px)',
2720
+ Small: '(min-width: 600px) and (max-width: 959.98px)',
2721
+ SmallMedium: '(min-width: 600px) and (max-width: 1059.98px)',
2722
+ Medium: '(min-width: 960px) and (max-width: 1279.98px)',
2723
+ MediumLarge: '(min-width: 960px) and (max-width: 1459.98px)',
2724
+ Large: '(min-width: 1460px) and (max-width: 1919.98px)'
2725
+ };
2726
+
2727
+ const UtilsMessages = {
2728
+ /**
2729
+ * Messages
2730
+ */
2731
+ // Select dialog
2732
+ UTILS_DIALOGS_SELECT_OPTIONS_CHANGED: '§utils-dialogs-select-options-changed'
2733
+ };
2734
+
2735
+ /**
2736
+ * Generic selection model that tracks a set of selected items identified by a lookup field.
2737
+ * Wraps Angular CDK's `SelectionModel` and adds lookup-based add/remove logic.
2738
+ *
2739
+ * @typeParam T - The item type held in the selection.
2740
+ * @typeParam V - The type of the lookup key used to identify items.
2741
+ */
2742
+ class SelectableModel {
2743
+ /**
2744
+ * Snapshot of all items currently tracked by this model (selected or previously toggled).
2745
+ * Backed by a signal — reads are always up to date.
2746
+ */
2747
+ get all() {
2748
+ return this._all();
2749
+ }
2750
+ /** The underlying CDK `SelectionModel` (provides `.selected`, `.isSelected`, etc.). */
2751
+ get current() {
2752
+ return this._current;
2753
+ }
2754
+ /**
2755
+ * @param allowMultiSelect - When `true` (default), multiple items can be selected simultaneously.
2756
+ * @param lookupFieldName - Name of the note used as the unique key when searching the internal list (default: `'id'`).
2757
+ */
2758
+ constructor(allowMultiSelect = true, lookupFieldName = 'id') {
2759
+ /**
2760
+ * Emits whenever the selection changes.
2761
+ * Carries the affected item, or `undefined` when the whole selection is cleared.
2762
+ */
2763
+ this.changed = new EventEmitter();
2764
+ this._all = signal([], /* @ts-ignore */
2765
+ ...(ngDevMode ? [{ debugName: "_all" }] : /* istanbul ignore next */ []));
2766
+ /** Signal that is `true` when at least one item is selected. */
2767
+ this.hasValue = computed(() => this._all().length > 0, /* @ts-ignore */
2768
+ ...(ngDevMode ? [{ debugName: "hasValue" }] : /* istanbul ignore next */ []));
2769
+ this._current = new SelectionModel(allowMultiSelect, []);
2770
+ this._lookupFieldName = lookupFieldName;
2771
+ }
2772
+ /**
2773
+ * Toggles the CDK selection state of an item that already exists in the tracked list.
2774
+ * Has no effect when `lookupValue` does not match any tracked item.
2775
+ * @param item - The item whose selection state should be toggled.
2776
+ * @param lookupValue - The key value used to locate the item in the internal list.
2777
+ */
2778
+ updateCurrent(item, lookupValue) {
2779
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
2780
+ if (p !== -1) {
2781
+ this._current.toggle(item);
2782
+ this.changed.emit(item);
2783
+ }
2784
+ }
2785
+ /**
2786
+ * Toggles an item in both the internal list and the CDK selection.
2787
+ * Adds the item when it is not yet tracked; removes it when it is.
2788
+ * @param item - The item to toggle.
2789
+ * @param lookupValue - The key value used to locate or register the item.
2790
+ */
2791
+ toggle(item, lookupValue) {
2792
+ if (lookupValue === undefined)
2793
+ return;
2794
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
2795
+ if (p === -1) {
2796
+ this._all.update(arr => [...arr, item]);
2797
+ }
2798
+ else {
2799
+ this._all.update(arr => arr.filter((_, i) => i !== p));
2800
+ }
2801
+ this._current.toggle(item);
2802
+ this.changed.emit(item);
2803
+ }
2804
+ /**
2805
+ * Adds an item to the internal list (when not already present) and marks it as selected.
2806
+ * @param item - The item to select.
2807
+ * @param lookupValue - The key value used to locate or register the item.
2808
+ */
2809
+ select(item, lookupValue) {
2810
+ if (lookupValue === undefined)
2811
+ return;
2812
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
2813
+ if (p === -1) {
2814
+ this._all.update(arr => [...arr, item]);
2815
+ }
2816
+ this._current.select(item);
2817
+ this.changed.emit(item);
2818
+ }
2819
+ /**
2820
+ * Removes an item from the internal list and deselects it in the CDK model.
2821
+ * Has no effect when the item is not currently tracked.
2822
+ * @param item - The item to deselect.
2823
+ * @param lookupValue - The key value used to locate the item in the internal list.
2824
+ */
2825
+ deselect(item, lookupValue) {
2826
+ if (lookupValue === undefined)
2827
+ return;
2828
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue);
2829
+ if (p !== -1) {
2830
+ this._all.update(arr => arr.filter((_, i) => i !== p));
2831
+ this._current.deselect(item);
2832
+ this.changed.emit(item);
2833
+ }
2834
+ }
2835
+ /**
2836
+ * Deselects all items whose lookup key is contained in `lookupValues`.
2837
+ * @param lookupValues - Array of key values identifying the items to deselect.
2838
+ */
2839
+ deselectByValues(lookupValues) {
2840
+ if (lookupValues.length === 0)
2841
+ return;
2842
+ for (const key of lookupValues) {
2843
+ const p = SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, key);
2844
+ if (p !== -1) {
2845
+ const item = this._all()[p];
2846
+ this._all.update(arr => arr.filter((_, i) => i !== p));
2847
+ this._current.deselect(item);
2848
+ this.changed.emit(item);
2849
+ }
2850
+ }
2851
+ }
2852
+ /**
2853
+ * Runs `clearFunc` against every currently selected item, then clears the CDK selection.
2854
+ * The internal tracked list is **not** affected; only the CDK selection is cleared.
2855
+ * @param clearFunc - Callback invoked for each currently selected item before clearing.
2856
+ */
2857
+ clearCurrent(clearFunc) {
2858
+ for (const item of this._current.selected) {
2859
+ clearFunc(item);
2860
+ }
2861
+ this._current.clear();
2862
+ this.changed.emit(undefined);
2863
+ }
2864
+ /**
2865
+ * Clears both the internal tracked list and the CDK selection.
2866
+ */
2867
+ clear() {
2868
+ this._all.set([]);
2869
+ this._current.clear();
2870
+ this.changed.emit(undefined);
2871
+ }
2872
+ /**
2873
+ * Returns `true` when the item identified by `lookupValue` is present in the tracked list.
2874
+ * @param lookupValue - The key value to look up.
2875
+ */
2876
+ isSelected(lookupValue) {
2877
+ return (lookupValue !== undefined &&
2878
+ SystemUtils.arrayFindIndexByKey(this._all(), this._lookupFieldName, lookupValue) !== -1);
2879
+ }
2880
+ }
2881
+
2882
+ /**
2883
+ * Standard Broadcast Messages Manager
2884
+ * https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel
2885
+ */
2886
+ /**
2887
+ * Wrapper around the native Web `BroadcastChannel` API that adds typed messaging,
2888
+ * multiple named subscriptions, and a graceful disposal mechanism.
2889
+ *
2890
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel
2891
+ */
2892
+ class BroadcastChannelManager {
2893
+ /**
2894
+ * The name of the underlying `BroadcastChannel`, or `undefined` when the API
2895
+ * is not supported by the current browser.
2896
+ */
2897
+ get currentBus() {
2898
+ return this.channel?.name;
2899
+ }
2900
+ /**
2901
+ * Opens a `BroadcastChannel` with the given name (when the API is available).
2902
+ * @param bus - The channel name to open (default: `'ARS-CHANNEL'`).
2903
+ */
2904
+ constructor(bus = 'ARS-CHANNEL') {
2905
+ this.subject = new Subject();
2906
+ this.subscriptions = [];
2907
+ if (typeof window !== 'undefined' && 'BroadcastChannel' in window) {
2908
+ this.channel = new BroadcastChannel(bus);
2909
+ this.channel.onmessageerror = (e) => {
2910
+ console.error('[BroadcastChannelManager] Message receive error', e);
2911
+ };
2912
+ this.channel.onmessage = (e) => {
2913
+ const bag = e.data;
2914
+ if (!bag)
2915
+ return;
2916
+ this.subject.next(bag);
2917
+ const info = this.subscriptions.find(m => m.messageId === bag.messageId);
2918
+ info?.action(bag);
2919
+ };
2920
+ }
2921
+ else {
2922
+ console.error('[BroadcastChannelManager] BroadcastChannel API is not supported in this browser');
2923
+ }
2924
+ }
2925
+ /**
2926
+ * Closes the underlying channel and completes the internal subject after a short delay
2927
+ * to allow any in-flight messages to be delivered.
2928
+ */
2929
+ dispose() {
2930
+ setTimeout(() => {
2931
+ this.subscriptions = [];
2932
+ this.channel?.close();
2933
+ this.channel = undefined;
2934
+ this.subject.complete();
2935
+ }, 1000);
2936
+ }
2937
+ /** @internal Implementation that handles all overloads. */
2938
+ sendMessage(messageIdOrBag, dataOrDelay, delay) {
2939
+ // Disambiguate overloads based on the first argument:
2940
+ // - string => (messageId, data?, delay?)
2941
+ // - object => (bag, delay?)
2942
+ let bag;
2943
+ let effectiveDelay;
2944
+ if (typeof messageIdOrBag === 'string') {
2945
+ bag = { messageId: messageIdOrBag, data: dataOrDelay };
2946
+ effectiveDelay = delay ?? 0;
2947
+ }
2948
+ else {
2949
+ bag = messageIdOrBag;
2950
+ effectiveDelay = (typeof dataOrDelay === 'number' ? dataOrDelay : 0);
2951
+ }
2952
+ const post = () => this.channel?.postMessage(bag);
2953
+ if (effectiveDelay > 0) {
2954
+ setTimeout(post, effectiveDelay);
2955
+ }
2956
+ else {
2957
+ post();
2958
+ }
2959
+ }
2960
+ /**
2961
+ * Registers a handler for messages with the given `messageId`.
2962
+ * If a handler for the same `messageId` is already registered it is replaced.
2963
+ * @param args - The subscription descriptor containing the message ID and handler.
2964
+ */
2965
+ subscribe(args) {
2966
+ const i = this.subscriptions.findIndex(m => m.messageId === args.messageId);
2967
+ if (i === -1) {
2968
+ this.subscriptions.push(args);
2969
+ }
2970
+ else {
2971
+ this.subscriptions[i].action = args.action;
2972
+ }
2973
+ }
2974
+ /**
2975
+ * Removes the subscription for the given message ID, if one exists.
2976
+ * @param messageId - The message type identifier whose subscription should be removed.
2977
+ */
2978
+ unsubscribe(messageId) {
2979
+ const i = this.subscriptions.findIndex(m => m.messageId === messageId);
2980
+ if (i !== -1) {
2981
+ this.subscriptions.splice(i, 1);
2982
+ }
2983
+ }
2984
+ /**
2985
+ * Removes all registered subscriptions.
2986
+ */
2987
+ unsubscribeAll() {
2988
+ this.subscriptions = [];
2989
+ }
2990
+ }
2991
+
2992
+ /**
2993
+ * Application-level messaging service that combines two transports:
2994
+ * - An in-process RxJS `Subject` for same-tab communication (`sendMessage` / `getMessage`).
2995
+ * - A native `BroadcastChannel` for cross-tab communication (`sendChannelMessage` / `subscribeChannelMessage`).
2996
+ */
2997
+ class BroadcastService {
2998
+ constructor() {
2999
+ this.subject = new Subject();
3000
+ this.channel = new BroadcastChannelManager('ARS-CHANNEL');
3001
+ }
3002
+ /**
3003
+ * Creates a new standalone `BroadcastChannelManager` instance bound to the shared ARS channel.
3004
+ * Useful when a caller needs direct channel access outside the service.
3005
+ * @returns A new `BroadcastChannelManager` connected to `'ARS-CHANNEL'`.
3006
+ */
3007
+ static createChannel() {
3008
+ return new BroadcastChannelManager('ARS-CHANNEL');
3009
+ }
3010
+ ngOnDestroy() {
3011
+ this.channel?.dispose();
3012
+ this.subject.complete();
3013
+ }
3014
+ /**
3015
+ * Publishes a message to the in-process subject, optionally after a delay.
3016
+ * @param id - The message type identifier.
3017
+ * @param data - Optional payload to attach to the message.
3018
+ * @param delay - Milliseconds to wait before publishing (default: `0`).
3019
+ */
3020
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3021
+ sendMessage(id, data, delay = 0) {
3022
+ if (delay > 0) {
3023
+ setTimeout(() => this.subject.next({ id, data }), delay);
3024
+ }
3025
+ else {
3026
+ this.subject.next({ id, data });
3027
+ }
3028
+ }
3029
+ /**
3030
+ * Publishes a typed message over the native `BroadcastChannel` (cross-tab), optionally after a delay.
3031
+ * @param id - The message type identifier.
3032
+ * @param data - Optional typed payload to send.
3033
+ * @param delay - Milliseconds to wait before sending (forwarded to the channel manager).
3034
+ */
3035
+ sendChannelMessage(id, data, delay) {
3036
+ this.channel.sendMessage(id, data, delay);
3037
+ }
3038
+ /**
3039
+ * Registers a handler that is invoked whenever a channel message with the given `id` is received.
3040
+ * @param id - The message type identifier to listen for.
3041
+ * @param action - Callback invoked with the full `BroadcastChannelMessageBag` when a matching message arrives.
3042
+ */
3043
+ subscribeChannelMessage(id, action) {
3044
+ this.channel.subscribe({ messageId: id, action });
3045
+ }
3046
+ /**
3047
+ * Removes the channel subscription previously registered for the given `id`, if any.
3048
+ * @param id - The message type identifier whose subscription should be removed.
3049
+ */
3050
+ unsubscribeChannelMessage(id) {
3051
+ this.channel.unsubscribe(id);
3052
+ }
3053
+ /**
3054
+ * Returns an `Observable` that emits every in-process message published via `sendMessage`.
3055
+ * @returns An observable stream of `BroadcastMessageInfo` objects.
3056
+ */
3057
+ getMessage() {
3058
+ return this.subject.asObservable();
3059
+ }
3060
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: BroadcastService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
3061
+ static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.1", ngImport: i0, type: BroadcastService }); }
3062
+ }
3063
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: BroadcastService, decorators: [{
3064
+ type: Service
3065
+ }] });
3066
+
3067
+ /**
3068
+ * Application-wide environment configuration service.
3069
+ * Exposes writable signals for the base URIs used throughout the application.
3070
+ * Values should be set once during application bootstrap (e.g. in `APP_INITIALIZER`).
3071
+ */
3072
+ class EnvironmentService {
3073
+ constructor() {
3074
+ /** Writable signal holding the base URI of the frontend application (e.g. `https://app.example.com`). */
3075
+ this.appUriSignal = signal('', /* @ts-ignore */
3076
+ ...(ngDevMode ? [{ debugName: "appUriSignal" }] : /* istanbul ignore next */ []));
3077
+ /** Writable signal holding the base URI of the backend API service (e.g. `https://api.example.com`). */
3078
+ this.appServiceUriSignal = signal('', /* @ts-ignore */
3079
+ ...(ngDevMode ? [{ debugName: "appServiceUriSignal" }] : /* istanbul ignore next */ []));
3080
+ /**
3081
+ * Writable signal holding the redirect URI used after login.
3082
+ * When empty, `appLoginRedirectUri` falls back to `appUri`.
3083
+ */
3084
+ this.appLoginRedirectUriSignal = signal('', /* @ts-ignore */
3085
+ ...(ngDevMode ? [{ debugName: "appLoginRedirectUriSignal" }] : /* istanbul ignore next */ []));
3086
+ /**
3087
+ * Writable signal holding the login endpoint URI of the backend service.
3088
+ * When empty, `appServiceLoginUri` falls back to `appServiceUri`.
3089
+ */
3090
+ this.appServiceLoginUriSignal = signal('', /* @ts-ignore */
3091
+ ...(ngDevMode ? [{ debugName: "appServiceLoginUriSignal" }] : /* istanbul ignore next */ []));
3092
+ // ── Computed: fallback logic ──────────────────────────────────────────────
3093
+ this._effectiveLoginRedirectUri = computed(() => this.appLoginRedirectUriSignal() || this.appUriSignal(), /* @ts-ignore */
3094
+ ...(ngDevMode ? [{ debugName: "_effectiveLoginRedirectUri" }] : /* istanbul ignore next */ []));
3095
+ this._effectiveServiceLoginUri = computed(() => this.appServiceLoginUriSignal() || this.appServiceUriSignal(), /* @ts-ignore */
3096
+ ...(ngDevMode ? [{ debugName: "_effectiveServiceLoginUri" }] : /* istanbul ignore next */ []));
3097
+ }
3098
+ // ── Getter / setter API (kept for backward compatibility) ─────────────────
3099
+ /** Base URI of the frontend application. */
3100
+ get appUri() { return this.appUriSignal(); }
3101
+ /** @param value - The base URI of the frontend application. */
3102
+ set appUri(value) { this.appUriSignal.set(value); }
3103
+ /** Base URI of the backend API service. */
3104
+ get appServiceUri() { return this.appServiceUriSignal(); }
3105
+ /** @param value - The base URI of the backend API service. */
3106
+ set appServiceUri(value) { this.appServiceUriSignal.set(value); }
3107
+ /**
3108
+ * Effective login redirect URI.
3109
+ * Returns the explicitly set value, or falls back to `appUri` when empty.
3110
+ */
3111
+ get appLoginRedirectUri() { return this._effectiveLoginRedirectUri(); }
3112
+ /** @param value - The redirect URI to use after login. */
3113
+ set appLoginRedirectUri(value) { this.appLoginRedirectUriSignal.set(value); }
3114
+ /**
3115
+ * Effective service login URI.
3116
+ * Returns the explicitly set value, or falls back to `appServiceUri` when empty.
3117
+ */
3118
+ get appServiceLoginUri() { return this._effectiveServiceLoginUri(); }
3119
+ /** @param value - The login endpoint URI of the backend service. */
3120
+ set appServiceLoginUri(value) { this.appServiceLoginUriSignal.set(value); }
3121
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: EnvironmentService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
3122
+ static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.1", ngImport: i0, type: EnvironmentService }); }
3123
+ }
3124
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: EnvironmentService, decorators: [{
3125
+ type: Service
3126
+ }] });
3127
+
3128
+ /**
3129
+ * Service that exposes reactive screen and device-capability information.
3130
+ */
3131
+ class ScreenService {
3132
+ constructor() {
3133
+ /**
3134
+ * Writable signal that holds the current Material breakpoint alias
3135
+ * (e.g. `'xs'`, `'sm'`, `'md'`, `'lg'`, `'xl'`).
3136
+ * Updated externally by the breakpoint observer in the consuming component or module.
3137
+ */
3138
+ this.mq = signal('', /* @ts-ignore */
3139
+ ...(ngDevMode ? [{ debugName: "mq" }] : /* istanbul ignore next */ []));
3140
+ }
3141
+ /**
3142
+ * Returns `true` when the primary input mechanism can hover over elements
3143
+ * with coarse pointer precision (i.e. a touch screen).
3144
+ */
3145
+ get isTouchable() {
3146
+ return SystemUtils.isTouchable();
3147
+ }
3148
+ /**
3149
+ * Returns `true` when the current browser is Internet Explorer or the legacy Edge (EdgeHTML).
3150
+ * Modern Chromium-based Edge reports a different user-agent and will return `false`.
3151
+ */
3152
+ get isIEOrEdge() {
3153
+ return /msie\s|trident\/|edge\//i.test(window.navigator.userAgent);
3154
+ }
3155
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ScreenService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
3156
+ static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.1", ngImport: i0, type: ScreenService }); }
3157
+ }
3158
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ScreenService, decorators: [{
3159
+ type: Service
3160
+ }] });
3161
+
3162
+ /**
3163
+ * Application-level theme management service.
3164
+ *
3165
+ * Responsibilities:
3166
+ * - Persists the user's preferred theme in `localStorage`.
3167
+ * - Applies `'light'` or `'dark'` CSS class to `<body>`.
3168
+ * - Reacts to OS-level colour-scheme changes when the theme is set to `'auto'`.
3169
+ * - Synchronises theme changes across browser tabs via `BroadcastChannel`.
3170
+ * - Exposes reactive signals (`themeIcon`, `themeName`, `toggleTooltip`) for the UI.
3171
+ */
3172
+ class ThemeService {
3173
+ constructor() {
3174
+ this.broadcastChannel = new BroadcastChannelManager('THEME-SERVICE');
3175
+ this.broadcastMessage = '$theme-changed';
3176
+ this.prefersColorSchemeMediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
3177
+ this.themeChanged = new BehaviorSubject('auto');
3178
+ this.renderFactory2 = inject(RendererFactory2);
3179
+ this.renderer = this.renderFactory2.createRenderer(null, null);
3180
+ /** The currently selected `ThemeType` (may be `'auto'`). */
3181
+ this.theme = signal('light', /* @ts-ignore */
3182
+ ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
3183
+ /** `true` when the theme is set to follow the system preference. */
3184
+ this.auto = computed(() => this.theme() === 'auto', /* @ts-ignore */
3185
+ ...(ngDevMode ? [{ debugName: "auto" }] : /* istanbul ignore next */ []));
3186
+ this.themeInfo = {
3187
+ auto: { icon: 'routine', name: 'Tema di sistema', next: 'light', tooltip: 'Passa a tema chiaro' },
3188
+ light: { icon: 'light_mode', name: 'Tema chiaro', next: 'dark', tooltip: 'Passa a tema scuro' },
3189
+ dark: { icon: 'dark_mode', name: 'Tema scuro', next: 'auto', tooltip: 'Passa a tema di sistema' },
3190
+ };
3191
+ /** Reactive Material icon code for the current theme (use with `<mat-icon>`). */
3192
+ this.themeIcon = computed(() => this.themeInfo[this.theme()].icon, /* @ts-ignore */
3193
+ ...(ngDevMode ? [{ debugName: "themeIcon" }] : /* istanbul ignore next */ []));
3194
+ /** Reactive human-readable name of the current theme. */
3195
+ this.themeName = computed(() => this.themeInfo[this.theme()].name, /* @ts-ignore */
3196
+ ...(ngDevMode ? [{ debugName: "themeName" }] : /* istanbul ignore next */ []));
3197
+ /** Reactive tooltip text for the theme toggle button. */
3198
+ this.toggleTooltip = computed(() => this.themeInfo[this.theme()].tooltip, /* @ts-ignore */
3199
+ ...(ngDevMode ? [{ debugName: "toggleTooltip" }] : /* istanbul ignore next */ []));
3200
+ this.changed = this.themeChanged.asObservable();
3201
+ }
3202
+ /**
3203
+ * Initialises the service: loads the preferred theme, registers OS-change listener,
3204
+ * and subscribes to cross-tab broadcast messages.
3205
+ * Call this once during application bootstrap (e.g. in `APP_INITIALIZER`).
3206
+ * @param theme - The initial theme to apply (defaults to the value stored in `localStorage`).
3207
+ */
3208
+ initialize(theme = this.getPreferredTheme()) {
3209
+ this.prefersColorSchemeMediaQueryList.onchange = () => {
3210
+ if (this.auto())
3211
+ this.setTheme(this.theme());
3212
+ };
3213
+ this.broadcastChannel.subscribe({
3214
+ messageId: this.broadcastMessage,
3215
+ action: (bag) => {
3216
+ if (bag.data && this.theme() !== bag.data) {
3217
+ this.setTheme(bag.data);
3218
+ }
3219
+ }
3220
+ });
3221
+ this.setTheme(theme);
3222
+ }
3223
+ ngOnDestroy() {
3224
+ this.broadcastChannel.dispose();
3225
+ }
3226
+ /**
3227
+ * Advances the theme through the cycle: `auto` → `light` → `dark` → `auto`.
3228
+ */
3229
+ toggleTheme() {
3230
+ this.setTheme(this.themeInfo[this.theme()].next);
3231
+ }
3232
+ /**
3233
+ * Returns the resolved effective theme (`'light'` or `'dark'`), taking the OS preference
3234
+ * into account when the current mode is `'auto'`.
3235
+ * @returns `'light'` or `'dark'` — never `'auto'`.
3236
+ */
3237
+ getTheme() {
3238
+ if (this.auto()) {
3239
+ return this.prefersColorSchemeMediaQueryList.matches ? 'dark' : 'light';
3240
+ }
3241
+ return this.getPreferredTheme();
3242
+ }
3243
+ /**
3244
+ * Reads the theme stored in `localStorage` and validates it.
3245
+ * Falls back to `'auto'` when the stored value is missing or invalid.
3246
+ * @returns The persisted `ThemeType`, or `'auto'` as default.
3247
+ */
3248
+ getPreferredTheme() {
3249
+ const stored = localStorage.getItem('preferred-theme');
3250
+ return stored && this.isTheme(stored) ? stored : 'auto';
3251
+ }
3252
+ /**
3253
+ * Returns `true` when `value` is a valid `ThemeType` string.
3254
+ * Acts as a TypeScript type guard.
3255
+ * @param value - The string to check.
3256
+ */
3257
+ isTheme(value) {
3258
+ return value === 'auto' || value === 'light' || value === 'dark';
3259
+ }
3260
+ /**
3261
+ * Applies the given theme to `<body>` (CSS class), persists it to `localStorage`,
3262
+ * notifies the `changed` observable, and broadcasts the change to other tabs.
3263
+ * @param theme - The `ThemeType` to apply (defaults to `'auto'`).
3264
+ */
3265
+ setTheme(theme = 'auto') {
3266
+ this.renderer.removeClass(document.body, 'light');
3267
+ this.renderer.removeClass(document.body, 'dark');
3268
+ this.theme.set(theme);
3269
+ localStorage.setItem('preferred-theme', theme);
3270
+ const effectiveClass = this.auto()
3271
+ ? (this.prefersColorSchemeMediaQueryList.matches ? 'dark' : 'light')
3272
+ : theme;
3273
+ this.renderer.addClass(document.body, effectiveClass);
3274
+ this.themeChanged.next(this.getTheme());
3275
+ this.broadcastChannel.sendMessage(this.broadcastMessage, theme);
3276
+ }
3277
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
3278
+ static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.1", ngImport: i0, type: ThemeService }); }
3279
+ }
3280
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ThemeService, decorators: [{
3281
+ type: Service
3282
+ }], ctorParameters: () => [] });
3283
+
3284
+ /*
3285
+ * Public API Surface of scm-utils
3286
+ */
3287
+
3288
+ /**
3289
+ * Generated bundle index. Do not edit.
3290
+ */
3291
+
3292
+ 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, SqlDateValidatorDirective, SystemUtils, ThemeService, TimeValidatorDirective, UpdateRelationsModel, UrlValidatorDirective, UtilsMessages, ValidIfDirective, ValidatorDirective, ValueModel, provideSCMCore, provideSCMDateFns };
3293
+ //# sourceMappingURL=fabio.buscaroli-scm-utils-core.mjs.map